Cole Medin commited on
Commit
f30612d
·
unverified ·
2 Parent(s): 9b97837 d18517a

Merge branch 'main' into respect-provider-choice

Browse files
.env.example CHANGED
@@ -43,5 +43,10 @@ OPENAI_LIKE_API_KEY=
43
  # You only need this environment variable set if you want to use Mistral models
44
  MISTRAL_API_KEY=
45
 
 
 
 
 
 
46
  # Include this environment variable if you want more logging for debugging locally
47
  VITE_LOG_LEVEL=debug
 
43
  # You only need this environment variable set if you want to use Mistral models
44
  MISTRAL_API_KEY=
45
 
46
+ # Get your xAI API key
47
+ # https://x.ai/api
48
+ # You only need this environment variable set if you want to use xAI models
49
+ XAI_API_KEY=
50
+
51
  # Include this environment variable if you want more logging for debugging locally
52
  VITE_LOG_LEVEL=debug
app/components/chat/APIKeyManager.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+
4
+ interface APIKeyManagerProps {
5
+ provider: string;
6
+ apiKey: string;
7
+ setApiKey: (key: string) => void;
8
+ }
9
+
10
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
11
+ const [isEditing, setIsEditing] = useState(false);
12
+ const [tempKey, setTempKey] = useState(apiKey);
13
+
14
+ const handleSave = () => {
15
+ setApiKey(tempKey);
16
+ setIsEditing(false);
17
+ };
18
+
19
+ return (
20
+ <div className="flex items-center gap-2 mt-2 mb-2">
21
+ <span className="text-sm text-bolt-elements-textSecondary">{provider} API Key:</span>
22
+ {isEditing ? (
23
+ <>
24
+ <input
25
+ type="password"
26
+ value={tempKey}
27
+ onChange={(e) => setTempKey(e.target.value)}
28
+ className="flex-1 p-1 text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
29
+ />
30
+ <IconButton onClick={handleSave} title="Save API Key">
31
+ <div className="i-ph:check" />
32
+ </IconButton>
33
+ <IconButton onClick={() => setIsEditing(false)} title="Cancel">
34
+ <div className="i-ph:x" />
35
+ </IconButton>
36
+ </>
37
+ ) : (
38
+ <>
39
+ <span className="flex-1 text-sm text-bolt-elements-textPrimary">
40
+ {apiKey ? '••••••••' : 'Not set'}
41
+ </span>
42
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
43
+ <div className="i-ph:pencil-simple" />
44
+ </IconButton>
45
+ </>
46
+ )}
47
+ </div>
48
+ );
49
+ };
app/components/chat/BaseChat.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
- import React, { type RefCallback } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
@@ -11,6 +11,8 @@ import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
 
 
14
 
15
  import styles from './BaseChat.module.scss';
16
 
@@ -26,15 +28,15 @@ const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
26
 
