diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 6af2115..a37fd98 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -8,48 +8,29 @@ export const PROVIDER_INFO = { }; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -const anthropicChatModels: Record[] = [ - { - displayName: 'Claude 4.1 Opus', - key: 'claude-opus-4-1-20250805', - }, - { - displayName: 'Claude 4 Opus', - key: 'claude-opus-4-20250514', - }, - { - displayName: 'Claude 4 Sonnet', - key: 'claude-sonnet-4-20250514', - }, - { - displayName: 'Claude 3.7 Sonnet', - key: 'claude-3-7-sonnet-20250219', - }, - { - displayName: 'Claude 3.5 Haiku', - key: 'claude-3-5-haiku-20241022', - }, - { - displayName: 'Claude 3.5 Sonnet v2', - key: 'claude-3-5-sonnet-20241022', - }, - { - displayName: 'Claude 3.5 Sonnet', - key: 'claude-3-5-sonnet-20240620', - }, - { - displayName: 'Claude 3 Opus', - key: 'claude-3-opus-20240229', - }, - { - displayName: 'Claude 3 Sonnet', - key: 'claude-3-sonnet-20240229', - }, - { - displayName: 'Claude 3 Haiku', - key: 'claude-3-haiku-20240307', - }, -]; +const ANTHROPIC_MODELS_ENDPOINT = 'https://api.anthropic.com/v1/models'; + +async function fetchAnthropicModels(apiKey: string): Promise { + const resp = await fetch(ANTHROPIC_MODELS_ENDPOINT, { + method: 'GET', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json', + }, + }); + + if (!resp.ok) { + throw new Error(`Anthropic models endpoint returned ${resp.status}`); + } + + const data = await resp.json(); + if (!data || !Array.isArray(data.data)) { + throw new Error('Unexpected Anthropic models response format'); + } + + return data.data; +} export const loadAnthropicChatModels = async () => { const anthropicApiKey = getAnthropicApiKey(); @@ -57,14 +38,25 @@ export const loadAnthropicChatModels = async () => { if (!anthropicApiKey) return {}; try { + const models = await fetchAnthropicModels(anthropicApiKey); + const anthropicChatModels = models + .map((model: any) => { + const id = model && model.id ? String(model.id) : ''; + const display = + model && model.display_name ? String(model.display_name) : id; + return { id, display }; + }) + .filter((model: any) => model.id) + .sort((a: any, b: any) => a.display.localeCompare(b.display)); + const chatModels: Record = {}; - anthropicChatModels.forEach((model) => { - chatModels[model.key] = { - displayName: model.displayName, + anthropicChatModels.forEach((model: any) => { + chatModels[model.id] = { + displayName: model.display, model: new ChatAnthropic({ apiKey: anthropicApiKey, - modelName: model.key, + modelName: model.id, temperature: 0.7, }) as unknown as BaseChatModel, }; diff --git a/src/lib/providers/gemini.ts b/src/lib/providers/gemini.ts index 418e0a4..98b74a3 100644 --- a/src/lib/providers/gemini.ts +++ b/src/lib/providers/gemini.ts @@ -12,55 +12,33 @@ export const PROVIDER_INFO = { import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; -const geminiChatModels: Record[] = [ - { - displayName: 'Gemini 2.5 Flash', - key: 'gemini-2.5-flash', - }, - { - displayName: 'Gemini 2.5 Flash-Lite', - key: 'gemini-2.5-flash-lite', - }, - { - displayName: 'Gemini 2.5 Pro', - key: 'gemini-2.5-pro', - }, - { - displayName: 'Gemini 2.0 Flash', - key: 'gemini-2.0-flash', - }, - { - displayName: 'Gemini 2.0 Flash-Lite', - key: 'gemini-2.0-flash-lite', - }, - { - displayName: 'Gemini 2.0 Flash Thinking Experimental', - key: 'gemini-2.0-flash-thinking-exp-01-21', - }, - { - displayName: 'Gemini 1.5 Flash', - key: 'gemini-1.5-flash', - }, - { - displayName: 'Gemini 1.5 Flash-8B', - key: 'gemini-1.5-flash-8b', - }, - { - displayName: 'Gemini 1.5 Pro', - key: 'gemini-1.5-pro', - }, -]; +const GEMINI_MODELS_ENDPOINT = + 'https://generativelanguage.googleapis.com/v1beta/models'; -const geminiEmbeddingModels: Record[] = [ - { - displayName: 'Text Embedding 004', - key: 'models/text-embedding-004', - }, - { - displayName: 'Embedding 001', - key: 'models/embedding-001', - }, -]; +async function fetchGeminiModels(apiKey: string): Promise { + const url = `${GEMINI_MODELS_ENDPOINT}?key=${encodeURIComponent( + apiKey, + )}&pageSize=1000`; + + const resp = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!resp.ok) { + throw new Error(`Gemini models endpoint returned ${resp.status}`); + } + + const data = await resp.json(); + + if (!data || !Array.isArray(data.models)) { + throw new Error('Unexpected Gemini models response format'); + } + + return data.models; +} export const loadGeminiChatModels = async () => { const geminiApiKey = getGeminiApiKey(); @@ -68,9 +46,32 @@ export const loadGeminiChatModels = async () => { if (!geminiApiKey) return {}; try { + const models = await fetchGeminiModels(geminiApiKey); + const geminiChatModels = models + .map((model: any) => { + const rawName = model && model.name ? String(model.name) : ''; + const stripped = rawName.replace(/^models\//i, ''); + return { + rawName, + key: stripped, + displayName: + model && model.displayName ? String(model.displayName) : stripped, + }; + }) + .filter((model: any) => { + const key = model.key.toLowerCase(); + const display = (model.displayName || '').toLowerCase(); + const excluded = ['audio', 'embedding', 'image', 'tts']; + return ( + key.startsWith('gemini') && + !excluded.some((s) => key.includes(s) || display.includes(s)) + ); + }) + .sort((a: any, b: any) => a.key.localeCompare(b.key)); + const chatModels: Record = {}; - geminiChatModels.forEach((model) => { + geminiChatModels.forEach((model: any) => { chatModels[model.key] = { displayName: model.displayName, model: new ChatGoogleGenerativeAI({ @@ -83,7 +84,7 @@ export const loadGeminiChatModels = async () => { return chatModels; } catch (err) { - console.error(`Error loading Gemini models: ${err}`); + console.error(`Error loading Gemini chat models: ${err}`); return {}; } }; @@ -94,9 +95,28 @@ export const loadGeminiEmbeddingModels = async () => { if (!geminiApiKey) return {}; try { + const models = await fetchGeminiModels(geminiApiKey); + const geminiEmbeddingModels = models + .map((model: any) => { + const rawName = model && model.name ? String(model.name) : ''; + const display = + model && model.displayName ? String(model.displayName) : rawName; + return { + rawName, + key: rawName, + displayName: display, + }; + }) + .filter( + (model: any) => + model.key.toLowerCase().includes('embedding') || + model.displayName.toLowerCase().includes('embedding'), + ) + .sort((a: any, b: any) => a.key.localeCompare(b.key)); + const embeddingModels: Record = {}; - geminiEmbeddingModels.forEach((model) => { + geminiEmbeddingModels.forEach((model: any) => { embeddingModels[model.key] = { displayName: model.displayName, model: new GoogleGenerativeAIEmbeddings({ @@ -108,7 +128,7 @@ export const loadGeminiEmbeddingModels = async () => { return embeddingModels; } catch (err) { - console.error(`Error loading Gemini embeddings models: ${err}`); + console.error(`Error loading Gemini embedding models: ${err}`); return {}; } }; diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts index 7e26763..fc46b6c 100644 --- a/src/lib/providers/openai.ts +++ b/src/lib/providers/openai.ts @@ -9,63 +9,31 @@ export const PROVIDER_INFO = { import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; -const openaiChatModels: Record[] = [ - { - displayName: 'GPT-3.5 Turbo', - key: 'gpt-3.5-turbo', - }, - { - displayName: 'GPT-4', - key: 'gpt-4', - }, - { - displayName: 'GPT-4 turbo', - key: 'gpt-4-turbo', - }, - { - displayName: 'GPT-4 omni', - key: 'gpt-4o', - }, - { - displayName: 'GPT-4 omni mini', - key: 'gpt-4o-mini', - }, - { - displayName: 'GPT 4.1 nano', - key: 'gpt-4.1-nano', - }, - { - displayName: 'GPT 4.1 mini', - key: 'gpt-4.1-mini', - }, - { - displayName: 'GPT 4.1', - key: 'gpt-4.1', - }, - { - displayName: 'GPT 5 nano', - key: 'gpt-5-nano', - }, - { - displayName: 'GPT 5 mini', - key: 'gpt-5-mini', - }, - { - displayName: 'GPT 5', - key: 'gpt-5', - }, -]; +const OPENAI_MODELS_ENDPOINT = 'https://api.openai.com/v1/models'; -const openaiEmbeddingModels: Record[] = [ - { - displayName: 'Text Embedding 3 Small', - key: 'text-embedding-3-small', - }, - { - displayName: 'Text Embedding 3 Large', - key: 'text-embedding-3-large', - }, -]; +async function fetchOpenAIModels(apiKey: string): Promise { + const resp = await fetch(OPENAI_MODELS_ENDPOINT, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + + if (!resp.ok) { + throw new Error(`OpenAI models endpoint returned ${resp.status}`); + } + + const data = await resp.json(); + + if (!data || !Array.isArray(data.data)) { + throw new Error('Unexpected OpenAI models response format'); + } + + return data.data + .map((model: any) => (model && model.id ? String(model.id) : undefined)) + .filter(Boolean) as string[]; +} export const loadOpenAIChatModels = async () => { const openaiApiKey = getOpenaiApiKey(); @@ -73,22 +41,41 @@ export const loadOpenAIChatModels = async () => { if (!openaiApiKey) return {}; try { + const modelIds = (await fetchOpenAIModels(openaiApiKey)).sort((a, b) => + a.localeCompare(b), + ); + const chatModels: Record = {}; - openaiChatModels.forEach((model) => { - chatModels[model.key] = { - displayName: model.displayName, + modelIds.forEach((model) => { + const lid = model.toLowerCase(); + const excludedSubstrings = [ + 'audio', + 'embedding', + 'image', + 'omni-moderation', + 'transcribe', + 'tts', + ]; + const isChat = + (lid.startsWith('gpt') || lid.startsWith('o')) && + !excludedSubstrings.some((s) => lid.includes(s)); + + if (!isChat) return; + + chatModels[model] = { + displayName: model, model: new ChatOpenAI({ apiKey: openaiApiKey, - modelName: model.key, - temperature: model.key.includes('gpt-5') ? 1 : 0.7, + modelName: model, + temperature: model.includes('gpt-5') ? 1 : 0.7, }) as unknown as BaseChatModel, }; }); return chatModels; } catch (err) { - console.error(`Error loading OpenAI models: ${err}`); + console.error(`Error loading OpenAI chat models: ${err}`); return {}; } }; @@ -99,21 +86,31 @@ export const loadOpenAIEmbeddingModels = async () => { if (!openaiApiKey) return {}; try { + const modelIds = (await fetchOpenAIModels(openaiApiKey)).sort((a, b) => + a.localeCompare(b), + ); + const embeddingModels: Record = {}; - openaiEmbeddingModels.forEach((model) => { - embeddingModels[model.key] = { - displayName: model.displayName, + modelIds.forEach((model) => { + const lid = model.toLowerCase(); + + const isEmbedding = lid.includes('embedding'); + + if (!isEmbedding) return; + + embeddingModels[model] = { + displayName: model, model: new OpenAIEmbeddings({ apiKey: openaiApiKey, - modelName: model.key, + modelName: model, }) as unknown as Embeddings, }; }); return embeddingModels; } catch (err) { - console.error(`Error loading OpenAI embeddings models: ${err}`); + console.error(`Error loading OpenAI embedding models: ${err}`); return {}; } };