thu-maic--openmaic
1718 行
48 KiB
TypeScript
1718 行
48 KiB
TypeScript
/**
|
|
* Unified AI Provider Configuration
|
|
*
|
|
* Supports multiple AI providers through Vercel AI SDK:
|
|
* - OpenAI (native)
|
|
* - Anthropic Claude (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 { createAnthropic } from '@ai-sdk/anthropic';
|
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
import { wrapLanguageModel, extractReasoningMiddleware } from 'ai';
|
|
import { 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 { getDefaultThinkingConfig, getThinkingMode, pickThinkingBudget } from './thinking-config';
|
|
import { createLogger } from '@/lib/logger';
|
|
// 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<string> = new Set(['openai', 'openrouter', 'ollama']);
|
|
|
|
/**
|
|
* Provider registry
|
|
*/
|
|
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-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,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
google: {
|
|
id: 'google',
|
|
name: 'Gemini',
|
|
type: 'google',
|
|
requiresApiKey: true,
|
|
defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
icon: '/logos/gemini.svg',
|
|
models: [
|
|
{
|
|
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-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.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<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 '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<string, unknown> = {};
|
|
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<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;
|
|
}
|
|
|
|
case 'lemonade': {
|
|
const chatTemplateKwargs: Record<string, unknown> = {};
|
|
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 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)
|
|
);
|
|
}
|
|
|
|
/** 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 (!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 'openai': {
|
|
const openaiOptions: Parameters<typeof createOpenAI>[0] = {
|
|
apiKey: effectiveApiKey,
|
|
baseURL: effectiveBaseUrl,
|
|
};
|
|
|
|
// For OpenAI-compatible providers (not native OpenAI), add a fetch
|
|
// wrapper that injects vendor-specific thinking params into the HTTP
|
|
// body. The thinking config is read from AsyncLocalStorage, set by
|
|
// callLLM / streamLLM at call time.
|
|
if (config.providerId !== 'openai') {
|
|
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<string, unknown>).__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 */
|
|
}
|
|
}
|
|
}
|
|
const response = await globalThis.fetch(url, init);
|
|
|
|
// Recover reasoning that @ai-sdk/openai's chat schema drops: rewrite
|
|
// streamed `reasoning_content` deltas into an inline <think> block
|
|
// (the model below is wrapped with extractReasoningMiddleware to split
|
|
// it back into first-class reasoning parts). No-op when absent.
|
|
const streamingReasoned = (() => {
|
|
let streaming = false;
|
|
if (init?.body && typeof init.body === 'string') {
|
|
try {
|
|
streaming = JSON.parse(init.body)?.stream === true;
|
|
} catch {
|
|
/* ignore request-body inspection failure */
|
|
}
|
|
}
|
|
return streaming ? wrapResponseWithReasoning(response) : response;
|
|
})();
|
|
|
|
if (providerId !== 'lemonade') {
|
|
return streamingReasoned;
|
|
}
|
|
|
|
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 = shouldUseOpenAIResponsesApi(config.providerId, config.modelId)
|
|
? openai.responses(config.modelId)
|
|
: openai.chat(config.modelId);
|
|
// OpenAI-compatible providers (e.g. DeepSeek, Qwen) stream reasoning
|
|
// either as a separate `reasoning_content` field (normalized to an inline
|
|
// <think> block by compatFetch) or as native inline <think>.
|
|
// Split it into first-class reasoning parts so the agent stream and UI can
|
|
// show a thinking panel and the answer text stays clean. Native OpenAI
|
|
// handles reasoning itself, so it is excluded.
|
|
if (config.providerId !== 'openai') {
|
|
model = wrapLanguageModel({
|
|
model,
|
|
middleware: extractReasoningMiddleware({ tagName: 'think' }),
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'anthropic': {
|
|
const anthropicOptions: Parameters<typeof createAnthropic>[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<string, unknown>).__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 '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(
|
|
/* webpackIgnore: true */ '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}`);
|
|
}
|
|
|
|
// Look up model info from the provider registry
|
|
const modelInfo = provider?.models.find((m) => m.id === config.modelId) || null;
|
|
|
|
return { model, modelInfo };
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
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 provider?.models.find((m) => m.id === modelId);
|
|
}
|