Skip to content

Commit e196442

Browse files
authored
feat: configure dynamic providers via .env (#1108)
* Use backend API route to fetch dynamic models # Conflicts: # app/components/chat/BaseChat.tsx * Override ApiKeys if provided in frontend * Remove obsolete artifact * Transport api keys from client to server in header * Cache static provider information * Restore reading provider settings from cookie * Reload only a single provider on api key change * Transport apiKeys and providerSettings via cookies. While doing this, introduce a simple helper function for cookies
1 parent 87ff810 commit e196442

File tree

8 files changed

+164
-150
lines changed

8 files changed

+164
-150
lines changed

app/components/chat/BaseChat.tsx

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
* Preventing TS checks with files presented in the video for a better presentation.
44
*/
55
import type { Message } from 'ai';
6-
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
6+
import React, { type RefCallback, useEffect, useState } from 'react';
77
import { ClientOnly } from 'remix-utils/client-only';
88
import { Menu } from '~/components/sidebar/Menu.client';
99
import { IconButton } from '~/components/ui/IconButton';
1010
import { Workbench } from '~/components/workbench/Workbench.client';
1111
import { classNames } from '~/utils/classNames';
12-
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
12+
import { PROVIDER_LIST } from '~/utils/constants';
1313
import { Messages } from './Messages.client';
1414
import { SendButton } from './SendButton.client';
1515
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
@@ -25,13 +25,13 @@ import GitCloneButton from './GitCloneButton';
2525
import FilePreview from './FilePreview';
2626
import { ModelSelector } from '~/components/chat/ModelSelector';
2727
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28-
import type { IProviderSetting, ProviderInfo } from '~/types/model';
28+
import type { ProviderInfo } from '~/types/model';
2929
import { ScreenshotStateManager } from './ScreenshotStateManager';
3030
import { toast } from 'react-toastify';
3131
import StarterTemplates from './StarterTemplates';
3232
import type { ActionAlert } from '~/types/actions';
3333
import ChatAlert from './ChatAlert';
34-
import { LLMManager } from '~/lib/modules/llm/manager';
34+
import type { ModelInfo } from '~/lib/modules/llm/types';
3535

3636
const TEXTAREA_MIN_HEIGHT = 76;
3737

@@ -102,35 +102,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
102102
) => {
103103
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
104104
const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
105-
const [modelList, setModelList] = useState(MODEL_LIST);
105+
const [modelList, setModelList] = useState<ModelInfo[]>([]);
106106
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
107107
const [isListening, setIsListening] = useState(false);
108108
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109109
const [transcript, setTranscript] = useState('');
110110
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
111111

112-
const getProviderSettings = useCallback(() => {
113-
let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
114-
115-
try {
116-
const savedProviderSettings = Cookies.get('providers');
117-
118-
if (savedProviderSettings) {
119-
const parsedProviderSettings = JSON.parse(savedProviderSettings);
120-
121-
if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
122-
providerSettings = parsedProviderSettings;
123-
}
124-
}
125-
} catch (error) {
126-
console.error('Error loading Provider Settings from cookies:', error);
127-
128-
// Clear invalid cookie data
129-
Cookies.remove('providers');
130-
}
131-
132-
return providerSettings;
133-
}, []);
134112
useEffect(() => {
135113
console.log(transcript);
136114
}, [transcript]);
@@ -169,25 +147,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
169147

