/** * Unified AI Provider Configuration * * Supports multiple AI providers through Vercel AI SDK: * - OpenAI (native) * - Anthropic Claude (native) * - Amazon Bedrock (native) * - Google Gemini (native) * - MiniMax (Anthropic-compatible, recommended by official) * - OpenAI-compatible providers (DeepSeek, Qwen, Kimi, GLM, SiliconFlow, Doubao, Tencent, Xiaomi, Lemonade, etc.) * * Sources: * - https://platform.openai.com/docs/models * - https://platform.claude.com/docs/en/about-claude/models/overview * - https://ai.google.dev/gemini-api/docs/models * - https://api-docs.deepseek.com/quick_start/pricing * - https://platform.moonshot.cn/docs/pricing/chat * - https://platform.minimaxi.com/docs/guides/text-generation * - https://platform.minimaxi.com/docs/api-reference/text-anthropic-api * - https://docs.bigmodel.cn/cn/guide/start/model-overview * - https://help.aliyun.com/zh/model-studio/models (Qwen/DashScope) * - https://siliconflow.cn/models * - https://siliconflow.cn/pricing * - https://www.volcengine.com/docs/82379/1330310 * - https://platform.xiaomimimo.com/static/docs/pricing.md * - https://platform.xiaomimimo.com/static/docs/tokenplan/quick-access.md */ import { createOpenAI } from '@ai-sdk/openai'; import { createAzure } from '@ai-sdk/azure'; import { createAnthropic } from '@ai-sdk/anthropic'; import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; import { createKimiReasoningPreservationMiddleware, restoreKimiReasoningInRequestBody, wrapJsonResponseWithReasoning, wrapResponseWithReasoning, } from './reasoning-sse'; import type { LanguageModel } from 'ai'; import type { ProviderId, ProviderConfig, ModelInfo, ModelConfig, ThinkingConfig, } from '@/lib/types/provider'; import { applyModelMetadata, getCatalogThinkingCapability } from './model-metadata'; import { findModelById } from './model-aliases'; import { getDefaultThinkingConfig, getThinkingMode, pickThinkingBudget, pickThinkingEffort, } from './thinking-config'; import { createLogger } from '@/lib/logger'; import { normalizeAzureBaseUrl } from './azure'; // NOTE: Do NOT import thinking-context.ts here — it uses node:async_hooks // which is server-only, and this file is also used on the client via // settings.ts. The thinking context is read from globalThis instead // (set by thinking-context.ts at module load time on the server). const log = createLogger('AIProviders'); // Re-export types for backward compatibility export type { ProviderId, ProviderConfig, ModelInfo, ModelConfig }; /** Provider IDs whose logos are monochrome-dark and need `dark:invert` in dark mode */ export const MONO_LOGO_PROVIDERS: ReadonlySet = new Set(['openai', 'openrouter', 'ollama']); /** * Provider registry */ export const PROVIDERS: Record = { openai: { id: 'openai', name: 'OpenAI', type: 'openai', defaultBaseUrl: 'https://api.openai.com/v1', requiresApiKey: true, icon: '/logos/openai.svg', models: [ { id: 'gpt-5.6', name: 'GPT-5.6 Sol', contextWindow: 1050000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'gpt-5.6-terra', name: 'GPT-5.6 Terra', contextWindow: 1050000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'gpt-5.6-luna', name: 'GPT-5.6 Luna', contextWindow: 1050000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { 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, }, }, }, ], }, azure: { id: 'azure', name: 'Azure OpenAI', type: 'azure', baseUrlPlaceholder: 'https://YOUR-RESOURCE.openai.azure.com/openai', supportsModelDiscovery: false, requiresApiKey: true, icon: '/logos/azure.svg', // Azure requests use user-defined deployment names rather than model IDs. models: [], }, atlascloud: { id: 'atlascloud', name: 'Atlas Cloud', type: 'openai', defaultBaseUrl: 'https://api.atlascloud.ai/v1', supportsModelDiscovery: true, requiresApiKey: true, models: [ { id: 'qwen/qwen3.5-flash', name: 'Qwen3.5 Flash', contextWindow: 1000000, outputWindow: 67072, capabilities: { streaming: true, tools: false, vision: false }, }, { id: 'deepseek-ai/deepseek-v4-pro', name: 'DeepSeek V4 Pro', contextWindow: 1048576, outputWindow: 393216, // Live-verified with enabled/disabled thinking payloads and a forced // OpenAI-compatible function call against Atlas Cloud. capabilities: { streaming: true, tools: true, vision: false, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, ], }, anthropic: { id: 'anthropic', name: 'Claude', type: 'anthropic', requiresApiKey: true, defaultBaseUrl: 'https://api.anthropic.com/v1', icon: '/logos/claude.svg', models: [ { id: 'claude-opus-5', name: 'Claude Opus 5', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'claude-sonnet-5', name: 'Claude Sonnet 5', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'claude-fable-5', name: 'Claude Fable 5', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: false, defaultEnabled: true, }, }, }, { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: false, }, }, }, { 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, }, }, }, ], }, bedrock: { id: 'bedrock', name: 'Amazon Bedrock', type: 'bedrock', requiresApiKey: false, icon: '/logos/bedrock.svg', models: [ { id: 'us.anthropic.claude-sonnet-5', name: 'Claude Sonnet 5 (Bedrock)', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.anthropic.claude-opus-4-8', name: 'Claude Opus 4.8 (Bedrock)', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.anthropic.claude-opus-4-7', name: 'Claude Opus 4.7 (Bedrock)', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.anthropic.claude-sonnet-4-6', name: 'Claude Sonnet 4.6 (Bedrock)', contextWindow: 1000000, outputWindow: 64000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.amazon.nova-pro-v1:0', name: 'Amazon Nova Pro', contextWindow: 300000, outputWindow: 10000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.amazon.nova-lite-v1:0', name: 'Amazon Nova Lite', contextWindow: 300000, outputWindow: 10000, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'us.amazon.nova-micro-v1:0', name: 'Amazon Nova Micro', contextWindow: 128000, outputWindow: 10000, capabilities: { streaming: true, tools: true, vision: false }, }, { id: 'us.meta.llama3-3-70b-instruct-v1:0', name: 'Llama 3.3 70B Instruct (Bedrock)', contextWindow: 128000, capabilities: { streaming: true, tools: true, vision: false }, }, ], }, google: { id: 'google', name: 'Gemini', type: 'google', requiresApiKey: true, defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta', icon: '/logos/gemini.svg', models: [ { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', contextWindow: 1048576, outputWindow: 65536, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'gemini-3.5-flash-lite', name: 'Gemini 3.5 Flash-Lite', contextWindow: 1048576, outputWindow: 65536, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', contextWindow: 1048576, outputWindow: 65536, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { 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: [ // GLM-5.2 Series - Long-horizon coding model { id: 'glm-5.2', name: 'GLM-5.2', contextWindow: 1000000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: false, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, // GLM-5.1 Series { 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 }, }, // GLM-5 Series { id: 'glm-5', name: 'GLM-5', contextWindow: 200000, outputWindow: 128000, capabilities: { streaming: true, tools: true, vision: false }, }, // GLM-4.7 Series { 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 }, }, // GLM-4.6 Series - Advanced coding & reasoning { 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.7-plus', name: 'Qwen3.7 Plus', contextWindow: 1000000, outputWindow: 64000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'qwen3.7-max', name: 'Qwen3.7 Max', contextWindow: 1000000, outputWindow: 64000, capabilities: { streaming: true, tools: true, vision: false, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: true, }, }, }, { 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-k3', name: 'Kimi K3', contextWindow: 1048576, outputWindow: 131072, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'kimi-k2.7-code', name: 'Kimi K2.7 Code', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: false, defaultEnabled: true, }, }, }, { id: 'kimi-k2.7-code-highspeed', name: 'Kimi K2.7 Code HighSpeed', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: false, defaultEnabled: true, }, }, }, { 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, }, }, }, // K2.5 Series (2026) - 1T MoE, 32B active parameters { 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-M3', name: 'MiniMax M3', contextWindow: 1000000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, { 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: [ // DeepSeek Series { 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 }, }, // Qwen Series { id: 'Qwen/Qwen3-VL-32B-Instruct', name: 'Qwen3-VL-32B-Instruct', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, // Kimi Series { id: 'Pro/moonshotai/Kimi-K2.5', name: 'Kimi-K2.5', contextWindow: 256000, outputWindow: 96000, capabilities: { streaming: true, tools: true, vision: false }, }, // GLM Series { 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-1-pro-260628', name: 'Doubao Seed 2.1 Pro', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'doubao-seed-2-1-turbo-260628', name: 'Doubao Seed 2.1 Turbo', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'doubao-seed-evolving', name: 'Doubao Seed Evolving', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, { id: 'doubao-seed-character-260628', name: 'Doubao Seed Character', contextWindow: 256000, outputWindow: 32768, capabilities: { streaming: true, tools: true, vision: true }, }, { 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.6', name: 'Grok 4.6', contextWindow: 500000, outputWindow: 500000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'grok-4.5', name: 'Grok 4.5', contextWindow: 500000, outputWindow: 500000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: true, defaultEnabled: true, }, }, }, { id: 'grok-4.3', name: 'Grok 4.3', contextWindow: 1000000, outputWindow: 30000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: true, defaultEnabled: false, }, }, }, { id: 'grok-build-0.1', name: 'Grok Build 0.1', contextWindow: 256000, outputWindow: 256000, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: false, budgetAdjustable: false, defaultEnabled: true, }, }, }, { 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', // Token Plan endpoints use the same OpenAI-compatible path with regional hosts. alternateBaseUrls: [ { label: 'settings.baseUrlRegion.xiaomiPayg', url: 'https://api.xiaomimimo.com/v1' }, { label: 'settings.baseUrlRegion.xiaomiTokenPlanCN', url: 'https://token-plan-cn.xiaomimimo.com/v1', }, { label: 'settings.baseUrlRegion.xiaomiTokenPlanSGP', url: 'https://token-plan-sgp.xiaomimimo.com/v1', }, { label: 'settings.baseUrlRegion.xiaomiTokenPlanEU', url: 'https://token-plan-ams.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-pro', name: 'MiMo V2 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, }, }, }, { id: 'mimo-v2-omni', name: 'MiMo V2 Omni', contextWindow: 262144, outputWindow: 131072, capabilities: { streaming: true, tools: true, vision: true, thinking: { toggleable: true, budgetAdjustable: false, defaultEnabled: true, }, }, }, { id: 'mimo-v2-flash', name: 'MiMo V2 Flash', contextWindow: 262144, outputWindow: 65536, capabilities: { streaming: true, tools: true, vision: false, 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 }, }, ], }, lemonade: { id: 'lemonade', name: 'Lemonade', type: 'openai', defaultBaseUrl: 'http://localhost:13305/v1', requiresApiKey: false, icon: '/logos/lemonade.svg', models: [ { id: 'Gemma-4-26B-A4B-it-GGUF', name: 'Gemma 4 26B A4B IT GGUF', capabilities: { streaming: true, tools: true, vision: false }, }, ], }, }; applyModelMetadata(PROVIDERS); /** * Get provider config (from built-in or unified config in localStorage) */ function getProviderConfig(providerId: ProviderId): ProviderConfig | null { // Check built-in providers first if (PROVIDERS[providerId]) { return PROVIDERS[providerId]; } // Check unified providersConfig in localStorage (browser only) 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; } /** * Model instance with its configuration info */ export interface ModelWithInfo { model: LanguageModel; modelInfo: ModelInfo | null; } function getCompatThinkingBodyParams( providerId: ProviderId, modelId: string, config: ThinkingConfig, ): Record | undefined { // This model is served through an OpenAI-compatible gateway even when the // deployment uses the `openai` provider slot. The gateway's chat template // toggle is neither OpenAI's `reasoning_effort` nor DeepSeek's native // `thinking` object: it requires this exact vLLM template argument. if (providerId === 'openai' && modelId === 'deepseek-v4-flash-vision-exp') { const mode = getThinkingMode(config); return mode === undefined ? undefined : { chat_template_kwargs: { thinking: mode === 'enabled' } }; } 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 'openai': { const effort = pickThinkingEffort(capability, config); return effort ? { reasoning_effort: effort } : undefined; } case 'kimi': case 'xiaomi': if (mode === 'disabled') return { thinking: { type: 'disabled' } }; if (mode === 'enabled') return { thinking: { type: 'enabled' } }; return undefined; case 'glm': { if (capability.control === 'effort') { if (mode === 'disabled' || config.effort === 'none') { return { thinking: { type: 'disabled' } }; } const effort = config.effort && capability.effortValues?.includes(config.effort) ? config.effort : mode === 'enabled' ? capability.defaultEffort : undefined; const body: Record = {}; if (mode === 'enabled' || effort) body.thinking = { type: 'enabled' }; if (effort) body.reasoning_effort = effort; return Object.keys(body).length > 0 ? body : undefined; } 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 = {}; 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 = {}; if (capability.control === 'toggle-budget') { if (mode === 'disabled') body.enable_thinking = false; if (mode === 'enabled') body.enable_thinking = true; } if (budget !== undefined && budget > 0) 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 = {}; 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; } case 'lemonade': { const chatTemplateKwargs: Record = {}; if (mode === 'enabled') { chatTemplateKwargs.enable_thinking = true; } else { chatTemplateKwargs.enable_thinking = false; } if (mode === 'enabled' && budget !== undefined) { chatTemplateKwargs.thinking_budget = budget; } return { chat_template_kwargs: chatTemplateKwargs }; } 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 resolveBedrockRegion(): string { return ( process.env.BEDROCK_REGION?.trim() || process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim() || 'us-east-1' ); } interface BedrockCredentials { accessKeyId: string; secretAccessKey: string; sessionToken?: string; expiration?: Date; } type BedrockCredentialProvider = () => Promise; let bedrockCredentialProviderPromise: Promise | undefined; function getBedrockCredentialProvider(): Promise { bedrockCredentialProviderPromise ??= import('@aws-sdk/credential-providers').then( ({ fromNodeProviderChain }) => fromNodeProviderChain(), ); return bedrockCredentialProviderPromise; } function createBedrockCredentialProvider(): BedrockCredentialProvider { return async () => { const credentialProvider = await getBedrockCredentialProvider(); const credentials = await credentialProvider(); return { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken, expiration: credentials.expiration, }; }; } function shouldUseOpenAIResponsesApi(providerId: ProviderId, modelId: string): boolean { if (providerId !== 'openai') return false; return ( /^gpt-5\.\d+-pro(?:-|$)/.test(modelId) || /^gpt-5\.6(?:-|$)/.test(modelId) || /^gpt-5\.5(?:-|$)/.test(modelId) || /^gpt-5\.[3-9]-codex(?:-|$)/.test(modelId) ); } function usesCustomOpenAIBaseUrl(baseUrl?: string): boolean { if (!baseUrl) return false; const trimmed = baseUrl.trim(); if (!trimmed) return false; try { const url = new URL(trimmed); const pathname = url.pathname.replace(/\/+$/, ''); return url.origin !== 'https://api.openai.com' || pathname !== '/v1'; } catch { return true; } } function shouldUseOpenAIStreamingChatCompat(providerId: ProviderId, baseUrl?: string): boolean { return ( providerId === 'openai' && usesCustomOpenAIBaseUrl(baseUrl) && process.env.OPENAI_COMPAT_USE_STREAMING_CHAT === 'true' ); } function requestUrlString(input: RequestInfo | URL): string { if (typeof input === 'string') return input; if (input instanceof URL) return input.toString(); return input.url; } function appendChatDelta(value: unknown): string { if (typeof value === 'string') return value; if (!Array.isArray(value)) return ''; return value .map((part) => { if (!part || typeof part !== 'object') return ''; const record = part as Record; return typeof record.text === 'string' ? record.text : ''; }) .join(''); } function openAIJsonResponseHeaders(response: Response): Headers { const headers = new Headers(response.headers); headers.set('content-type', 'application/json'); headers.delete('content-length'); headers.delete('content-encoding'); headers.delete('transfer-encoding'); return headers; } function openAIStreamErrorStatus(error: Record): number { const status = typeof error.code === 'number' ? error.code : typeof error.code === 'string' ? Number(error.code) : NaN; return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500; } async function fetchCustomOpenAIChat( input: RequestInfo | URL, init?: RequestInit, ): Promise { const requestUrl = requestUrlString(input); if (!requestUrl.includes('/chat/completions') || !init?.body || typeof init.body !== 'string') { return globalThis.fetch(input, init); } let requestBody: Record; try { requestBody = JSON.parse(init.body) as Record; } catch { return globalThis.fetch(input, init); } if (requestBody.stream === true) return globalThis.fetch(input, init); const streamOptions = requestBody.stream_options && typeof requestBody.stream_options === 'object' && !Array.isArray(requestBody.stream_options) ? (requestBody.stream_options as Record) : {}; const response = await globalThis.fetch(input, { ...init, body: JSON.stringify({ ...requestBody, stream: true, stream_options: { ...streamOptions, include_usage: true }, }), }); if (!response.ok) return response; const rawStream = await response.text(); const streamLines = rawStream.split(/\r?\n/); if (!streamLines.some((line) => line.startsWith('data:'))) { const headers = new Headers(response.headers); headers.delete('content-length'); headers.delete('content-encoding'); headers.delete('transfer-encoding'); return new Response(rawStream, { status: response.status, statusText: response.statusText, headers, }); } let id = ''; let created = 0; let model = typeof requestBody.model === 'string' ? requestBody.model : ''; let content = ''; let finishReason: unknown = null; let usage: unknown; const toolCalls = new Map< number, { id: string; type: string; function: { name: string; arguments: string } } >(); for (const line of streamLines) { if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data || data === '[DONE]') continue; try { const chunk = JSON.parse(data) as Record; const error = chunk.error && typeof chunk.error === 'object' && !Array.isArray(chunk.error) ? (chunk.error as Record) : undefined; if (error && typeof error.message === 'string') { return new Response(JSON.stringify(chunk), { status: openAIStreamErrorStatus(error), headers: openAIJsonResponseHeaders(response), }); } if (typeof chunk.id === 'string') id = chunk.id; if (typeof chunk.created === 'number') created = chunk.created; if (typeof chunk.model === 'string') model = chunk.model; if (chunk.usage) usage = chunk.usage; const choices = Array.isArray(chunk.choices) ? chunk.choices : []; for (const rawChoice of choices) { if (!rawChoice || typeof rawChoice !== 'object') continue; const choice = rawChoice as Record; if (typeof choice.index === 'number' && choice.index !== 0) continue; if (choice.finish_reason) finishReason = choice.finish_reason; if (!choice.delta || typeof choice.delta !== 'object') continue; const delta = choice.delta as Record; content += appendChatDelta(delta.content); if (!Array.isArray(delta.tool_calls)) continue; for (const rawToolCall of delta.tool_calls) { if (!rawToolCall || typeof rawToolCall !== 'object') continue; const toolCall = rawToolCall as Record; const index = typeof toolCall.index === 'number' ? toolCall.index : 0; const current = toolCalls.get(index) || { id: '', type: 'function', function: { name: '', arguments: '' }, }; if (typeof toolCall.id === 'string') current.id = toolCall.id; if (typeof toolCall.type === 'string') current.type = toolCall.type; if (toolCall.function && typeof toolCall.function === 'object') { const fn = toolCall.function as Record; if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name; if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments; } toolCalls.set(index, current); } } } catch { // Ignore non-JSON SSE lines and continue collecting valid chunks. } } const message: Record = { role: 'assistant', content }; if (toolCalls.size > 0) { message.tool_calls = [...toolCalls.entries()] .sort(([a], [b]) => a - b) .map(([, toolCall]) => toolCall); } return new Response( JSON.stringify({ id: id || `chatcmpl_${Date.now()}`, object: 'chat.completion', created: created || Math.floor(Date.now() / 1000), model, choices: [{ index: 0, message, finish_reason: finishReason }], ...(usage ? { usage } : {}), }), { status: response.status, statusText: response.statusText, headers: openAIJsonResponseHeaders(response), }, ); } /** Returns true if the provider requires an API key (defaults to true for unknown providers). */ export function isProviderKeyRequired(providerId: string): boolean { return getProviderConfig(providerId as ProviderId)?.requiresApiKey ?? true; } /** * Get a configured language model instance with its info * Accepts individual parameters for flexibility and security */ export function getModel(config: ModelConfig): ModelWithInfo { // providerType can come from client for custom providers; fall back to registry. let providerType = config.providerType; const provider = getProviderConfig(config.providerId); const requiresApiKey = provider?.requiresApiKey ?? true; if (provider && providerType && providerType !== provider.type) { throw new Error( `Provider type mismatch for ${config.providerId}: expected ${provider.type}, received ${providerType}.`, ); } if (!providerType) { if (provider) { providerType = provider.type; } else { throw new Error(`Unknown provider: ${config.providerId}. Please provide providerType.`); } } // Validate API key if required if (requiresApiKey && !config.apiKey) { throw new Error(`API key required for provider: ${config.providerId}`); } // Use provided API key, or empty string for providers that don't require one const effectiveApiKey = config.apiKey || ''; // Resolve base URL: explicit > provider default > SDK default const effectiveBaseUrl = normalizeMiniMaxAnthropicBaseUrl( config.providerId, config.baseUrl || provider?.defaultBaseUrl || undefined, ); let model: LanguageModel; switch (providerType) { case 'azure': { const azure = createAzure({ apiKey: effectiveApiKey, baseURL: normalizeAzureBaseUrl(effectiveBaseUrl), }); model = azure(config.modelId); break; } case 'openai': { const useStreamingChatCompat = shouldUseOpenAIStreamingChatCompat( config.providerId, effectiveBaseUrl, ); const openaiOptions: Parameters[0] = { apiKey: effectiveApiKey, baseURL: effectiveBaseUrl, name: config.providerId, }; // A custom base URL makes the `openai` slot an OpenAI-compatible gateway, // not the native OpenAI service. Give it the same request/response seam // as named compatible providers: inject the gateway's thinking control // and recover reasoning_content before the SDK schema can discard it. const usesOpenAIResponses = !useStreamingChatCompat && shouldUseOpenAIResponsesApi(config.providerId, config.modelId); const usesCompatTransport = config.providerId !== 'openai' || (usesCustomOpenAIBaseUrl(config.baseUrl) && !usesOpenAIResponses); if (usesCompatTransport) { const providerId = config.providerId; const compatFetch = async (url: RequestInfo | URL, init?: RequestInit) => { // Read thinking config from globalThis (set by thinking-context.ts) const thinkingCtx = (globalThis as Record).__thinkingContext as | { getStore?: () => unknown } | undefined; const thinkingFromContext = thinkingCtx?.getStore?.() as ThinkingConfig | undefined; const thinking = thinkingFromContext ?? (providerId === 'lemonade' ? getDefaultThinkingConfig(getCatalogThinkingCapability(providerId, config.modelId)) : 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); if (providerId === 'lemonade' && 'stream_options' in body) { delete body.stream_options; } Object.assign(body, extra); init = { ...init, body: JSON.stringify(body) }; } catch { /* leave body as-is */ } } } if ( providerId === 'kimi' && config.modelId === 'kimi-k3' && init?.body && typeof init.body === 'string' ) { try { const body = JSON.parse(init.body); restoreKimiReasoningInRequestBody(body); init = { ...init, body: JSON.stringify(body) }; } catch { /* leave body as-is */ } } const response = useStreamingChatCompat ? await fetchCustomOpenAIChat(url, init) : await globalThis.fetch(url, init); // Recover reasoning that @ai-sdk/openai's chat schema drops: rewrite // streamed `reasoning_content` deltas into an inline block // (the model below is wrapped with extractReasoningMiddleware to split // it back into first-class reasoning parts). No-op when absent. let streaming = false; if (init?.body && typeof init.body === 'string') { try { streaming = JSON.parse(init.body)?.stream === true; } catch { /* ignore request-body inspection failure */ } } const normalizedReasoningResponse = streaming ? wrapResponseWithReasoning(response) : providerId === 'kimi' && config.modelId === 'kimi-k3' ? await wrapJsonResponseWithReasoning(response) : response; if (providerId !== 'lemonade') { return normalizedReasoningResponse; } const contentType = response.headers.get('content-type') || ''; let isStreamingRequest = false; if (init?.body && typeof init.body === 'string') { try { const requestBody = JSON.parse(init.body); isStreamingRequest = requestBody?.stream === true; } catch { /* ignore request-body inspection failure */ } } if (isStreamingRequest) { return response; } try { const cloned = response.clone(); const text = await cloned.text(); try { JSON.parse(text); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.warn( `[Lemonade] Invalid JSON response from OpenAI-compatible path: status=${response.status}, contentType=${contentType || 'n/a'}, bodyLen=${text.length}, first=${JSON.stringify(text.slice(0, 500))}, last=${JSON.stringify(text.slice(Math.max(0, text.length - 500)))}, parseError=${message}`, ); } } catch (error) { log.warn('[Lemonade] Failed to inspect JSON response body:', error); } return response; }; openaiOptions.fetch = compatFetch as typeof globalThis.fetch; } const openai = createOpenAI(openaiOptions); model = usesOpenAIResponses ? openai.responses(config.modelId) : openai.chat(config.modelId); // OpenAI-compatible providers (e.g. DeepSeek, Qwen), including a custom // gateway configured through the `openai` slot, stream reasoning // either as a separate `reasoning_content` field (normalized to an inline // block by compatFetch) or as native inline . // Split it into first-class reasoning parts so the agent stream and UI can // show a thinking panel and the answer text stays clean. if (usesCompatTransport) { const middleware = config.providerId === 'kimi' && config.modelId === 'kimi-k3' ? [ createKimiReasoningPreservationMiddleware(), extractReasoningMiddleware({ tagName: 'think' }), ] : extractReasoningMiddleware({ tagName: 'think' }); model = wrapLanguageModel({ model, middleware, }); } break; } case 'anthropic': { const anthropicOptions: Parameters[0] = { baseURL: effectiveBaseUrl, }; if (config.providerId === 'minimax' && effectiveApiKey.startsWith('sk-cp-')) { anthropicOptions.authToken = effectiveApiKey; } else { anthropicOptions.apiKey = effectiveApiKey; } if (config.providerId === 'minimax') { anthropicOptions.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => { const capability = getCatalogThinkingCapability(config.providerId, config.modelId); const thinkingCtx = (globalThis as Record).__thinkingContext as | { getStore?: () => unknown } | undefined; const thinking = thinkingCtx?.getStore?.() as ThinkingConfig | undefined; if ( capability?.requestAdapter === 'anthropic' && capability.control !== 'none' && getThinkingMode(thinking) === 'disabled' && init?.body && typeof init.body === 'string' ) { try { const body = JSON.parse(init.body); body.thinking = { type: 'disabled' }; init = { ...init, body: JSON.stringify(body) }; } catch { /* leave body as-is */ } } return globalThis.fetch(url, init); }) as typeof globalThis.fetch; } const anthropic = createAnthropic(anthropicOptions); model = anthropic.chat(config.modelId); break; } case 'bedrock': { const bedrock = createAmazonBedrock({ apiKey: effectiveApiKey || undefined, region: resolveBedrockRegion(), baseURL: effectiveBaseUrl, credentialProvider: createBedrockCredentialProvider(), }); model = bedrock(config.modelId); break; } case 'google': { const googleOptions: Parameters[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( /* webpackIgnore: true */ 'undici' )) as { ProxyAgent: new (proxyUrl: string) => unknown; fetch: ( input: string | URL | Request, init?: Record, ) => Promise; }; agent ??= new ProxyAgent(proxy); const response = await undiciFetch(input, { ...(init as Record), 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}`); } // Look up model info from the provider registry const modelInfo = findModelById(config.providerId, provider?.models, config.modelId) ?? null; return { model, modelInfo }; } /** * Deprecation notice for bare model ids (no `provider:` prefix). parseModelString * keeps defaulting them to `openai` for backward compatibility, but that fallback * is deprecated: configs should write `provider:model` explicitly. Emitted only * by the boot-time config validation for config-derived sites — never for * request-derived strings, which would let clients drive log volume. */ export const BARE_MODEL_ID_DEPRECATION_MSG = 'bare model ids default to openai for backward compatibility; this fallback is deprecated — write provider:model'; /** Bare model ids already surfaced, so the deprecation fires once per unique id. */ const warnedBareModelIds = new Set(); /** * Warn once per unique bare model id. `where` names the config site (e.g. * `DEFAULT_MODEL` or a MODEL_ROUTES stage). Callers must pass only * config-derived ids (the config surface is finite, so the dedupe set is * bounded); request-derived strings must never reach this function. */ export function warnBareModelIdDeprecation(bareModelId: string, where?: string): boolean { if (warnedBareModelIds.has(bareModelId)) return false; warnedBareModelIds.add(bareModelId); const context = where ? `${where}: ` : ''; console.warn(`[config] ${context}${BARE_MODEL_ID_DEPRECATION_MSG} (bare id "${bareModelId}")`); return true; } /** * Parse model string in format "providerId:modelId" or just "modelId" (defaults to OpenAI) */ export function parseModelString(modelString: string): { providerId: ProviderId; modelId: string; } { // Split only on the first colon to handle model IDs that contain colons const colonIndex = modelString.indexOf(':'); if (colonIndex > 0) { return { providerId: modelString.slice(0, colonIndex) as ProviderId, modelId: modelString.slice(colonIndex + 1), }; } // Default to OpenAI for backward compatibility (deprecated; boot-time config // validation warns for config-derived bare ids). Deliberately no warning // here: this path is reachable with request-controlled strings, which must // not drive logging or dedupe-set growth. return { providerId: 'openai', modelId: modelString, }; } /** * Get all available models grouped by provider */ export function getAllModels(): { provider: ProviderConfig; models: ModelInfo[]; }[] { return Object.values(PROVIDERS).map((provider) => ({ provider, models: provider.models, })); } /** * Get provider by ID */ export function getProvider(providerId: ProviderId): ProviderConfig | undefined { return PROVIDERS[providerId]; } /** * Get model info */ export function getModelInfo(providerId: ProviderId, modelId: string): ModelInfo | undefined { const provider = PROVIDERS[providerId]; return findModelById(providerId, provider?.models, modelId); }