2360 lines
67 KiB
TypeScript
2360 lines
67 KiB
TypeScript
/**
|
|
* 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<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.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<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 '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<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 && 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<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 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<BedrockCredentials>;
|
|
|
|
let bedrockCredentialProviderPromise: Promise<BedrockCredentialProvider> | undefined;
|
|
|
|
function getBedrockCredentialProvider(): Promise<BedrockCredentialProvider> {
|
|
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<string, unknown>;
|
|
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<string, unknown>): 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<Response> {
|
|
const requestUrl = requestUrlString(input);
|
|
if (!requestUrl.includes('/chat/completions') || !init?.body || typeof init.body !== 'string') {
|
|
return globalThis.fetch(input, init);
|
|
}
|
|
|
|
let requestBody: Record<string, unknown>;
|
|
try {
|
|
requestBody = JSON.parse(init.body) as Record<string, unknown>;
|
|
} 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<string, unknown>)
|
|
: {};
|
|
|
|
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<string, unknown>;
|
|
const error =
|
|
chunk.error && typeof chunk.error === 'object' && !Array.isArray(chunk.error)
|
|
? (chunk.error as Record<string, unknown>)
|
|
: 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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown> = { 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<typeof createOpenAI>[0] = {
|
|
apiKey: effectiveApiKey,
|
|
baseURL: effectiveBaseUrl,
|
|
name: config.providerId,
|
|
};
|
|
|
|
if (useStreamingChatCompat) {
|
|
openaiOptions.fetch = fetchCustomOpenAIChat as typeof globalThis.fetch;
|
|
}
|
|
|
|
// 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 */
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = 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.
|
|
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 =
|
|
!useStreamingChatCompat && 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') {
|
|
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<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 'bedrock': {
|
|
const bedrock = createAmazonBedrock({
|
|
apiKey: effectiveApiKey || undefined,
|
|
region: resolveBedrockRegion(),
|
|
baseURL: effectiveBaseUrl,
|
|
credentialProvider: createBedrockCredentialProvider(),
|
|
});
|
|
model = bedrock(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 = findModelById(config.providerId, provider?.models, 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 findModelById(providerId, provider?.models, modelId);
|
|
}
|