// Model capabilities — what each model can read/do beyond plain text. // // Fallback order (first match wins), result merged over DEFAULT_CAPABILITIES: // 1. PROVIDER_CAPABILITIES[provider][model] — provider-specific override // 2. MODEL_CAPABILITIES[model] — canonical exact id (handles exceptions) // 3. PATTERN_CAPABILITIES — glob match, ordered specific -> generic // 4. DEFAULT_CAPABILITIES — safe floor (always returned) // // ── HOW TO ADD / UPDATE A MODEL ────────────────────────────────────── // Authoritative data source: https://models.dev/api.json (145 providers, 4000+ // models, MIT). Each model exposes the exact fields we map below: // modalities.input ["text","image","pdf","audio","video"] -> vision / pdf / audioInput / videoInput // modalities.output ["text","image","audio"] -> imageOutput / audioOutput // reasoning -> reasoning tool_call -> tools // limit.context -> contextWindow limit.output -> maxOutput // Look up the model id, then: // • If a PATTERN below already covers it correctly -> nothing to do. // • If it is an exception (pattern would mis-match) -> add an exact entry to // MODEL_CAPABILITIES (only the fields that differ from DEFAULT). // • If a whole new family -> add an ordered PATTERN (specific before generic). // NOTE: models.dev has NO "search" flag (web search is a runtime tool, not a // model spec); set `search` from vendor docs (Claude 4.x+, GPT-5.x/4o, Gemini // 2.0+, Grok, Perplexity). Verify with: curl -s https://models.dev/api.json import { matchPattern } from "./pricing.js"; /** * Safe floor — every resolved result is merged over this so consumers * never need null-checks. Most modern LLMs meet these limits. */ export const DEFAULT_CAPABILITIES = { // input modalities vision: false, // read images pdf: false, // read PDF / documents audioInput: false, // read audio videoInput: false, // read video // output modalities imageOutput: false, // generate images audioOutput: false, // generate audio // features search: false, // built-in web search tool / grounding tools: true, // function / tool calling reasoning: false, // thinking / reasoning // thinking wire format (only meaningful when reasoning:true). null → derive from transport.format. // enum: openai|claude-adaptive|claude-budget|gemini-level|gemini-budget|zai|qwen|deepseek|kimi|minimax|hunyuan|step thinkingFormat: null, thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable) thinkingRange: null, // { min, max } for budget formats; null = no clamp // limits (tokens) contextWindow: 200000, maxOutput: 64000, }; // User-added model metadata can carry dashboard service kinds instead of the // runtime capability names used here. Map those typed model kinds into input / // output capabilities so custom vision models are not treated as text-only. const SERVICE_KIND_CAPABILITIES = { imageToText: { vision: true }, image: { imageOutput: true }, stt: { audioInput: true }, tts: { audioOutput: true }, embedding: { tools: false }, }; export function capabilitiesFromServiceKind(kind) { return SERVICE_KIND_CAPABILITIES[kind] || null; } /** * Canonical exact-id overrides — used for exceptions that patterns would * otherwise mis-match. Only declare deltas vs DEFAULT. */ export const MODEL_CAPABILITIES = { // Claude 4.6/4.7 have 1M context + adaptive thinking (override generic claude pattern) "claude-opus-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-4.7": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-sonnet-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 }, "claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 }, // Gemini image-gen / OpenAI image / xai image variants "gpt-image-1": { imageOutput: true, tools: false }, // GLM vision variant (text GLM has no vision) "glm-4.6v": { vision: true, reasoning: true, thinkingFormat: "zai", contextWindow: 128000 }, // Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases "vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 }, "coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 }, }; /** * Provider-specific capability overrides. Keyed by provider alias/id. */ export const PROVIDER_CAPABILITIES = { // CodeBuddy.cn — authoritative per-model metadata from the gateway's model // config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision= // supportsImages). Every model reasons via OpenAI-style reasoning_effort // (see registry thinkingFormat). `onlyReasoning` models can't turn thinking // off → thinkingCanDisable:false (clamped to minimal instead of disabled). "codebuddy-cn": { "glm-5.2": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 }, "glm-5.1": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 }, "glm-5.0": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 }, "glm-5.0-turbo": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 }, "glm-5v-turbo": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 38000 }, "glm-4.7": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 }, "minimax-m3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 512000, maxOutput: 48000 }, "minimax-m2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 }, "kimi-k2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 }, "kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 }, "kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 }, "hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 }, "deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, "deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, "deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 }, }, }; /** * Pattern fallback — glob (* = wildcard), matched case-insensitively and * anchored (^...$) so a pattern must match the full model id. ORDER MATTERS: * vision/specific variants first, text-only/generic families last, to avoid * a broad family pattern swallowing an exception (e.g. glm-4.6v vs glm-5). */ export const PATTERN_CAPABILITIES = [ // ── Claude (4.6+ = adaptive thinking; older/haiku = budget) ────── { pattern: "*claude*opus-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } }, { pattern: "*claude*opus-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } }, { pattern: "*claude*opus-4.8*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } }, { pattern: "*claude*sonnet-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } }, { pattern: "*claude*sonnet-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } }, { pattern: "*claude*haiku*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } }, { pattern: "*claude*opus*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } }, { pattern: "*claude*sonnet*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } }, { pattern: "*claude*fable*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } }, { pattern: "*claude*mythos*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } }, { pattern: "*claude-3*", caps: { vision: true } }, { pattern: "*claude*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } }, // ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─ { pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } }, { pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } }, { pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-2.5*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-budget", thinkingRange: { min: 0, max: 24576 }, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-2*", caps: { vision: true, audioInput: true, videoInput: true, search: true, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini*", caps: { vision: true, search: true, contextWindow: 1048576 } }, { pattern: "*gemma*", caps: { vision: true, contextWindow: 128000 } }, { pattern: "*nanobanana*", caps: { vision: true, imageOutput: true } }, // ── OpenAI GPT-5.x (vision + thinking + web search) ────────────── { pattern: "*gpt-5*image*", caps: { imageOutput: true } }, { pattern: "*gpt-5*codex*", caps: { reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } }, { pattern: "*gpt-5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } }, { pattern: "*gpt-4o*", caps: { vision: true, search: true, contextWindow: 128000, maxOutput: 16384 } }, { pattern: "*gpt-4.1*", caps: { vision: true, contextWindow: 1000000, maxOutput: 32768 } }, { pattern: "*gpt-4-turbo*", caps: { vision: true, contextWindow: 128000 } }, { pattern: "*gpt-4*", caps: { contextWindow: 128000 } }, { pattern: "*gpt-3.5*", caps: { contextWindow: 16385, maxOutput: 4096 } }, { pattern: "*gpt-oss*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } }, // ── OpenAI o-series (reasoning, vision) ────────────────────────── { pattern: "*o1-mini*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } }, { pattern: "*o1*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } }, { pattern: "*o3*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } }, { pattern: "*o4*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } }, // ── Grok (vision + Live Search) ────────────────────────────────── { pattern: "*grok*image*", caps: { imageOutput: true } }, { pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } }, { pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } }, { pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 131072 } }, { pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } }, // ── Qwen (enable_thinking + thinking_budget; QwQ = thinking-only) ─ { pattern: "*qwen*vl*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } }, { pattern: "*qwen*max*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } }, { pattern: "*qwen*plus*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } }, { pattern: "*qwen*235b*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } }, { pattern: "*qwen*coder*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 } }, { pattern: "*qwq*", caps: { reasoning: true, thinkingFormat: "qwen", thinkingCanDisable: false, contextWindow: 131072 } }, { pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } }, // ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ───── { pattern: "*kimi*k2.7*code*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 262144 } }, { pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } }, { pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } }, // ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─ { pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } }, { pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } }, { pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } }, { pattern: "*glm*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } }, // ── DeepSeek (thinking.enabled + reasoning_effort; r1 = thinking-only) ─ { pattern: "*deepseek-v4*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 } }, { pattern: "*reasoner*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } }, { pattern: "*deepseek-r*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } }, { pattern: "*deepseek-chat*", caps: { contextWindow: 128000 } }, { pattern: "*deepseek*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000 } }, // ── MiniMax (M3 = adaptive; M2.x cannot disable) ───────────────── { pattern: "*minimax*image*", caps: { imageOutput: true } }, { pattern: "*minimax-m3*", caps: { vision: true, reasoning: true, thinkingFormat: "minimax", contextWindow: 1048576, maxOutput: 512000 } }, { pattern: "*minimax-m2.7*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 204800, maxOutput: 131072 } }, { pattern: "*minimax*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 131072 } }, // ── Xiaomi MiMo (vision, 1M / 262K ctx) ────────────────────────── { pattern: "*mimo*v2.5*", caps: { vision: true, contextWindow: 1048576, maxOutput: 131072 } }, { pattern: "*mimo*omni*", caps: { vision: true, audioInput: true, contextWindow: 262144, maxOutput: 131072 } }, { pattern: "*mimo*", caps: { vision: true, contextWindow: 262144, maxOutput: 131072 } }, // ── Llama (4 = vision/1M; 3.x = text-only/128K) ────────────────── { pattern: "*llama-4*", caps: { vision: true, contextWindow: 1000000 } }, { pattern: "*llama*", caps: { contextWindow: 128000 } }, // ── Mistral (Large 3 = vision/256K; codestral text) ────────────── { pattern: "*codestral*", caps: { contextWindow: 256000 } }, { pattern: "*mistral-large*", caps: { vision: true, contextWindow: 256000 } }, { pattern: "*mistral*", caps: { contextWindow: 128000 } }, // ── Cohere (Command A Vision = vision; others text) ────────────── { pattern: "*command-a-vision*", caps: { vision: true, contextWindow: 128000 } }, { pattern: "*command*", caps: { contextWindow: 128000 } }, // ── Perplexity (web search native) ─────────────────────────────── { pattern: "*sonar*", caps: { search: true, contextWindow: 128000 } }, { pattern: "*pplx*", caps: { search: true, contextWindow: 128000 } }, { pattern: "*perplexity*", caps: { search: true, contextWindow: 128000 } }, // ── Others ─────────────────────────────────────────────────────── { pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } }, { pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } }, { pattern: "*step-*", caps: { reasoning: true, thinkingFormat: "step", contextWindow: 128000 } }, { pattern: "*nemotron*", caps: { reasoning: true, contextWindow: 128000 } }, { pattern: "*ling-*", caps: { reasoning: true, contextWindow: 128000 } }, ]; /** * Resolve capabilities for a model using the 4-step fallback chain, * merged over DEFAULT_CAPABILITIES so the result is always complete. * * @param {string} provider * @param {string} model * @returns {object} full capabilities object */ export function getCapabilitiesForModel(provider, model) { if (!model) return { ...DEFAULT_CAPABILITIES }; // 1. Provider-specific override if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) { return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] }; } // 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7") const baseModel = model.includes("/") ? model.split("/").pop() : model; if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] }; if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] }; // 3. Pattern match (first match wins) for (const { pattern, caps } of PATTERN_CAPABILITIES) { if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) { return { ...DEFAULT_CAPABILITIES, ...caps }; } } // 4. Floor return { ...DEFAULT_CAPABILITIES }; }