Skip to content

Commit 2e49905

Browse files
authored
Merge pull request #513 from thecodacus/together-ai-dynamic-model-list
feat(Dynamic Models): Added Together AI Dynamic Models
2 parents a0eb0a0 + 7efad13 commit 2e49905

File tree

5 files changed

+103
-26
lines changed

5 files changed

+103
-26
lines changed

app/lib/.server/llm/stream-text.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2-
// @ts-nocheck – TODO: Provider proper types
3-
41
import { convertToCoreMessages, streamText as _streamText } from 'ai';
52
import { getModel } from '~/lib/.server/llm/model';
63
import { MAX_TOKENS } from './constants';
74
import { getSystemPrompt } from './prompts';
8-
import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_LIST, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
5+
import { DEFAULT_MODEL, DEFAULT_PROVIDER, getModelList, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
96

107
interface ToolResult<Name extends string, Args, Result> {
118
toolCallId: string;
@@ -43,7 +40,7 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
4340
* Extract provider
4441
* const providerMatch = message.content.match(PROVIDER_REGEX);
4542
*/
46-
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER;
43+
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
4744

4845
const cleanedContent = Array.isArray(message.content)
4946
? message.content.map((item) => {
@@ -61,10 +58,15 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
6158
return { model, provider, content: cleanedContent };
6259
}
6360

64-
export function streamText(messages: Messages, env: Env, options?: StreamingOptions, apiKeys?: Record<string, string>) {
61+
export async function streamText(
62+
messages: Messages,
63+
env: Env,
64+
options?: StreamingOptions,
65+
apiKeys?: Record<string, string>,
66+
) {
6567
let currentModel = DEFAULT_MODEL;
66-
let currentProvider = DEFAULT_PROVIDER;
67-
68+
let currentProvider = DEFAULT_PROVIDER.name;
69+
const MODEL_LIST = await getModelList(apiKeys || {});
6870
const processedMessages = messages.map((message) => {
6971
if (message.role === 'user') {
7072
const { model, provider, content } = extractPropertiesFromMessage(message);
@@ -86,10 +88,10 @@ export function streamText(messages: Messages, env: Env, options?: StreamingOpti
8688
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
8789

8890
return _streamText({
89-
...options,
90-
model: getModel(currentProvider, currentModel, env, apiKeys),
91+
model: getModel(currentProvider, currentModel, env, apiKeys) as any,
9192
system: getSystemPrompt(),
9293
maxTokens: dynamicMaxTokens,
93-
messages: convertToCoreMessages(processedMessages),
94+
messages: convertToCoreMessages(processedMessages as any),
95+
...options,
9496
});
9597
}

app/routes/api.chat.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2-
// @ts-nocheck – TODO: Provider proper types
3-
41
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
52
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
63
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
@@ -11,8 +8,8 @@ export async function action(args: ActionFunctionArgs) {
118
return chatAction(args);
129
}
1310

14-
function parseCookies(cookieHeader) {
15-
const cookies = {};
11+
function parseCookies(cookieHeader: string) {
12+
const cookies: any = {};
1613

1714
// Split the cookie string by semicolons and spaces
1815
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
@@ -32,23 +29,21 @@ function parseCookies(cookieHeader) {
3229
}
3330

3431
async function chatAction({ context, request }: ActionFunctionArgs) {
35-
const { messages, model } = await request.json<{
32+
const { messages } = await request.json<{
3633
messages: Messages;
3734
model: string;
3835
}>();
3936

4037
const cookieHeader = request.headers.get('Cookie');
4138

4239
// Parse the cookie's value (returns an object or null if no cookie exists)
43-
const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || '{}');
40+
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
4441

4542
const stream = new SwitchableStream();
4643

4744
try {
4845
const options: StreamingOptions = {
4946
toolChoice: 'none',
50-
apiKeys,
51-
model,
5247
onFinish: async ({ text: content, finishReason }) => {
5348
if (finishReason !== 'length') {
5449
return stream.close();
@@ -65,7 +60,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
6560
messages.push({ role: 'assistant', content });
6661
messages.push({ role: 'user', content: CONTINUE_PROMPT });
6762

68-
const result = await streamText(messages, context.cloudflare.env, options);
63+
const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
6964

7065
return stream.switchSource(result.toAIStream());
7166
},
@@ -81,7 +76,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
8176
contentType: 'text/plain; charset=utf-8',
8277
},
8378
});
84-
} catch (error) {
79+
} catch (error: any) {
8580
console.log(error);
8681

8782
if (error.message?.includes('API key')) {

app/types/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ModelInfo } from '~/utils/types';
33
export type ProviderInfo = {
44
staticModels: ModelInfo[];
55
name: string;
6-
getDynamicModels?: () => Promise<ModelInfo[]>;
6+
getDynamicModels?: (apiKeys?: Record<string, string>) => Promise<ModelInfo[]>;
77
getApiKeyLink?: string;
88
labelForGetApiKey?: string;
99
icon?: string;

app/utils/constants.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Cookies from 'js-cookie';
12
import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types';
23
import type { ProviderInfo } from '~/types/model';
34

@@ -262,6 +263,7 @@ const PROVIDER_LIST: ProviderInfo[] = [
262263
},
263264
{
264265
name: 'Together',
266+
getDynamicModels: getTogetherModels,
265267
staticModels: [
266268
{
267269
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
@@ -293,6 +295,61 @@ const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat(
293295

294296
export let MODEL_LIST: ModelInfo[] = [...staticModels];
295297

298+
export async function getModelList(apiKeys: Record<string, string>) {
299+
MODEL_LIST = [
300+
...(
301+
await Promise.all(
302+
PROVIDER_LIST.filter(
303+
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
304+
).map((p) => p.getDynamicModels(apiKeys)),
305+
)
306+
).flat(),
307+
...staticModels,
308+
];
309+
return MODEL_LIST;
310+
}
311+
312+
async function getTogetherModels(apiKeys?: Record<string, string>): Promise<ModelInfo[]> {
313+
try {
314+
const baseUrl = import.meta.env.TOGETHER_API_BASE_URL || '';
315+
const provider = 'Together';
316+
317+
if (!baseUrl) {
318+
return [];
319+
}
320+
321+
let apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
322+
323+
if (apiKeys && apiKeys[provider]) {
324+
apiKey = apiKeys[provider];
325+
}
326+
327+
if (!apiKey) {
328+
return [];
329+
}
330+
331+
const response = await fetch(`${baseUrl}/models`, {
332+
headers: {
333+
Authorization: `Bearer ${apiKey}`,
334+
},
335+
});
336+
const res = (await response.json()) as any;
337+
const data: any[] = (res || []).filter((model: any) => model.type == 'chat');
338+
339+
return data.map((m: any) => ({
340+
name: m.id,
341+
label: `${m.display_name} - in:$${m.pricing.input.toFixed(
342+
2,
343+
)} out:$${m.pricing.output.toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
344+
provider,
345+
maxTokenAllowed: 8000,
346+
}));
347+
} catch (e) {
348+
console.error('Error getting OpenAILike models:', e);
349+
return [];
350+
}
351+
}
352+
296353
const getOllamaBaseUrl = () => {
297354
const defaultBaseUrl = import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
298355

@@ -340,7 +397,14 @@ async function getOpenAILikeModels(): Promise<ModelInfo[]> {
340397
return [];
341398
}
342399

343-
const apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
400+
let apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
401+
402+
const apikeys = JSON.parse(Cookies.get('apiKeys') || '{}');
403+
404+
if (apikeys && apikeys.OpenAILike) {
405+
apiKey = apikeys.OpenAILike;
406+
}
407+
344408
const response = await fetch(`${baseUrl}/models`, {
345409
headers: {
346410
Authorization: `Bearer ${apiKey}`,
@@ -414,16 +478,32 @@ async function getLMStudioModels(): Promise<ModelInfo[]> {
414478
}
415479

416480
async function initializeModelList(): Promise<ModelInfo[]> {
481+
let apiKeys: Record<string, string> = {};
482+
483+
try {
484+
const storedApiKeys = Cookies.get('apiKeys');
485+
486+
if (storedApiKeys) {
487+
const parsedKeys = JSON.parse(storedApiKeys);
488+
489+
if (typeof parsedKeys === 'object' && parsedKeys !== null) {
490+
apiKeys = parsedKeys;
491+
}
492+
}
493+
} catch (error: any) {
494+
console.warn(`Failed to fetch apikeys from cookies:${error?.message}`);
495+
}
417496
MODEL_LIST = [
418497
...(
419498
await Promise.all(
420499
PROVIDER_LIST.filter(
421500
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
422-
).map((p) => p.getDynamicModels()),
501+
).map((p) => p.getDynamicModels(apiKeys)),
423502
)
424503
).flat(),
425504
...staticModels,
426505
];
506+
427507
return MODEL_LIST;
428508
}
429509

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig((config) => {
2727
chrome129IssuePlugin(),
2828
config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
2929
],
30-
envPrefix:["VITE_","OPENAI_LIKE_API_","OLLAMA_API_BASE_URL","LMSTUDIO_API_BASE_URL"],
30+
envPrefix: ["VITE_", "OPENAI_LIKE_API_", "OLLAMA_API_BASE_URL", "LMSTUDIO_API_BASE_URL","TOGETHER_API_BASE_URL"],
3131
css: {
3232
preprocessorOptions: {
3333
scss: {

0 commit comments

Comments
 (0)