| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createOpenAI } from '@ai-sdk/openai'; |
| import { createAnthropic } from '@ai-sdk/anthropic'; |
| import { createGoogleGenerativeAI } from '@ai-sdk/google'; |
| import type { LanguageModel } from 'ai'; |
| import type { |
| ProviderId, |
| ProviderConfig, |
| ModelInfo, |
| ModelConfig, |
| ThinkingConfig, |
| } from '@/lib/types/provider'; |
| import { applyModelMetadata, getCatalogThinkingCapability } from './model-metadata'; |
| import { getThinkingMode, pickThinkingBudget } from './thinking-config'; |
| import { createLogger } from '@/lib/logger'; |
| |
| |
| |
| |
|
|
| const log = createLogger('AIProviders'); |
|
|
| |
| export type { ProviderId, ProviderConfig, ModelInfo, ModelConfig }; |
|
|
| |
| export const MONO_LOGO_PROVIDERS: ReadonlySet<string> = new Set(['openai', 'openrouter', 'ollama']); |
|
|
| |
| |
| |
| export const PROVIDERS: Record<ProviderId, ProviderConfig> = { |
| openai: { |
| id: 'openai', |
| name: 'OpenAI', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.openai.com/v1', |
| requiresApiKey: true, |
| icon: '/logos/openai.svg', |
| models: [ |
| { |
| id: 'gpt-5.5', |
| name: 'GPT-5.5', |
| contextWindow: 1050000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'gpt-5.4-pro', |
| name: 'GPT-5.4 Pro', |
| contextWindow: 1050000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'gpt-5.4', |
| name: 'GPT-5.4', |
| contextWindow: 1050000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'gpt-5.4-mini', |
| name: 'GPT-5.4 Mini', |
| contextWindow: 400000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'gpt-5.4-nano', |
| name: 'GPT-5.4 Nano', |
| contextWindow: 400000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| anthropic: { |
| id: 'anthropic', |
| name: 'Claude', |
| type: 'anthropic', |
| requiresApiKey: true, |
| defaultBaseUrl: 'https://api.anthropic.com/v1', |
| icon: '/logos/claude.svg', |
| models: [ |
| { |
| id: 'claude-opus-4-7', |
| name: 'Claude Opus 4.7', |
| contextWindow: 1000000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'claude-opus-4-6', |
| name: 'Claude Opus 4.6', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'claude-sonnet-4-6', |
| name: 'Claude Sonnet 4.6', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'claude-sonnet-4-5', |
| name: 'Claude Sonnet 4.5', |
| contextWindow: 200000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'claude-haiku-4-5', |
| name: 'Claude Haiku 4.5', |
| contextWindow: 200000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| google: { |
| id: 'google', |
| name: 'Gemini', |
| type: 'google', |
| requiresApiKey: true, |
| defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta', |
| icon: '/logos/gemini.svg', |
| models: [ |
| { |
| id: 'gemini-3.1-pro-preview', |
| name: 'Gemini 3.1 Pro Preview', |
| contextWindow: 1048576, |
| outputWindow: 65536, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'gemini-3-flash-preview', |
| name: 'Gemini 3 Flash Preview', |
| contextWindow: 1048576, |
| outputWindow: 65536, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'gemini-2.5-flash', |
| name: 'Gemini 2.5 Flash', |
| contextWindow: 1048576, |
| outputWindow: 65536, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'gemini-2.5-flash-lite', |
| name: 'Gemini 2.5 Flash Lite', |
| contextWindow: 1048576, |
| outputWindow: 65536, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'gemini-2.5-pro', |
| name: 'Gemini 2.5 Pro', |
| contextWindow: 1048576, |
| outputWindow: 65536, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| glm: { |
| id: 'glm', |
| name: 'GLM', |
| type: 'openai', |
| defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', |
| alternateBaseUrls: [ |
| { label: 'settings.baseUrlRegion.china', url: 'https://open.bigmodel.cn/api/paas/v4' }, |
| { label: 'settings.baseUrlRegion.international', url: 'https://api.z.ai/api/paas/v4' }, |
| ], |
| requiresApiKey: true, |
| icon: '/logos/glm.svg', |
| models: [ |
| |
| { |
| id: 'glm-5.1', |
| name: 'GLM-5.1', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'glm-5v-turbo', |
| name: 'GLM-5V-Turbo', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| |
| { |
| id: 'glm-5', |
| name: 'GLM-5', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| |
| { |
| id: 'glm-4.7', |
| name: 'GLM-4.7', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'glm-4.7-flashx', |
| name: 'GLM-4.7-FlashX', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'glm-4.7-flash', |
| name: 'GLM-4.7-Flash', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| |
| { |
| id: 'glm-4.6', |
| name: 'GLM-4.6', |
| contextWindow: 200000, |
| outputWindow: 128000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'glm-4.6v', |
| name: 'GLM-4.6V', |
| contextWindow: 128000, |
| outputWindow: 32000, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'glm-4.6v-flash', |
| name: 'GLM-4.6V-Flash', |
| contextWindow: 128000, |
| outputWindow: 32000, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| ], |
| }, |
|
|
| qwen: { |
| id: 'qwen', |
| name: 'Qwen', |
| type: 'openai', |
| defaultBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', |
| requiresApiKey: true, |
| icon: '/logos/qwen.svg', |
| models: [ |
| { |
| id: 'qwen3.6-max-preview', |
| name: 'Qwen3.6 Max Preview', |
| contextWindow: 256000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.6-plus', |
| name: 'Qwen3.6 Plus', |
| contextWindow: 1000000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.6-plus-2026-04-02', |
| name: 'Qwen3.6 Plus (2026-04-02)', |
| contextWindow: 1000000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.6-flash', |
| name: 'Qwen3.6 Flash', |
| contextWindow: 1000000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.6-flash-2026-04-16', |
| name: 'Qwen3.6 Flash (2026-04-16)', |
| contextWindow: 1000000, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.6-35b-a3b', |
| name: 'Qwen3.6 35B A3B', |
| contextWindow: 262144, |
| outputWindow: 64000, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: false, |
| }, |
| }, |
| }, |
| { |
| id: 'qwen3.5-flash', |
| name: 'Qwen3.5 Flash', |
| contextWindow: 1000000, |
| outputWindow: 65536, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'qwen3.5-plus', |
| name: 'Qwen3.5 Plus', |
| contextWindow: 1000000, |
| outputWindow: 65536, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'qwen3-max', |
| name: 'Qwen3 Max', |
| contextWindow: 262144, |
| outputWindow: 65536, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'qwen3-vl-plus', |
| name: 'Qwen3 VL Plus', |
| contextWindow: 262144, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| ], |
| }, |
|
|
| deepseek: { |
| id: 'deepseek', |
| name: 'DeepSeek', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.deepseek.com/v1', |
| requiresApiKey: true, |
| icon: '/logos/deepseek.svg', |
| models: [ |
| { |
| id: 'deepseek-v4-pro', |
| name: 'DeepSeek V4 Pro', |
| contextWindow: 1048576, |
| outputWindow: 393216, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'deepseek-v4-flash', |
| name: 'DeepSeek V4 Flash', |
| contextWindow: 1048576, |
| outputWindow: 393216, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: true, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| kimi: { |
| id: 'kimi', |
| name: 'Kimi', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.moonshot.cn/v1', |
| alternateBaseUrls: [ |
| { label: 'settings.baseUrlRegion.china', url: 'https://api.moonshot.cn/v1' }, |
| { label: 'settings.baseUrlRegion.international', url: 'https://api.moonshot.ai/v1' }, |
| ], |
| requiresApiKey: true, |
| icon: '/logos/kimi.png', |
| models: [ |
| { |
| id: 'kimi-k2.6', |
| name: 'Kimi K2.6', |
| contextWindow: 256000, |
| outputWindow: 8192, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| |
| { |
| id: 'kimi-k2.5', |
| name: 'Kimi K2.5', |
| contextWindow: 256000, |
| outputWindow: 8192, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'kimi-k2-thinking', |
| name: 'Kimi K2 Thinking', |
| contextWindow: 256000, |
| outputWindow: 8192, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| minimax: { |
| id: 'minimax', |
| name: 'MiniMax', |
| type: 'anthropic', |
| defaultBaseUrl: 'https://api.minimaxi.com/anthropic/v1', |
| alternateBaseUrls: [ |
| { label: 'settings.baseUrlRegion.china', url: 'https://api.minimaxi.com/anthropic/v1' }, |
| { label: 'settings.baseUrlRegion.international', url: 'https://api.minimax.io/anthropic/v1' }, |
| ], |
| requiresApiKey: true, |
| icon: '/logos/minimax.svg', |
| models: [ |
| { |
| id: 'MiniMax-M2.7', |
| name: 'MiniMax M2.7', |
| contextWindow: 204800, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| ], |
| }, |
|
|
| siliconflow: { |
| id: 'siliconflow', |
| name: '硅基流动', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.siliconflow.cn/v1', |
| requiresApiKey: true, |
| icon: '/logos/siliconflow.svg', |
| models: [ |
| |
| { |
| id: 'deepseek-ai/DeepSeek-V3.2', |
| name: 'DeepSeek-V3.2', |
| contextWindow: 128000, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'deepseek-ai/DeepSeek-R1', |
| name: 'DeepSeek-R1', |
| contextWindow: 128000, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', |
| name: 'DeepSeek-R1-Distill-Qwen-7B', |
| contextWindow: 128000, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| |
| { |
| id: 'Qwen/Qwen3-VL-32B-Instruct', |
| name: 'Qwen3-VL-32B-Instruct', |
| contextWindow: 256000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| |
| { |
| id: 'Pro/moonshotai/Kimi-K2.5', |
| name: 'Kimi-K2.5', |
| contextWindow: 256000, |
| outputWindow: 96000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| |
| { |
| id: 'THUDM/GLM-4.1V-9B-Thinking', |
| name: 'GLM-4.1V-9B-Thinking', |
| contextWindow: 64000, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'THUDM/GLM-Z1-Rumination-32B-0414', |
| name: 'GLM-Z1-Rumination-32B', |
| contextWindow: 32000, |
| outputWindow: 16384, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| ], |
| }, |
|
|
| doubao: { |
| id: 'doubao', |
| name: '豆包', |
| type: 'openai', |
| defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', |
| requiresApiKey: true, |
| icon: '/logos/doubao.svg', |
| models: [ |
| { |
| id: 'doubao-seed-2-0-pro-260215', |
| name: 'Doubao Seed 2.0 Pro', |
| contextWindow: 128000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'doubao-seed-2-0-lite-260215', |
| name: 'Doubao Seed 2.0 Lite', |
| contextWindow: 128000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'doubao-seed-2-0-mini-260215', |
| name: 'Doubao Seed 2.0 Mini', |
| contextWindow: 128000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'doubao-seed-1-8-251228', |
| name: 'Doubao Seed 1.8', |
| contextWindow: 128000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| ], |
| }, |
|
|
| openrouter: { |
| id: 'openrouter', |
| name: 'OpenRouter', |
| type: 'openai', |
| defaultBaseUrl: 'https://openrouter.ai/api/v1', |
| requiresApiKey: true, |
| icon: '/logos/openrouter.svg', |
| models: [ |
| { |
| id: 'deepseek/deepseek-v4-pro', |
| name: 'DeepSeek V4 Pro', |
| contextWindow: 1048576, |
| outputWindow: 131072, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'deepseek/deepseek-v4-flash', |
| name: 'DeepSeek V4 Flash', |
| contextWindow: 1048576, |
| outputWindow: 131072, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| ], |
| }, |
|
|
| grok: { |
| id: 'grok', |
| name: 'Grok', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.x.ai/v1', |
| requiresApiKey: true, |
| icon: '/logos/grok.svg', |
| models: [ |
| { |
| id: 'grok-4.20-reasoning', |
| name: 'Grok 4.20 Reasoning', |
| contextWindow: 2000000, |
| outputWindow: 131072, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'grok-4.20', |
| name: 'Grok 4.20', |
| contextWindow: 2000000, |
| outputWindow: 131072, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'grok-4.20-multi-agent', |
| name: 'Grok 4.20 Multi-Agent', |
| contextWindow: 2000000, |
| outputWindow: 131072, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'grok-4-1-fast-reasoning', |
| name: 'Grok 4.1 Fast Reasoning', |
| contextWindow: 2000000, |
| outputWindow: 131072, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: false, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'grok-4-1-fast-non-reasoning', |
| name: 'Grok 4.1 Fast', |
| contextWindow: 2000000, |
| outputWindow: 131072, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'grok-code-fast-1', |
| name: 'Grok Code Fast', |
| contextWindow: 256000, |
| outputWindow: 32768, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| ], |
| }, |
|
|
| 'tencent-hunyuan': { |
| id: 'tencent-hunyuan', |
| name: 'Tencent Hunyuan', |
| type: 'openai', |
| defaultBaseUrl: 'https://tokenhub.tencentmaas.com/v1', |
| alternateBaseUrls: [ |
| { label: 'settings.baseUrlRegion.china', url: 'https://tokenhub.tencentmaas.com/v1' }, |
| { |
| label: 'settings.baseUrlRegion.international', |
| url: 'https://tokenhub-intl.tencentmaas.com/v1', |
| }, |
| ], |
| requiresApiKey: true, |
| icon: '/logos/hunyuan.svg', |
| models: [ |
| { |
| id: 'hy3-preview', |
| name: 'Tencent Hy3 Preview', |
| contextWindow: 256000, |
| outputWindow: 64000, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| ], |
| }, |
|
|
| xiaomi: { |
| id: 'xiaomi', |
| name: 'Xiaomi MiMo', |
| type: 'openai', |
| defaultBaseUrl: 'https://api.xiaomimimo.com/v1', |
| requiresApiKey: true, |
| icon: '/logos/xiaomi.svg', |
| models: [ |
| { |
| id: 'mimo-v2.5-pro', |
| name: 'MiMo V2.5 Pro', |
| contextWindow: 1048576, |
| outputWindow: 131072, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: false, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| { |
| id: 'mimo-v2.5', |
| name: 'MiMo V2.5', |
| contextWindow: 1048576, |
| outputWindow: 131072, |
| capabilities: { |
| streaming: true, |
| tools: true, |
| vision: true, |
| thinking: { |
| toggleable: true, |
| budgetAdjustable: false, |
| defaultEnabled: true, |
| }, |
| }, |
| }, |
| ], |
| }, |
|
|
| ollama: { |
| id: 'ollama', |
| name: 'Ollama', |
| type: 'openai', |
| defaultBaseUrl: 'http://localhost:11434/v1', |
| requiresApiKey: false, |
| icon: '/logos/ollama.svg', |
| models: [ |
| { |
| id: 'llama3.3', |
| name: 'Llama 3.3 70B', |
| contextWindow: 131072, |
| outputWindow: 4096, |
| capabilities: { streaming: true, tools: true, vision: false }, |
| }, |
| { |
| id: 'gemma3', |
| name: 'Gemma 3 12B', |
| contextWindow: 131072, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: true, vision: true }, |
| }, |
| { |
| id: 'deepseek-r1', |
| name: 'DeepSeek R1', |
| contextWindow: 131072, |
| outputWindow: 8192, |
| capabilities: { streaming: true, tools: false, vision: false }, |
| }, |
| ], |
| }, |
| }; |
|
|
| applyModelMetadata(PROVIDERS); |
|
|
| |
| |
| |
| function getProviderConfig(providerId: ProviderId): ProviderConfig | null { |
| |
| if (PROVIDERS[providerId]) { |
| return PROVIDERS[providerId]; |
| } |
|
|
| |
| if (typeof window !== 'undefined') { |
| try { |
| const storedConfig = localStorage.getItem('providersConfig'); |
| if (storedConfig) { |
| const config = JSON.parse(storedConfig); |
| const providerSettings = config[providerId]; |
| if (providerSettings) { |
| return { |
| id: providerId, |
| name: providerSettings.name, |
| type: providerSettings.type, |
| defaultBaseUrl: providerSettings.defaultBaseUrl, |
| icon: providerSettings.icon, |
| requiresApiKey: providerSettings.requiresApiKey, |
| models: providerSettings.models, |
| }; |
| } |
| } |
| } catch (e) { |
| log.error('Failed to load provider config:', e); |
| } |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| export interface ModelWithInfo { |
| model: LanguageModel; |
| modelInfo: ModelInfo | null; |
| } |
|
|
| function getCompatThinkingBodyParams( |
| providerId: ProviderId, |
| modelId: string, |
| config: ThinkingConfig, |
| ): Record<string, unknown> | undefined { |
| const capability = getCatalogThinkingCapability(providerId, modelId); |
| if (!capability || capability.control === 'none') return undefined; |
|
|
| const mode = getThinkingMode(config); |
| const budget = pickThinkingBudget(capability, config); |
|
|
| switch (capability.requestAdapter) { |
| case 'kimi': |
| case 'glm': |
| case 'xiaomi': |
| if (mode === 'disabled') return { thinking: { type: 'disabled' } }; |
| if (mode === 'enabled') return { thinking: { type: 'enabled' } }; |
| return undefined; |
|
|
| case 'deepseek': { |
| if (mode === 'disabled' || config.effort === 'none') { |
| return { thinking: { type: 'disabled' } }; |
| } |
|
|
| const effort = config.effort === 'max' || config.effort === 'xhigh' ? 'max' : 'high'; |
| return { |
| thinking: { type: 'enabled' }, |
| reasoning_effort: effort, |
| }; |
| } |
|
|
| case 'qwen': { |
| if (mode === 'disabled') return { enable_thinking: false }; |
| const body: Record<string, unknown> = {}; |
| if (mode === 'enabled') body.enable_thinking = true; |
| if (budget !== undefined) body.thinking_budget = budget; |
| return Object.keys(body).length > 0 ? body : undefined; |
| } |
|
|
| case 'siliconflow': { |
| const body: Record<string, unknown> = {}; |
| if (capability.control === 'toggle-budget') { |
| if (mode === 'disabled') body.enable_thinking = false; |
| if (mode === 'enabled') body.enable_thinking = true; |
| } |
| if (budget !== undefined) body.thinking_budget = budget; |
| return Object.keys(body).length > 0 ? body : undefined; |
| } |
|
|
| case 'doubao': { |
| if (capability.control === 'effort') { |
| const effort = |
| mode === 'disabled' |
| ? 'minimal' |
| : config.effort && capability.effortValues?.includes(config.effort) |
| ? config.effort |
| : mode === 'enabled' |
| ? capability.defaultEffort |
| : undefined; |
| return effort ? { reasoning_effort: effort } : undefined; |
| } |
| if (mode === 'auto') return { thinking: { type: 'auto' } }; |
| if (mode === 'disabled') return { thinking: { type: 'disabled' } }; |
| if (mode === 'enabled') return { thinking: { type: 'enabled' } }; |
| return undefined; |
| } |
|
|
| case 'openrouter': { |
| const reasoning: Record<string, unknown> = {}; |
| if (mode === 'disabled') reasoning.enabled = false; |
| if (mode === 'enabled') reasoning.enabled = true; |
| if (config.effort) reasoning.effort = config.effort; |
| if (budget !== undefined) reasoning.max_tokens = budget; |
| if (typeof config.excludeReasoningOutput === 'boolean') { |
| reasoning.exclude = config.excludeReasoningOutput; |
| } |
| return Object.keys(reasoning).length > 0 ? { reasoning } : undefined; |
| } |
|
|
| case 'hunyuan': { |
| let reasoningEffort: 'no_think' | 'low' | 'high' | undefined; |
| if (mode === 'disabled' || config.effort === 'none') { |
| reasoningEffort = 'no_think'; |
| } else if (config.effort === 'high' || config.effort === 'max' || config.effort === 'xhigh') { |
| reasoningEffort = 'high'; |
| } else if ( |
| config.effort === 'low' || |
| config.effort === 'medium' || |
| config.effort === 'minimal' |
| ) { |
| reasoningEffort = 'low'; |
| } else if (mode === 'enabled') { |
| reasoningEffort = capability.defaultEffort === 'high' ? 'high' : 'low'; |
| } |
| return reasoningEffort |
| ? { chat_template_kwargs: { reasoning_effort: reasoningEffort } } |
| : undefined; |
| } |
|
|
| default: |
| return undefined; |
| } |
| } |
|
|
| function normalizeMiniMaxAnthropicBaseUrl( |
| providerId: ProviderId, |
| baseUrl?: string, |
| ): string | undefined { |
| if (providerId !== 'minimax' || !baseUrl) { |
| return baseUrl; |
| } |
|
|
| const trimmed = baseUrl.replace(/\/$/, ''); |
| if (trimmed.endsWith('/anthropic/v1')) { |
| return trimmed; |
| } |
| if (trimmed.endsWith('/anthropic')) { |
| return `${trimmed}/v1`; |
| } |
| return `${trimmed}/anthropic/v1`; |
| } |
|
|
| function shouldUseOpenAIResponsesApi(providerId: ProviderId, modelId: string): boolean { |
| if (providerId !== 'openai') return false; |
|
|
| return ( |
| /^gpt-5\.\d+-pro(?:-|$)/.test(modelId) || |
| /^gpt-5\.5(?:-|$)/.test(modelId) || |
| /^gpt-5\.[3-9]-codex(?:-|$)/.test(modelId) |
| ); |
| } |
|
|
| |
| export function isProviderKeyRequired(providerId: string): boolean { |
| return getProviderConfig(providerId as ProviderId)?.requiresApiKey ?? true; |
| } |
|
|
| |
| |
| |
| |
| export function getModel(config: ModelConfig): ModelWithInfo { |
| |
| let providerType = config.providerType; |
| const provider = getProviderConfig(config.providerId); |
| const requiresApiKey = provider?.requiresApiKey ?? true; |
|
|
| if (!providerType) { |
| if (provider) { |
| providerType = provider.type; |
| } else { |
| throw new Error(`Unknown provider: ${config.providerId}. Please provide providerType.`); |
| } |
| } |
|
|
| |
| if (requiresApiKey && !config.apiKey) { |
| throw new Error(`API key required for provider: ${config.providerId}`); |
| } |
|
|
| |
| const effectiveApiKey = config.apiKey || ''; |
|
|
| |
| const effectiveBaseUrl = normalizeMiniMaxAnthropicBaseUrl( |
| config.providerId, |
| config.baseUrl || provider?.defaultBaseUrl || undefined, |
| ); |
|
|
| let model: LanguageModel; |
|
|
| switch (providerType) { |
| case 'openai': { |
| const openaiOptions: Parameters<typeof createOpenAI>[0] = { |
| apiKey: effectiveApiKey, |
| baseURL: effectiveBaseUrl, |
| }; |
|
|
| |
| |
| |
| |
| if (config.providerId !== 'openai') { |
| const providerId = config.providerId; |
| openaiOptions.fetch = async (url: RequestInfo | URL, init?: RequestInit) => { |
| |
| const thinkingCtx = (globalThis as Record<string, unknown>).__thinkingContext as |
| | { getStore?: () => unknown } |
| | undefined; |
| const thinking = thinkingCtx?.getStore?.() as ThinkingConfig | undefined; |
| if (thinking && init?.body && typeof init.body === 'string') { |
| const extra = getCompatThinkingBodyParams(providerId, config.modelId, thinking); |
| if (extra) { |
| try { |
| const body = JSON.parse(init.body); |
| Object.assign(body, extra); |
| init = { ...init, body: JSON.stringify(body) }; |
| } catch { |
| |
| } |
| } |
| } |
| return globalThis.fetch(url, init); |
| }; |
| } |
|
|
| const openai = createOpenAI(openaiOptions); |
| model = shouldUseOpenAIResponsesApi(config.providerId, config.modelId) |
| ? openai.responses(config.modelId) |
| : openai.chat(config.modelId); |
| break; |
| } |
|
|
| case 'anthropic': { |
| const anthropic = createAnthropic({ |
| apiKey: effectiveApiKey, |
| baseURL: effectiveBaseUrl, |
| }); |
| model = anthropic.chat(config.modelId); |
| break; |
| } |
|
|
| case 'google': { |
| const googleOptions: Parameters<typeof createGoogleGenerativeAI>[0] = { |
| apiKey: effectiveApiKey, |
| baseURL: effectiveBaseUrl, |
| }; |
| if (config.proxy) { |
| const proxy = config.proxy; |
| let agent: unknown; |
| googleOptions.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { |
| const { ProxyAgent, fetch: undiciFetch } = (await import( |
| 'undici' |
| )) as { |
| ProxyAgent: new (proxyUrl: string) => unknown; |
| fetch: ( |
| input: string | URL | Request, |
| init?: Record<string, unknown>, |
| ) => Promise<unknown>; |
| }; |
| agent ??= new ProxyAgent(proxy); |
| const response = await undiciFetch(input, { |
| ...(init as Record<string, unknown>), |
| dispatcher: agent, |
| }); |
| return response as Response; |
| }) as typeof fetch; |
| } |
| const google = createGoogleGenerativeAI(googleOptions); |
| model = google.chat(config.modelId); |
| break; |
| } |
|
|
| default: |
| throw new Error(`Unsupported provider type: ${providerType}`); |
| } |
|
|
| |
| const modelInfo = provider?.models.find((m) => m.id === config.modelId) || null; |
|
|
| return { model, modelInfo }; |
| } |
|
|
| |
| |
| |
| export function parseModelString(modelString: string): { |
| providerId: ProviderId; |
| modelId: string; |
| } { |
| |
| const colonIndex = modelString.indexOf(':'); |
|
|
| if (colonIndex > 0) { |
| return { |
| providerId: modelString.slice(0, colonIndex) as ProviderId, |
| modelId: modelString.slice(colonIndex + 1), |
| }; |
| } |
|
|
| |
| return { |
| providerId: 'openai', |
| modelId: modelString, |
| }; |
| } |
|
|
| |
| |
| |
| export function getAllModels(): { |
| provider: ProviderConfig; |
| models: ModelInfo[]; |
| }[] { |
| return Object.values(PROVIDERS).map((provider) => ({ |
| provider, |
| models: provider.models, |
| })); |
| } |
|
|
| |
| |
| |
| export function getProvider(providerId: ProviderId): ProviderConfig | undefined { |
| return PROVIDERS[providerId]; |
| } |
|
|
| |
| |
| |
| export function getModelInfo(providerId: ProviderId, modelId: string): ModelInfo | undefined { |
| const provider = PROVIDERS[providerId]; |
| return provider?.models.find((m) => m.id === modelId); |
| } |
|
|