27
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
28
  return (
29
- <div className="mb-2">
30
- <select
31
  value={provider}
32
  onChange={(e) => {
33
  setProvider(e.target.value);
34
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
35
  setModel(firstModel ? firstModel.name : '');
36
  }}
37
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
38
  >
39
  {providerList.map((provider) => (
40
  <option key={provider} value={provider}>
@@ -51,7 +53,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
51
  <select
52
  value={model}
53
  onChange={(e) => setModel(e.target.value)}
54
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
55
  >
56
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
57
  <option key={modelOption.name} value={modelOption.name}>
@@ -111,6 +113,41 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
111
  ref,
112
  ) => {
113
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  return (
116
  <div
@@ -125,11 +162,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
125
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
126
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
127
  {!chatStarted && (
128
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
129
- <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
130
  Where ideas begin
131
  </h1>
132
- <p className="mb-4 text-center text-bolt-elements-textSecondary">
133
  Bring ideas to life in seconds or get help on existing projects.
134
  </p>
135
  </div>
@@ -163,15 +200,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
163
  provider={provider}
164
  setProvider={setProvider}
165
  providerList={providerList}
 
 
 
 
 
 
 
166
  />
167
  <div
168
  className={classNames(
169
- 'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
170
  )}
171
  >
172
  <textarea
173
  ref={textareaRef}
174
- className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
175
  onKeyDown={(event) => {
176
  if (event.key === 'Enter') {
177
  if (event.shiftKey) {
@@ -210,12 +254,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
210
  />
211
  )}
212
  </ClientOnly>
213
- <div className="flex justify-between text-sm p-4 pt-2">
214
  <div className="flex gap-1 items-center">
215
  <IconButton
216
  title="Enhance prompt"
217
  disabled={input.length === 0 || enhancingPrompt}
218
- className={classNames({
219
  'opacity-100!': enhancingPrompt,
220
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
221
  promptEnhanced,
@@ -224,7 +268,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
224
  >
225
  {enhancingPrompt ? (
226
  <>
227
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
228
  <div className="ml-1.5">Enhancing prompt...</div>
229
  </>
230
  ) : (
@@ -237,7 +281,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
237
  </div>
238
  {input.length > 3 ? (
239
  <div className="text-xs text-bolt-elements-textTertiary">
240
- Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
241
  </div>
242
  ) : null}
243
  </div>
@@ -271,4 +315,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
271
  </div>
272
  );
273
  },
274
- );
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
+ import React, { type RefCallback, useEffect } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
 
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
14
+ import { APIKeyManager } from './APIKeyManager';
15
+ import Cookies from 'js-cookie';
16
 
17
  import styles from './BaseChat.module.scss';
18
 
 
28
 
29
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
30
  return (
31
+ <div className="mb-2 flex gap-2">
32
+ <select
33
  value={provider}
34
  onChange={(e) => {
35
  setProvider(e.target.value);
36
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
+ className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
40
  >
41
  {providerList.map((provider) => (
42
  <option key={provider} value={provider}>
 
53
  <select
54
  value={model}
55
  onChange={(e) => setModel(e.target.value)}
56
+ className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
57
  >
58
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
59
  <option key={modelOption.name} value={modelOption.name}>
 
113
  ref,
114
  ) => {
115
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
116
+ const [provider, setProvider] = useState(DEFAULT_PROVIDER);
117
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
118
+
119
+ useEffect(() => {
120
+ // Load API keys from cookies on component mount
121
+ try {
122
+ const storedApiKeys = Cookies.get('apiKeys');
123
+ if (storedApiKeys) {
124
+ const parsedKeys = JSON.parse(storedApiKeys);
125
+ if (typeof parsedKeys === 'object' && parsedKeys !== null) {
126
+ setApiKeys(parsedKeys);
127
+ }
128
+ }
129
+ } catch (error) {
130
+ console.error('Error loading API keys from cookies:', error);
131
+ // Clear invalid cookie data
132
+ Cookies.remove('apiKeys');
133
+ }
134
+ }, []);
135
+
136
+ const updateApiKey = (provider: string, key: string) => {
137
+ try {
138
+ const updatedApiKeys = { ...apiKeys, [provider]: key };
139
+ setApiKeys(updatedApiKeys);
140
+ // Save updated API keys to cookies with 30 day expiry and secure settings
141
+ Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
142
+ expires: 30, // 30 days
143
+ secure: true, // Only send over HTTPS
144
+ sameSite: 'strict', // Protect against CSRF
145
+ path: '/' // Accessible across the site
146
+ });
147
+ } catch (error) {
148
+ console.error('Error saving API keys to cookies:', error);
149
+ }
150
+ };
151
 
152
  return (
153
  <div
 
162
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
163
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
164
  {!chatStarted && (
165
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
166
+ <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
167
  Where ideas begin
168
  </h1>
169
+ <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
170
  Bring ideas to life in seconds or get help on existing projects.
171
  </p>
172
  </div>
 
200
  provider={provider}
201
  setProvider={setProvider}
202
  providerList={providerList}
203
+ provider={provider}
204
+ setProvider={setProvider}
205
+ />
206
+ <APIKeyManager
207
+ provider={provider}
208
+ apiKey={apiKeys[provider] || ''}
209
+ setApiKey={(key) => updateApiKey(provider, key)}
210
  />
211
  <div
212
  className={classNames(
213
+ 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
214
  )}
215
  >
216
  <textarea
217
  ref={textareaRef}
218
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
219
  onKeyDown={(event) => {
220
  if (event.key === 'Enter') {
221
  if (event.shiftKey) {
 
254
  />
255
  )}
256
  </ClientOnly>
257
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
258
  <div className="flex gap-1 items-center">
259
  <IconButton
260
  title="Enhance prompt"
261
  disabled={input.length === 0 || enhancingPrompt}
262
+ className={classNames('transition-all', {
263
  'opacity-100!': enhancingPrompt,
264
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
265
  promptEnhanced,
 
268
  >
269
  {enhancingPrompt ? (
270
  <>
271
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
272
  <div className="ml-1.5">Enhancing prompt...</div>
273
  </>
274
  ) : (
 
281
  </div>
282
  {input.length > 3 ? (
283
  <div className="text-xs text-bolt-elements-textTertiary">
284
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for a new line
285
  </div>
286
  ) : null}
287
  </div>
 
315
  </div>
316
  );
317
  },
318
+ );
app/components/chat/Chat.client.tsx CHANGED
@@ -15,6 +15,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
 
18
 
19
  const toastAnimation = cssTransition({
20
  enter: 'animated fadeInRight',
@@ -80,8 +81,13 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
80
 
81
  const [animationScope, animate] = useAnimate();
82
 
 
 
83
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
84
  api: '/api/chat',
 
 
 
85
  onError: (error) => {
86
  logger.error('Request failed\n\n', error);
87
  toast.error('There was an error processing your request');
@@ -203,6 +209,13 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
203
 
204
  const [messageRef, scrollRef] = useSnapScroll();
205
 
 
 
 
 
 
 
 
206
  return (
207
  <BaseChat
208
  ref={animationScope}
 
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
18
+ import Cookies from 'js-cookie';
19
 
20
  const toastAnimation = cssTransition({
21
  enter: 'animated fadeInRight',
 
81
 
82
  const [animationScope, animate] = useAnimate();
83
 
84
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
85
+
86
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
87
  api: '/api/chat',
88
+ body: {
89
+ apiKeys
90
+ },
91
  onError: (error) => {
92
  logger.error('Request failed\n\n', error);
93
  toast.error('There was an error processing your request');
 
209
 
210
  const [messageRef, scrollRef] = useSnapScroll();
211
 
212
+ useEffect(() => {
213
+ const storedApiKeys = Cookies.get('apiKeys');
214
+ if (storedApiKeys) {
215
+ setApiKeys(JSON.parse(storedApiKeys));
216
+ }
217
+ }, []);
218
+
219
  return (
220
  <BaseChat
221
  ref={animationScope}
app/lib/.server/llm/api-key.ts CHANGED
@@ -2,12 +2,18 @@
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
- export function getAPIKey(cloudflareEnv: Env, provider: string) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
 
 
 
 
 
 
11
  switch (provider) {
12
  case 'Anthropic':
13
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
@@ -25,6 +31,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string) {
25
  return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
26
  case "OpenAILike":
27
  return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
 
 
28
  default:
29
  return "";
30
  }
 
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
+ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
11
+ // First check user-provided API keys
12
+ if (userApiKeys?.[provider]) {
13
+ return userApiKeys[provider];
14
+ }
15
+
16
+ // Fall back to environment variables
17
  switch (provider) {
18
  case 'Anthropic':
19
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
 
31
  return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
32
  case "OpenAILike":
33
  return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
34
+ case "xAI":
35
+ return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
36
  default:
37
  return "";
38
  }
app/lib/.server/llm/model.ts CHANGED
@@ -58,7 +58,10 @@ export function getGroqModel(apiKey: string, model: string) {
58
  }
59
 
60
  export function getOllamaModel(baseURL: string, model: string) {
61
- let Ollama = ollama(model);
 
 
 
62
  Ollama.config.baseURL = `${baseURL}/api`;
63
  return Ollama;
64
  }
@@ -80,8 +83,16 @@ export function getOpenRouterModel(apiKey: string, model: string) {
80
  return openRouter.chat(model);
81
  }
82
 
83
- export function getModel(provider: string, model: string, env: Env) {
84
- const apiKey = getAPIKey(env, provider);
 
 
 
 
 
 
 
 
85
  const baseURL = getBaseURL(env, provider);
86
 
87
  switch (provider) {
@@ -101,6 +112,8 @@ export function getModel(provider: string, model: string, env: Env) {
101
  return getDeepseekModel(apiKey, model)
102
  case 'Mistral':
103
  return getMistralModel(apiKey, model);
 
 
104
  default:
105
  return getOllamaModel(baseURL, model);
106
  }
 
58
  }
59
 
60
  export function getOllamaModel(baseURL: string, model: string) {
61
+ let Ollama = ollama(model, {
62
+ numCtx: 32768,
63
+ });
64
+
65
  Ollama.config.baseURL = `${baseURL}/api`;
66
  return Ollama;
67
  }
 
83
  return openRouter.chat(model);
84
  }
85
 
86
+ export function getXAIModel(apiKey: string, model: string) {
87
+ const openai = createOpenAI({
88
+ baseURL: 'https://api.x.ai/v1',
89
+ apiKey,
90
+ });
91
+
92
+ return openai(model);
93
+ }
94
+ export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
95
+ const apiKey = getAPIKey(env, provider, apiKeys);
96
  const baseURL = getBaseURL(env, provider);
97
 
98
  switch (provider) {
 
112
  return getDeepseekModel(apiKey, model)
113
  case 'Mistral':
114
  return getMistralModel(apiKey, model);
115
+ case 'xAI':
116
+ return getXAIModel(apiKey, model);
117
  default:
118
  return getOllamaModel(baseURL, model);
119
  }
app/lib/.server/llm/stream-text.ts CHANGED
@@ -42,7 +42,12 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
42
  return { model, provider, content: cleanedContent };
43
  }
44
 
45
- export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
 
 
 
 
 
46
  let currentModel = DEFAULT_MODEL;
47
  let currentProvider = DEFAULT_PROVIDER;
48
 
@@ -63,7 +68,7 @@ export function streamText(messages: Messages, env: Env, options?: StreamingOpti
63
  });
64
 
65
  return _streamText({
66
- model: getModel(currentProvider, currentModel, env),
67
  system: getSystemPrompt(),
68
  maxTokens: MAX_TOKENS,
69
  messages: convertToCoreMessages(processedMessages),
 
42
  return { model, provider, content: cleanedContent };
43
  }
44
 
45
+ export function streamText(
46
+ messages: Messages,
47
+ env: Env,
48
+ options?: StreamingOptions,
49
+ apiKeys?: Record<string, string>
50
+ ) {
51
  let currentModel = DEFAULT_MODEL;
52
  let currentProvider = DEFAULT_PROVIDER;
53
 
 
68
  });
69
 
70
  return _streamText({
71
+ model: getModel(currentProvider, currentModel, env, apiKeys),
72
  system: getSystemPrompt(),
73
  maxTokens: MAX_TOKENS,
74
  messages: convertToCoreMessages(processedMessages),
app/routes/api.chat.ts CHANGED
@@ -11,13 +11,17 @@ export async function action(args: ActionFunctionArgs) {
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
- const { messages } = await request.json<{ messages: Messages }>();
 
 
 
15
 
16
  const stream = new SwitchableStream();
17
 
18
  try {
19
  const options: StreamingOptions = {
20
  toolChoice: 'none',
 
21
  onFinish: async ({ text: content, finishReason }) => {
22
  if (finishReason !== 'length') {
23
  return stream.close();
@@ -40,7 +44,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
40
  },
41
  };
42
 
43
- const result = await streamText(messages, context.cloudflare.env, options);
44
 
45
  stream.switchSource(result.toAIStream());
46
 
@@ -52,6 +56,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
52
  });
53
  } catch (error) {
54
  console.log(error);
 
 
 
 
 
 
 
55
 
56
  throw new Response(null, {
57
  status: 500,
 
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
+ const { messages, apiKeys } = await request.json<{
15
+ messages: Messages,
16
+ apiKeys: Record<string, string>
17
+ }>();
18
 
19
  const stream = new SwitchableStream();
20
 
21
  try {
22
  const options: StreamingOptions = {
23
  toolChoice: 'none',
24
+ apiKeys,
25
  onFinish: async ({ text: content, finishReason }) => {
26
  if (finishReason !== 'length') {
27
  return stream.close();
 
44
  },
45
  };
46
 
47
+ const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
48
 
49
  stream.switchSource(result.toAIStream());
50
 
 
56
  });
57
  } catch (error) {
58
  console.log(error);
59
+
60
+ if (error.message?.includes('API key')) {
61
+ throw new Response('Invalid or missing API key', {
62
+ status: 401,
63
+ statusText: 'Unauthorized'
64
+ });
65
+ }
66
 
67
  throw new Response(null, {
68
  status: 500,
app/utils/constants.ts CHANGED
@@ -16,6 +16,7 @@ const staticModels: ModelInfo[] = [
16
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
17
  { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
18
  { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
 
19
  { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter' },
20
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
21
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
@@ -33,8 +34,9 @@ const staticModels: ModelInfo[] = [
33
  { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
34
  { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI' },
35
  { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI' },
36
- { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek' },
37
- { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek' },
 
38
  { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral' },
39
  { name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral' },
40
  { name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral' },
@@ -109,4 +111,4 @@ async function initializeModelList(): Promise<void> {
109
  MODEL_LIST = [...ollamaModels, ...openAiLikeModels, ...staticModels];
110
  }
111
  initializeModelList().then();
112
- export { getOllamaModels, getOpenAILikeModels, initializeModelList };
 
16
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
17
  { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
18
  { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
19
+ { name: 'x-ai/grok-beta', label: "xAI Grok Beta (OpenRouter)", provider: 'OpenRouter' },
20
  { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter' },
21
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
22
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
 
34
  { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
35
  { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI' },
36
  { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI' },
37
+ { name: 'grok-beta', label: "xAI Grok Beta", provider: 'xAI' },
38
+ { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek'},
39
+ { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek'},
40
  { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral' },
41
  { name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral' },
42
  { name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral' },
 
111
  MODEL_LIST = [...ollamaModels, ...openAiLikeModels, ...staticModels];
112
  }
113
  initializeModelList().then();
114
+ export { getOllamaModels, getOpenAILikeModels, initializeModelList };
package.json CHANGED
@@ -28,8 +28,8 @@
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
31
- "@ai-sdk/openai": "^0.0.66",
32
  "@ai-sdk/mistral": "^0.0.43",
 
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
@@ -71,6 +71,7 @@
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
 
74
  "jszip": "^3.10.1",
75
  "nanostores": "^0.10.3",
76
  "ollama-ai-provider": "^0.15.2",
@@ -94,6 +95,7 @@
94
  "@remix-run/dev": "^2.10.0",
95
  "@types/diff": "^5.2.1",
96
  "@types/file-saver": "^2.0.7",
 
97
  "@types/react": "^18.2.20",
98
  "@types/react-dom": "^18.2.7",
99
  "fast-glob": "^3.3.2",
 
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
 
31
  "@ai-sdk/mistral": "^0.0.43",
32
+ "@ai-sdk/openai": "^0.0.66",
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
 
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
74
+ "js-cookie": "^3.0.5",
75
  "jszip": "^3.10.1",
76
  "nanostores": "^0.10.3",
77
  "ollama-ai-provider": "^0.15.2",
 
95
  "@remix-run/dev": "^2.10.0",
96
  "@types/diff": "^5.2.1",
97
  "@types/file-saver": "^2.0.7",
98
+ "@types/js-cookie": "^3.0.6",
99
  "@types/react": "^18.2.20",
100
  "@types/react-dom": "^18.2.7",
101
  "fast-glob": "^3.3.2",
pnpm-lock.yaml CHANGED
@@ -146,6 +146,9 @@ importers:
146
  jose:
147
  specifier: ^5.6.3
148
  version: 5.6.3
 
 
 
149
  jszip:
150
  specifier: ^3.10.1
151
  version: 3.10.1
@@ -210,6 +213,9 @@ importers:
210
  '@types/file-saver':
211
  specifier: ^2.0.7
212
  version: 2.0.7
 
 
 
213
  '@types/react':
214
  specifier: ^18.2.20
215
  version: 18.3.3
@@ -1872,6 +1878,9 @@ packages:
1872
  '@types/[email protected]':
1873
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
1874
 
 
 
 
1875
  '@types/[email protected]':
1876
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1877
 
@@ -3455,6 +3464,10 @@ packages:
3455
3456
  resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
3457
 
 
 
 
 
3458
3459
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
3460
 
@@ -7248,6 +7261,8 @@ snapshots:
7248
  dependencies:
7249
  '@types/unist': 3.0.2
7250
 
 
 
7251
  '@types/[email protected]': {}
7252
 
7253
  '@types/[email protected]':
@@ -9211,6 +9226,8 @@ snapshots:
9211
 
9212
9213
 
 
 
9214
9215
 
9216
 
146
  jose:
147
  specifier: ^5.6.3
148
  version: 5.6.3
149
+ js-cookie:
150
+ specifier: ^3.0.5
151
+ version: 3.0.5
152
  jszip:
153
  specifier: ^3.10.1
154
  version: 3.10.1
 
213
  '@types/file-saver':
214
  specifier: ^2.0.7
215
  version: 2.0.7
216
+ '@types/js-cookie':
217
+ specifier: ^3.0.6
218
+ version: 3.0.6
219
  '@types/react':
220
  specifier: ^18.2.20
221
  version: 18.3.3
 
1878
  '@types/[email protected]':
1879
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
1880
 
1881
+ '@types/[email protected]':
1882
+ resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
1883
+
1884
  '@types/[email protected]':
1885
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1886
 
 
3464
3465
  resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
3466
 
3467
3468
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
3469
+ engines: {node: '>=14'}
3470
+
3471
3472
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
3473
 
 
7261
  dependencies:
7262
  '@types/unist': 3.0.2
7263
 
7264
+ '@types/[email protected]': {}
7265
+
7266
  '@types/[email protected]': {}
7267
 
7268
  '@types/[email protected]':
 
9226
 
9227
9228
 
9229
9230
+
9231
9232
 
9233