2026-06-15 11:38:43 +07:00
// 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)
//
2026-08-27 17:53:53 +07:00
// Two extra layers then refine the result, and neither can override the hand
// written tables above (steps 1-2 short-circuit before they are consulted):
// • the synced catalog — modalities keyed by model, limits keyed by provider
// + model, refreshed from models.dev in the background. It reads a file, so
// the server installs it via setCatalogSource(); this module stays free of
// node:fs because the dashboard bundles it into the browser too.
// • visionPatterns.js — name-based vision detection, last resort so a model
// nobody has catalogued yet still accepts images.
// Both only ever turn a capability ON.
//
2026-06-15 11:38:43 +07:00
// ── 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" ;
2026-08-27 17:53:53 +07:00
import { looksLikeVisionModel } from "./visionPatterns.js" ;
2026-06-15 11:38:43 +07:00
/**
* 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
2026-06-15 18:18:04 +07:00
// 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
2026-08-28 12:32:26 +07:00
thinkingEffortSupported : false , // zai format only: model accepts a reasoning_effort level (GLM-5.2+; older GLM ignores it)
2026-06-15 11:38:43 +07:00
// limits (tokens)
contextWindow : 200000 ,
maxOutput : 64000 ,
};
2026-06-18 09:13:27 +07:00
// 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 ;
}
2026-06-15 11:38:43 +07:00
/**
* Canonical exact-id overrides — used for exceptions that patterns would
* otherwise mis-match. Only declare deltas vs DEFAULT.
*/
export const MODEL_CAPABILITIES = {
2026-09-02 20:26:02 +07:00
// Claude Fable 5.1, Opus 5, 4.6/4.7/4.8, and Kiro Sonnet 5 have 1M context + adaptive thinking (override generic claude pattern)
"claude-fable-5-1" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 128000 },
2026-07-25 17:30:02 +07:00
"claude-opus-5" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-07-29 19:32:31 +07:00
"claude-opus-5-thinking" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-opus-5-agentic" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-opus-5-thinking-agentic" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-15 18:18:04 +07:00
"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 },
2026-06-26 11:37:21 +07:00
"claude-opus-4-7" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-26 11:17:48 +07:00
"claude-opus-4.8" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-15 18:18:04 +07:00
"claude-opus-4-6" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-26 11:17:48 +07:00
"claude-opus-4-8" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-opus-4.8-thinking" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-opus-4-8-thinking" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-26 11:58:29 +07:00
"claude-sonnet-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 : 128000 },
2026-07-03 10:52:22 +07:00
"claude-sonnet-5" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-sonnet-5-thinking" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-sonnet-5-agentic" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
"claude-sonnet-5-thinking-agentic" : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-15 11:38:43 +07:00
// Gemini image-gen / OpenAI image / xai image variants
"gpt-image-1" : { imageOutput : true , tools : false },
2026-08-27 17:53:53 +07:00
// GLM vision variants (text GLM has no vision) — 5.3-Flash and 5V-Turbo are
// natively multimodal per z.ai, and 5.3-Flash carries the full 1M window.
2026-08-27 17:52:01 +07:00
"glm-5.3-flash" : { vision : true , videoInput : true , pdf : true , reasoning : true , thinkingFormat : "zai" , contextWindow : 1000000 , maxOutput : 131072 },
"glm-4.6v" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "zai" , contextWindow : 128000 , maxOutput : 32768 },
"glm-4.5v" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "zai" , contextWindow : 64000 , maxOutput : 16384 },
// DeepSeek's first V4 model with image input; text limits match V4-Flash.
"deepseek-v4-flash-vision-exp" : { vision : true , reasoning : true , thinkingFormat : "deepseek" , contextWindow : 1000000 , maxOutput : 384000 },
2026-06-15 11:38:43 +07:00
2026-09-17 17:52:47 +07:00
// DeepSeek V4.1-Flash is natively multimodal — models.dev lists
// opencode-go/deepseek-v4.1-flash with modalities.input ["text","image"] — and upstream
// the retired v4-flash / vision-exp ids route to it, so the live V4.1 ids carry the
// same image capability as the exp id above. "deepseek-flash" is the GA id on the
// DeepSeek API; it previously fell through to the generic *deepseek* pattern, whose
// 128K/64K limits are kept here. The repeated fields are deliberate: an exact entry
// short-circuits the pattern table, so a vision-only delta would drop them.
"deepseek-v4.1-flash" : { vision : true , reasoning : true , thinkingFormat : "deepseek" , contextWindow : 1000000 , maxOutput : 384000 },
"deepseek-flash" : { vision : true , reasoning : true , thinkingFormat : "deepseek" , contextWindow : 128000 , maxOutput : 64000 },
2026-06-15 11:38:43 +07:00
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
2026-06-15 18:18:04 +07:00
"vision-model" : { vision : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 },
"coder-model" : { reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 },
2026-07-17 12:09:14 +07:00
// Kimi flagship + coding (platform + Kimi Code ids) — vision/video native
"kimi-k3" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 1048576 , maxOutput : 131072 },
"k3" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 1048576 , maxOutput : 131072 },
"kimi-for-coding" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 },
"kimi-for-coding-highspeed" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 },
"kimi-k2.7-code" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 },
"kimi-k2.7-code-highspeed" : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 },
2026-09-03 09:23:24 +07:00
// OpenCode Free Muse Spark — multimodal (text+image per models.dev meta/muse-spark)
// via OpenAI Responses input_image; reasoning supports up to xhigh.
"muse-spark-1.2-contributor-free" : { vision : true , reasoning : true , thinkingFormat : "openai" , contextWindow : 1048576 , maxOutput : 131072 },
"muse-spark-1.3-contributor-free" : { vision : true , reasoning : true , thinkingFormat : "openai" , contextWindow : 1048576 , maxOutput : 131072 },
2026-09-17 18:01:33 +07:00
// OpenCode Free Union Alpha — multimodal (text+vision), 262K context, 131K max output
"union-alpha" : { vision : true , contextWindow : 262144 , maxOutput : 131072 },
2026-06-15 11:38:43 +07:00
};
2026-07-16 14:37:08 +07:00
const KIRO_GPT_5_6_CAPABILITIES = { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 272000 , maxOutput : 128000 };
2026-07-20 17:21:41 +07:00
// Codex OAuth (ChatGPT backend) — per-model context window reported by upstream
// (lower than OpenAI API's 1.05M). Sol differs from Terra/Luna. #2720
const CODEX_GPT_56_SOL_CAPS = { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 372000 , maxOutput : 128000 };
const CODEX_GPT_56_DEFAULT_CAPS = { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 272000 , maxOutput : 128000 };
2026-06-15 11:38:43 +07:00
/**
* Provider-specific capability overrides. Keyed by provider alias/id.
*/
2026-06-19 15:33:26 +07:00
export const PROVIDER_CAPABILITIES = {
2026-07-03 12:15:58 +07:00
// NVIDIA NIM is OpenAI-compatible → rejects MiniMax/GLM native `thinking` field.
// Force openai reasoning_effort format for its reasoning models. #issue
"nvidia" : {
"minimaxai/minimax-m2.7" : { reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 200000 , maxOutput : 131072 },
"minimaxai/minimax-m3" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 512000 , maxOutput : 131072 },
"z-ai/glm-5.2" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 128000 },
"deepseek-ai/deepseek-v4-pro" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 1000000 , maxOutput : 65536 },
"deepseek-ai/deepseek-v4-flash" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 1000000 , maxOutput : 65536 },
},
2026-07-20 17:21:41 +07:00
"codex" : {
2026-09-05 22:57:00 +07:00
"gpt-6-astra" : { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 272000 , maxOutput : 128000 },
2026-07-20 17:21:41 +07:00
"gpt-5.6-sol" : CODEX_GPT_56_SOL_CAPS ,
"gpt-5.6-sol-review" : CODEX_GPT_56_SOL_CAPS ,
"gpt-5.6-terra" : CODEX_GPT_56_DEFAULT_CAPS ,
"gpt-5.6-terra-review" : CODEX_GPT_56_DEFAULT_CAPS ,
"gpt-5.6-luna" : CODEX_GPT_56_DEFAULT_CAPS ,
"gpt-5.6-luna-review" : CODEX_GPT_56_DEFAULT_CAPS ,
},
2026-07-16 14:37:08 +07:00
"kiro" : {
"gpt-5.6-sol" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-terra" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-luna" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-sol-thinking" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-terra-thinking" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-luna-thinking" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-sol-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-terra-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-luna-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-sol-thinking-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-terra-thinking-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
"gpt-5.6-luna-thinking-agentic" : KIRO_GPT_5_6_CAPABILITIES ,
},
2026-06-19 15:33:26 +07:00
// 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
2026-09-05 21:02:16 +07:00
// (see registry thinkingFormat). For thinkingCanDisable use the server's
// reasoning.canDisableThinking flag — see the note in the codebuddy-cn block
// below; it is NOT the inverse of onlyReasoning.
2026-06-19 15:33:26 +07:00
"codebuddy-cn" : {
2026-09-05 21:02:16 +07:00
"glm-5.2" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 48000 },
"glm-5.1" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 200000 , maxOutput : 48000 },
2026-06-19 15:33:26 +07:00
"glm-5.0" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 48000 },
2026-09-05 21:02:16 +07:00
// maxOutput 64000 per both the plugin-baked fallback and the live server
// table (the old 38000 had no source and truncated output).
"glm-5v-turbo" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 200000 , maxOutput : 64000 },
2026-06-19 15:33:26 +07:00
"glm-4.7" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 48000 },
2026-09-05 21:02:16 +07:00
"minimax-m3" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 512000 , maxOutput : 128000 },
2026-06-19 15:33:26 +07:00
"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 },
2026-09-05 21:02:16 +07:00
// Per-model values mirror the server's product-config payload (the plugin
// fetches it from copilot.tencent.com; the `models[]` entries carry
// maxInputTokens/maxOutputTokens/supportsImages). contextWindow =
// maxInputTokens, maxOutput = maxOutputTokens. Where the server and the
// plugin-baked fallback disagree, the server table wins.
// ⚠️ thinkingCanDisable maps to the server's reasoning.canDisableThinking —
// it is NOT the inverse of onlyReasoning. onlyReasoning means "thinking is
// on by default"; canDisableThinking means "it CAN be turned off". glm-5.3
// and glm-5.3-flash are onlyReasoning:true BUT canDisableThinking:true, so
// their thinking is switchable; the hy* models are forced always-on.
2026-09-03 09:40:34 +07:00
"hy3" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 192000 , maxOutput : 64000 },
"hy4-preview" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 64000 },
2026-09-05 21:02:16 +07:00
"glm-5.3" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 48000 },
"glm-5.3-flash" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 32000 },
"kimi-k3-1" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 32000 },
"deepseek-v4-pro" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 50000 },
2026-09-10 21:57:22 +07:00
// deepseek-v4.1-flash replaces v4-flash (dropped from the server list;
// the old endpoint still answers 200 but the published list is the
// contract). maxOutput 128000 per the server's product-config payload.
"deepseek-v4.1-flash" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 128000 },
2026-06-19 15:33:26 +07:00
},
2026-09-11 22:03:48 +07:00
// CodeBuddy intl — same gateway catalog as CN, so deepseek-v4.1-flash mirrors
// the codebuddy-cn entry (the openai-style reasoning_effort format matters:
// the generic *deepseek-v4* pattern would otherwise pick the vendor-native
// "deepseek" thinking shape, which the CodeBuddy gateway does not accept).
"codebuddy-intl" : {
"deepseek-v4.1-flash" : { vision : true , reasoning : true , thinkingFormat : "openai" , thinkingCanDisable : true , contextWindow : 1000000 , maxOutput : 128000 },
},
2026-09-03 23:01:53 +07:00
// Qoder — upstream exposes opaque internal ids (dfmodel, kmodel, …); the
// registry `name` is display-only and capability lookup matches on the raw
// id, so every qoder model would fall through to DEFAULT_CAPABILITIES
// (200K) without this map. contextWindow follows the real model family's
// spec: the /algo/api/v2/model/list max_input_tokens under-reports some
// windows (GLM-5.3 / Kimi-K3 / Qwen3.8-Max claim 180K but accept more).
// max_output_tokens arrives as 0 for every model, so outputs are
// best-guess from the real model family. Vision tags below follow the
2026-09-10 22:06:49 +07:00
// upstream is_vl flag. The executor uploads inlined images to
// /api/v2/image/upload and leaves image_urls/chat_context.imageUrls null
// (same as qodercli). reasoning:true on all of them — every model can
2026-09-03 23:01:53 +07:00
// reason; the upstream is_reasoning flag only drives model_config selection.
// thinkingFormat keeps the true-model family for documentation/UI, but
// thinkingCanDisable:false everywhere: the executor only forwards
// messages/tools/max_tokens, and thinking is fixed upstream via
// modelConfig.is_reasoning — client thinking intent is dropped, so "none"
// must never be offered as an option.
"qoder" : {
"ultimate" : { vision : true , reasoning : true , thinkingFormat : "claude-adaptive" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 128000 }, // Claude Opus 5
"performance" : { vision : true , reasoning : true , thinkingFormat : "claude-adaptive" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 128000 }, // Claude Sonnet 5
"dmodel" : { reasoning : true , thinkingFormat : "deepseek" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // DeepSeek-V4-Pro
"dfmodel" : { reasoning : true , thinkingFormat : "deepseek" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // DeepSeek-V4-Flash
"gmodel" : { reasoning : true , thinkingFormat : "zai" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 128000 }, // GLM-5.3
"gfmodel" : { vision : true , reasoning : true , thinkingFormat : "zai" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 128000 }, // GLM-5.3-Flash
"kmodel_latest" : { vision : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // Kimi-K3
"kmodel" : { vision : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 256000 , maxOutput : 65536 }, // Kimi-K2.7-Code
"mmodel" : { reasoning : true , thinkingFormat : "minimax" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 512000 }, // MiniMax-M3
"qmodel_latest" : { vision : true , reasoning : true , thinkingFormat : "qwen" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // Qwen3.7-Max
"qmodel" : { vision : true , reasoning : true , thinkingFormat : "qwen" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // Qwen3.7-Plus
"qfmodel" : { vision : true , reasoning : true , thinkingFormat : "qwen" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // Qwen3.8-Flash
"qmodel_38max" : { vision : true , reasoning : true , thinkingFormat : "qwen" , thinkingCanDisable : false , contextWindow : 1000000 , maxOutput : 65536 }, // Qwen3.8-Max
},
2026-07-30 09:43:55 +07:00
// Poolside Laguna — OpenAI-compatible, all reasoning-capable (32K max output).
2026-07-29 20:04:17 +07:00
"poolside" : {
2026-07-30 09:43:55 +07:00
"laguna-s-2.1" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 1000000 , maxOutput : 32000 },
"laguna-xs-2.1" : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 32000 },
2026-07-29 20:04:17 +07:00
},
2026-09-11 22:03:48 +07:00
// Ollama Cloud — the generic *deepseek-v4* pattern misses the vision badge
// the library page publishes for this model (text+image in, 1M context).
// ponytail: thinkingFormat stays "deepseek" to preserve today's body shape;
// Ollama's native toggle is the top-level `think` field (bool or
// low/medium/high/max), which no format in thinkingUnified.js emits yet —
// openai-to-ollama.js drops it. Wire a "think" format when thinking on
// Ollama Cloud is actually needed.
"ollama" : {
"deepseek-v4.1-flash:cloud" : { vision : true , reasoning : true , thinkingFormat : "deepseek" , contextWindow : 1000000 , maxOutput : 384000 },
},
2026-06-19 15:33:26 +07:00
};
2026-06-15 11:38:43 +07:00
/**
* 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 = [
2026-06-15 18:18:04 +07:00
// ── Claude (4.6+ = adaptive thinking; older/haiku = budget) ──────
2026-07-29 19:32:31 +07:00
{ pattern : "*claude*opus-5*" , caps : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-adaptive" , contextWindow : 1000000 , maxOutput : 128000 } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
{ pattern : "*claude-3*" , caps : { vision : true } },
2026-06-15 18:18:04 +07:00
{ pattern : "*claude*" , caps : { vision : true , reasoning : true , search : true , thinkingFormat : "claude-budget" } },
2026-06-15 11:38:43 +07:00
// ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─
{ pattern : "*gemini*image*" , caps : { vision : true , imageOutput : true , contextWindow : 1048576 } },
2026-09-03 09:12:52 +07:00
{ pattern : "*gemini-3.8*" , caps : { vision : true , audioInput : true , videoInput : true , reasoning : true , search : true , thinkingFormat : "gemini-level" , thinkingCanDisable : false , contextWindow : 1048576 , maxOutput : 65536 } },
2026-08-14 16:26:45 +07:00
{ pattern : "*gemini-3.7*" , caps : { vision : true , audioInput : true , videoInput : true , reasoning : true , search : true , thinkingFormat : "gemini-level" , thinkingCanDisable : false , contextWindow : 1048576 , maxOutput : 65536 } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
{ 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 } },
2026-09-05 22:57:00 +07:00
// ── OpenAI GPT-6.x (vision + thinking + web search) ──────────────
{ pattern : "*gpt-6*" , caps : { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 272000 , maxOutput : 128000 } },
2026-06-15 11:38:43 +07:00
// ── OpenAI GPT-5.x (vision + thinking + web search) ──────────────
{ pattern : "*gpt-5*image*" , caps : { imageOutput : true } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
{ 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 } },
2026-06-15 18:18:04 +07:00
{ pattern : "*gpt-oss*" , caps : { reasoning : true , thinkingFormat : "openai" , contextWindow : 128000 } },
2026-06-15 11:38:43 +07:00
// ── OpenAI o-series (reasoning, vision) ──────────────────────────
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
// ── Grok (vision + Live Search) ──────────────────────────────────
{ pattern : "*grok*image*" , caps : { imageOutput : true } },
2026-06-15 18:18:04 +07:00
{ pattern : "*grok-code*" , caps : { reasoning : true , thinkingFormat : "openai" , contextWindow : 256000 } },
2026-08-27 17:52:01 +07:00
// Grok 4.6: 500k context, no text output limit (docs.x.ai/developers/grok-4-6)
{ pattern : "*grok-4.6*" , caps : { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 500000 , maxOutput : 500000 } },
2026-07-10 11:41:19 +07:00
// Grok 4.5 (Grok CLI / Grok Build): 500k context per cli-chat-proxy /v1/models
{ pattern : "*grok-4.5*" , caps : { vision : true , reasoning : true , search : true , thinkingFormat : "openai" , contextWindow : 500000 , maxOutput : 64000 } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
2026-06-29 15:51:56 +07:00
// ── Qwen (3.5+ = native vision/video; coder & max = text-only; QwQ = thinking-only) ─
2026-06-15 18:18:04 +07:00
{ pattern : "*qwen*vl*" , caps : { vision : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 262144 } },
2026-06-29 15:51:56 +07:00
{ pattern : "*qwen*omni*" , caps : { vision : true , audioInput : true , videoInput : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 262144 , maxOutput : 65536 } },
{ pattern : "*qwen*coder*" , caps : { reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 } },
{ pattern : "*qwen*max*" , caps : { reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 , maxOutput : 65536 } },
{ pattern : "*qwen3.5*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 , maxOutput : 65536 } },
{ pattern : "*qwen3.6*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 , maxOutput : 65536 } },
{ pattern : "*qwen3.7*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "qwen" , contextWindow : 1000000 , maxOutput : 65536 } },
2026-06-15 18:18:04 +07:00
{ 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 : "*qwq*" , caps : { reasoning : true , thinkingFormat : "qwen" , thinkingCanDisable : false , contextWindow : 131072 } },
{ pattern : "*qwen*" , caps : { reasoning : true , thinkingFormat : "qwen" , contextWindow : 262144 } },
2026-06-15 11:38:43 +07:00
2026-06-15 18:18:04 +07:00
// ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ─────
2026-07-17 12:09:14 +07:00
{ pattern : "*kimi*k3*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 1048576 , maxOutput : 131072 } },
{ pattern : "*kimi*for-coding*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 } },
{ pattern : "*kimi*k2.7*code*" , caps : { vision : true , videoInput : true , reasoning : true , thinkingFormat : "kimi" , thinkingCanDisable : false , contextWindow : 262144 , maxOutput : 65536 } },
2026-06-15 18:18:04 +07:00
{ pattern : "*kimi*k2*" , caps : { vision : true , reasoning : true , thinkingFormat : "kimi" , contextWindow : 262144 , maxOutput : 262144 } },
{ pattern : "*kimi*" , caps : { reasoning : true , thinkingFormat : "kimi" , contextWindow : 262144 } },
2026-06-15 11:38:43 +07:00
2026-06-15 18:18:04 +07:00
// ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
2026-08-28 12:32:26 +07:00
// reasoning_effort is only read by z.ai from GLM-5.2 onward (docs.z.ai/guides/capabilities/thinking) —
// older GLM (4.x, 5.0, 5.1, 5-turbo, 5v-turbo) ignore it, so gate it per exact version, not the "*glm-5*" catch-all.
{ pattern : "*glm-5.3*" , caps : { reasoning : true , thinkingFormat : "zai" , thinkingEffortSupported : true , contextWindow : 200000 , maxOutput : 128000 } },
{ pattern : "*glm-5.2*" , caps : { reasoning : true , thinkingFormat : "zai" , thinkingEffortSupported : true , contextWindow : 200000 , maxOutput : 128000 } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
2026-06-15 18:18:04 +07:00
// ── DeepSeek (thinking.enabled + reasoning_effort; r1 = thinking-only) ─
2026-09-17 18:38:10 +07:00
// v4.1+ has real image input (probed live on Alibaba MaaS: correct color
// read from a PNG). v4-pro / v4-flash-0731 accept image blocks but ignore
// them (answered "Unknown"), so vision stays scoped to v4.* dotted releases.
{ pattern : "*deepseek-v4.*" , caps : { vision : true , reasoning : true , thinkingFormat : "deepseek" , thinkingEffortSupported : true , contextWindow : 1000000 , maxOutput : 128000 } },
{ pattern : "*deepseek-v4*" , caps : { reasoning : true , thinkingFormat : "deepseek" , thinkingEffortSupported : true , contextWindow : 1000000 , maxOutput : 384000 } },
2026-06-15 18:18:04 +07:00
{ pattern : "*reasoner*" , caps : { reasoning : true , thinkingFormat : "deepseek" , thinkingCanDisable : false , contextWindow : 128000 } },
{ pattern : "*deepseek-r*" , caps : { reasoning : true , thinkingFormat : "deepseek" , thinkingCanDisable : false , contextWindow : 128000 } },
2026-06-16 23:32:28 +07:00
{ pattern : "*deepseek-chat*" , caps : { contextWindow : 128000 } },
{ pattern : "*deepseek*" , caps : { reasoning : true , thinkingFormat : "deepseek" , contextWindow : 128000 } },
2026-06-15 11:38:43 +07:00
2026-06-15 18:18:04 +07:00
// ── MiniMax (M3 = adaptive; M2.x cannot disable) ─────────────────
2026-06-15 11:38:43 +07:00
{ pattern : "*minimax*image*" , caps : { imageOutput : true } },
2026-06-21 17:50:08 +07:00
{ pattern : "*minimax-m3*" , caps : { vision : true , reasoning : true , thinkingFormat : "minimax" , contextWindow : 1048576 , maxOutput : 512000 } },
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
// ── Xiaomi MiMo (vision, 1M / 262K ctx) ──────────────────────────
2026-08-05 16:09:42 +07:00
{ pattern : "*mimo*v2.5*" , caps : { vision : true , audioInput : true , videoInput : true , contextWindow : 1048576 , maxOutput : 131072 } },
2026-06-15 11:38:43 +07:00
{ 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 } },
2026-07-30 09:43:55 +07:00
// ── Poolside Laguna (resellers: openrouter/nvidia/kilocode/vercel/...) ──
// Free tiers cap S 2.1 well below the paid 1M window → match the free suffix
// (":free" or "-free", depending on reseller) before the plain id.
{ pattern : "*laguna-s-2.1*free*" , caps : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 32000 } },
{ pattern : "*laguna-s-2.1*" , caps : { reasoning : true , thinkingFormat : "openai" , contextWindow : 1000000 , maxOutput : 32000 } },
{ pattern : "*laguna*" , caps : { reasoning : true , thinkingFormat : "openai" , contextWindow : 200000 , maxOutput : 32000 } },
2026-09-03 09:23:24 +07:00
// ── OpenCode Free Muse Spark (multimodal text+image; OpenAI Responses reasoning supports up to xhigh) ─
{ pattern : "*muse*spark*" , caps : { vision : true , reasoning : true , thinkingFormat : "openai" , contextWindow : 1048576 , maxOutput : 131072 } },
2026-06-15 11:38:43 +07:00
// ── Others ───────────────────────────────────────────────────────
2026-06-15 18:18:04 +07:00
{ 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 } },
2026-06-15 11:38:43 +07:00
{ 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
*/
2026-08-27 17:53:53 +07:00
const MODALITY_KEYS = [ "vision" , "pdf" , "audioInput" , "videoInput" ];
// Catalog lookups, installed by the server at startup. Left as no-ops in the
// browser bundle, where there is no file to read.
2026-09-17 17:52:47 +07:00
//
// The server bundles this module into every route chunk that needs it, and each
// copy carries its own module state, so an install landing in the copy the
// startup hook imported stays invisible to the copy resolving requests. The slot
// lives on globalThis instead; the local binding is the fast path.
2026-08-27 17:53:53 +07:00
let catalogSource = null ;
/**
* Install the synced catalog reader (server only).
2026-09-17 17:52:47 +07:00
* @param {{ getModalities: (provider: string, model: string) => object|null,
* getLimits: (provider: string, model: string) => object|null } | null} source
2026-08-27 17:53:53 +07:00
*/
export function setCatalogSource ( source ) {
catalogSource = source ;
2026-09-17 17:52:47 +07:00
if ( typeof globalThis !== "undefined" ) globalThis . __9rCatalogSource = source ;
}
function getCatalogSource () {
if ( catalogSource ) return catalogSource ;
if ( typeof globalThis === "undefined" ) return null ;
return ( catalogSource = globalThis . __9rCatalogSource || null );
2026-08-27 17:53:53 +07:00
}
// Apply the synced catalog + name heuristic on top of a table-resolved result.
// Strictly additive: a capability already true stays true, and a false one only
// flips when an outside source positively declares support.
function refine ( base , provider , model ) {
const result = { ... DEFAULT_CAPABILITIES , ... base };
2026-09-17 17:52:47 +07:00
const source = getCatalogSource ();
if ( source ) {
const modalities = source . getModalities ( provider , model );
2026-08-27 17:53:53 +07:00
if ( modalities ) {
for ( const key of MODALITY_KEYS ) {
if ( modalities [ key ] === true ) result [ key ] = true ;
}
}
2026-09-17 17:52:47 +07:00
const limits = source . getLimits ( provider , model );
2026-08-27 17:53:53 +07:00
if ( limits ) {
if ( limits . contextWindow > 0 ) result . contextWindow = limits . contextWindow ;
if ( limits . maxOutput > 0 ) result . maxOutput = limits . maxOutput ;
}
}
if ( ! result . vision && looksLikeVisionModel ( model )) result . vision = true ;
return result ;
}
2026-09-16 20:16:32 +07:00
// Mirrors Command Code CLI `isKnownTextOnlyModel` (no image input). New models
// default to vision; only this denylist stays text-only.
const COMMANDCODE_TEXT_ONLY = new Set ([
"deepseek/deepseek-v4-pro" ,
"deepseek/deepseek-v4-flash" ,
"deepseek/deepseek-v4-flash-fast" ,
"zai-org/glm-5.3" ,
"zai-org/glm-5.2" ,
"zai-org/glm-5.2-fast" ,
"zai-org/glm-5.1" ,
"zai-org/glm-5" ,
"minimaxai/minimax-m2.7" ,
"minimax/minimax-m2.7-free" ,
"minimaxai/minimax-m2.5" ,
"xiaomi/mimo-v2.5-pro" ,
"qwen/qwen3.6-max-preview" ,
"qwen/qwen3.7-max" ,
"meituan/longcat-2.0:free" ,
"stepfun/step-3.5-flash" ,
"tencent/hy4-preview" ,
"tencent/hy3" ,
"tencent/hy3-paid" ,
"nvidia/nemotron-3-ultra-550b-a55b" ,
"poolside/laguna-s-2.1-free" ,
"inclusionai/ling-3.0-flash-free" ,
"inclusionai/ling-3.0-flash-sante:free" ,
]);
function isCommandCodeTextOnly ( model ) {
const key = String ( model || "" ). toLowerCase ();
if ( COMMANDCODE_TEXT_ONLY . has ( key )) return true ;
for ( const id of COMMANDCODE_TEXT_ONLY ) {
const base = id . includes ( "/" ) ? id . slice ( id . lastIndexOf ( "/" ) + 1 ) : id ;
if ( key === base || key . endsWith ( "/" + base )) return true ;
}
return false ;
}
2026-06-15 11:38:43 +07:00
export function getCapabilitiesForModel ( provider , model ) {
if ( ! model ) return { ... DEFAULT_CAPABILITIES };
2026-07-16 14:37:08 +07:00
// Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7".
const baseModel = model . includes ( "/" ) ? model . split ( "/" ). pop () : model ;
2026-09-16 20:16:32 +07:00
// CommandCode wire is /alpha/generate for every model. Family patterns
// (deepseek-v4 → thinkingFormat:deepseek, vision:false) must not win here.
if ( provider === "commandcode" || provider === "cmc" ) {
const providerCaps = PROVIDER_CAPABILITIES . commandcode ;
if ( providerCaps ? .[ model ]) return { ... DEFAULT_CAPABILITIES , ... providerCaps [ model ] };
if ( providerCaps ? .[ baseModel ]) return { ... DEFAULT_CAPABILITIES , ... providerCaps [ baseModel ] };
return {
... DEFAULT_CAPABILITIES ,
reasoning : true ,
thinkingFormat : "commandcode" ,
thinkingEffortSupported : true ,
vision : ! isCommandCodeTextOnly ( model ),
contextWindow : 1000000 ,
maxOutput : 384000 ,
};
}
2026-06-15 11:38:43 +07:00
// 1. Provider-specific override
2026-07-16 14:37:08 +07:00
if ( provider ) {
const providerCaps = PROVIDER_CAPABILITIES [ provider ];
if ( providerCaps ? .[ model ]) return { ... DEFAULT_CAPABILITIES , ... providerCaps [ model ] };
if ( providerCaps ? .[ baseModel ]) return { ... DEFAULT_CAPABILITIES , ... providerCaps [ baseModel ] };
2026-06-15 11:38:43 +07:00
}
2026-07-16 14:37:08 +07:00
// 2. Canonical exact
2026-06-15 11:38:43 +07:00
if ( MODEL_CAPABILITIES [ baseModel ]) return { ... DEFAULT_CAPABILITIES , ... MODEL_CAPABILITIES [ baseModel ] };
if ( MODEL_CAPABILITIES [ model ]) return { ... DEFAULT_CAPABILITIES , ... MODEL_CAPABILITIES [ model ] };
2026-08-27 17:53:53 +07:00
// 3. Pattern match (first match wins), refined by catalog + name heuristic
2026-06-15 11:38:43 +07:00
for ( const { pattern , caps } of PATTERN_CAPABILITIES ) {
if ( matchPattern ( pattern , baseModel ) || matchPattern ( pattern , model )) {
2026-08-27 17:53:53 +07:00
return refine ( caps , provider , model );
2026-06-15 11:38:43 +07:00
}
}
// 4. Floor
2026-08-27 17:53:53 +07:00
return refine ( null , provider , model );
2026-06-15 11:38:43 +07:00
}