170148
useEffect(() => {
171149
if (typeof window !== 'undefined') {
172-
const providerSettings = getProviderSettings();
173150
let parsedApiKeys: Record<string, string> | undefined = {};
174151

175152
try {
176153
parsedApiKeys = getApiKeysFromCookies();
177154
setApiKeys(parsedApiKeys);
178155
} catch (error) {
179156
console.error('Error loading API keys from cookies:', error);
180-
181-
// Clear invalid cookie data
182157
Cookies.remove('apiKeys');
183158
}
159+
184160
setIsModelLoading('all');
185-
initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
186-
.then((modelList) => {
187-
setModelList(modelList);
161+
fetch('/api/models')
162+
.then((response) => response.json())
163+
.then((data) => {
164+
const typedData = data as { modelList: ModelInfo[] };
165+
setModelList(typedData.modelList);
188166
})
189167
.catch((error) => {
190-
console.error('Error initializing model list:', error);
168+
console.error('Error fetching model list:', error);
191169
})
192170
.finally(() => {
193171
setIsModelLoading(undefined);
@@ -200,29 +178,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
200178
setApiKeys(newApiKeys);
201179
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
202180

203-
const provider = LLMManager.getInstance(import.meta.env || process.env || {}).getProvider(providerName);
181+
setIsModelLoading(providerName);
204182

205-
if (provider && provider.getDynamicModels) {
206-
setIsModelLoading(providerName);
183+
let providerModels: ModelInfo[] = [];
207184

208-
try {
209-
const providerSettings = getProviderSettings();
210-
const staticModels = provider.staticModels;
211-
const dynamicModels = await provider.getDynamicModels(
212-
newApiKeys,
213-
providerSettings,
214-
import.meta.env || process.env || {},
215-
);
216-
217-
setModelList((preModels) => {
218-
const filteredOutPreModels = preModels.filter((x) => x.provider !== providerName);
219-
return [...filteredOutPreModels, ...staticModels, ...dynamicModels];
220-
});
221-
} catch (error) {
222-
console.error('Error loading dynamic models:', error);
223-
}
224-
setIsModelLoading(undefined);
185+
try {
186+
const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);
187+
const data = await response.json();
188+
providerModels = (data as { modelList: ModelInfo[] }).modelList;
189+
} catch (error) {
190+
console.error('Error loading dynamic models for:', providerName, error);
225191
}
192+
193+
// Only update models for the specific provider
194+
setModelList((prevModels) => {
195+
const otherModels = prevModels.filter((model) => model.provider !== providerName);
196+
return [...otherModels, ...providerModels];
197+
});
198+
setIsModelLoading(undefined);
226199
};
227200

228201
const startListening = () => {

app/lib/api/cookies.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export function parseCookies(cookieHeader: string | null) {
2+
const cookies: Record<string, string> = {};
3+
4+
if (!cookieHeader) {
5+
return cookies;
6+
}
7+
8+
// Split the cookie string by semicolons and spaces
9+
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
10+
11+
items.forEach((item) => {
12+
const [name, ...rest] = item.split('=');
13+
14+
if (name && rest.length > 0) {
15+
// Decode the name and value, and join value parts in case it contains '='
16+
const decodedName = decodeURIComponent(name.trim());
17+
const decodedValue = decodeURIComponent(rest.join('=').trim());
18+
cookies[decodedName] = decodedValue;
19+
}
20+
});
21+
22+
return cookies;
23+
}
24+
25+
export function getApiKeysFromCookie(cookieHeader: string | null): Record<string, string> {
26+
const cookies = parseCookies(cookieHeader);
27+
return cookies.apiKeys ? JSON.parse(cookies.apiKeys) : {};
28+
}
29+
30+
export function getProviderSettingsFromCookie(cookieHeader: string | null): Record<string, any> {
31+
const cookies = parseCookies(cookieHeader);
32+
return cookies.providers ? JSON.parse(cookies.providers) : {};
33+
}

app/lib/modules/llm/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class LLMManager {
8383

8484
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
8585

86-
if (providerSettings) {
86+
if (providerSettings && Object.keys(providerSettings).length > 0) {
8787
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
8888
}
8989

app/routes/api.enhancer.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,13 @@
11
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2-
3-
//import { StreamingTextResponse, parseStreamPart } from 'ai';
42
import { streamText } from '~/lib/.server/llm/stream-text';
53
import { stripIndents } from '~/utils/stripIndent';
6-
import type { IProviderSetting, ProviderInfo } from '~/types/model';
4+
import type { ProviderInfo } from '~/types/model';
5+
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
76

87
export async function action(args: ActionFunctionArgs) {
98
return enhancerAction(args);
109
}
1110

12-
function parseCookies(cookieHeader: string) {
13-
const cookies: any = {};
14-
15-
// Split the cookie string by semicolons and spaces
16-
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
17-
18-
items.forEach((item) => {
19-
const [name, ...rest] = item.split('=');
20-
21-
if (name && rest) {
22-
// Decode the name and value, and join value parts in case it contains '='
23-
const decodedName = decodeURIComponent(name.trim());
24-
const decodedValue = decodeURIComponent(rest.join('=').trim());
25-
cookies[decodedName] = decodedValue;
26-
}
27-
});
28-
29-
return cookies;
30-
}
31-
3211
async function enhancerAction({ context, request }: ActionFunctionArgs) {
3312
const { message, model, provider } = await request.json<{
3413
message: string;
@@ -55,12 +34,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
5534
}
5635

5736
const cookieHeader = request.headers.get('Cookie');
58-
59-
// Parse the cookie's value (returns an object or null if no cookie exists)
60-
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
61-
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
62-
parseCookies(cookieHeader || '').providers || '{}',
63-
);
37+
const apiKeys = getApiKeysFromCookie(cookieHeader);
38+
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
6439

6540
try {
6641
const result = await streamText({

app/routes/api.llmcall.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
11
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2-
3-
//import { StreamingTextResponse, parseStreamPart } from 'ai';
42
import { streamText } from '~/lib/.server/llm/stream-text';
53
import type { IProviderSetting, ProviderInfo } from '~/types/model';
64
import { generateText } from 'ai';
7-
import { getModelList, PROVIDER_LIST } from '~/utils/constants';
5+
import { PROVIDER_LIST } from '~/utils/constants';
86
import { MAX_TOKENS } from '~/lib/.server/llm/constants';
7+
import { LLMManager } from '~/lib/modules/llm/manager';
8+
import type { ModelInfo } from '~/lib/modules/llm/types';
9+
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
910

1011
export async function action(args: ActionFunctionArgs) {
1112
return llmCallAction(args);
1213
}
1314

14-
function parseCookies(cookieHeader: string) {
15-
const cookies: any = {};
16-
17-
// Split the cookie string by semicolons and spaces
18-
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
19-
20-
items.forEach((item) => {
21-
const [name, ...rest] = item.split('=');
22-
23-
if (name && rest) {
24-
// Decode the name and value, and join value parts in case it contains '='
25-
const decodedName = decodeURIComponent(name.trim());
26-
const decodedValue = decodeURIComponent(rest.join('=').trim());
27-
cookies[decodedName] = decodedValue;
28-
}
29-
});
30-
31-
return cookies;
15+
async function getModelList(options: {
16+
apiKeys?: Record<string, string>;
17+
providerSettings?: Record<string, IProviderSetting>;
18+
serverEnv?: Record<string, string>;
19+
}) {
20+
const llmManager = LLMManager.getInstance(import.meta.env);
21+
return llmManager.updateModelList(options);
3222
}
3323

3424
async function llmCallAction({ context, request }: ActionFunctionArgs) {
@@ -58,12 +48,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
5848
}
5949

6050
const cookieHeader = request.headers.get('Cookie');
61-
62-
// Parse the cookie's value (returns an object or null if no cookie exists)
63-
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
64-
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
65-
parseCookies(cookieHeader || '').providers || '{}',
66-
);
51+
const apiKeys = getApiKeysFromCookie(cookieHeader);
52+
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
6753

6854
if (streamOutput) {
6955
try {
@@ -105,8 +91,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
10591
}
10692
} else {
10793
try {
108-
const MODEL_LIST = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
109-
const modelDetails = MODEL_LIST.find((m) => m.name === model);
94+
const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
95+
const modelDetails = models.find((m: ModelInfo) => m.name === model);
11096

11197
if (!modelDetails) {
11298
throw new Error('Model not found');

app/routes/api.models.$provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { loader } from './api.models';
2+
export { loader };

0 commit comments

Comments
 (0)