Skip to main content

vtcode_config/models/model_id/
capabilities.rs

1use crate::models::Provider;
2
3use super::ModelId;
4
5#[cfg(not(docsrs))]
6#[allow(dead_code)]
7mod capability_generated {
8    include!(concat!(env!("OUT_DIR"), "/model_capabilities.rs"));
9}
10
11#[cfg(docsrs)]
12#[allow(dead_code)]
13mod capability_generated {
14    #[derive(Clone, Copy)]
15    pub struct Pricing {
16        pub input: Option<f64>,
17        pub output: Option<f64>,
18        pub cache_read: Option<f64>,
19        pub cache_write: Option<f64>,
20    }
21
22    #[derive(Clone, Copy)]
23    pub struct Entry {
24        pub provider: &'static str,
25        pub id: &'static str,
26        pub display_name: &'static str,
27        pub description: &'static str,
28        pub context_window: usize,
29        pub max_output_tokens: Option<usize>,
30        pub reasoning: bool,
31        pub tool_call: bool,
32        pub vision: bool,
33        pub input_modalities: &'static [&'static str],
34        pub caching: bool,
35        pub structured_output: bool,
36        pub pricing: Pricing,
37    }
38
39    pub const ENTRIES: &[Entry] = &[];
40    pub const PROVIDERS: &[&str] = &[];
41
42    pub fn metadata_for(_provider: &str, _id: &str) -> Option<Entry> {
43        None
44    }
45
46    pub fn models_for_provider(_provider: &str) -> Option<&'static [&'static str]> {
47        None
48    }
49}
50
51/// Catalog metadata generated from `docs/models.json`.
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct ModelPricing {
54    pub input: Option<f64>,
55    pub output: Option<f64>,
56    pub cache_read: Option<f64>,
57    pub cache_write: Option<f64>,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct ModelCatalogEntry {
62    pub provider: &'static str,
63    pub id: &'static str,
64    pub display_name: &'static str,
65    pub description: &'static str,
66    pub context_window: usize,
67    pub max_output_tokens: Option<usize>,
68    pub reasoning: bool,
69    pub tool_call: bool,
70    pub vision: bool,
71    pub input_modalities: &'static [&'static str],
72    pub caching: bool,
73    pub structured_output: bool,
74    pub pricing: ModelPricing,
75}
76
77fn catalog_provider_key(provider: &str) -> &str {
78    if provider.eq_ignore_ascii_case("google") || provider.eq_ignore_ascii_case("gemini") {
79        "gemini"
80    } else if provider.eq_ignore_ascii_case("openai") {
81        "openai"
82    } else if provider.eq_ignore_ascii_case("anthropic") {
83        "anthropic"
84    } else if provider.eq_ignore_ascii_case("deepseek") {
85        "deepseek"
86    } else if provider.eq_ignore_ascii_case("openrouter") {
87        "openrouter"
88    } else if provider.eq_ignore_ascii_case("ollama") {
89        "ollama"
90    } else if provider.eq_ignore_ascii_case("lmstudio") {
91        "lmstudio"
92    } else if provider.eq_ignore_ascii_case("llamacpp")
93        || provider.eq_ignore_ascii_case("llama.cpp")
94    {
95        "llamacpp"
96    } else if provider.eq_ignore_ascii_case("moonshot") {
97        "moonshot"
98    } else if provider.eq_ignore_ascii_case("zai") {
99        "zai"
100    } else if provider.eq_ignore_ascii_case("minimax") {
101        "minimax"
102    } else if provider.eq_ignore_ascii_case("huggingface") {
103        "huggingface"
104    } else if provider.eq_ignore_ascii_case("stepfun") {
105        "stepfun"
106    } else if provider.eq_ignore_ascii_case("poolside") {
107        "poolside"
108    } else {
109        provider
110    }
111}
112
113fn capability_provider_key(provider: Provider) -> &'static str {
114    match provider {
115        Provider::Gemini => "gemini",
116        Provider::OpenAI => "openai",
117        Provider::Anthropic => "anthropic",
118        Provider::Copilot => "copilot",
119        Provider::DeepSeek => "deepseek",
120        Provider::OpenRouter => "openrouter",
121        Provider::Ollama => "ollama",
122        Provider::LmStudio => "lmstudio",
123        Provider::LlamaCpp => "llamacpp",
124        Provider::Moonshot => "moonshot",
125        Provider::ZAI => "zai",
126        Provider::Minimax => "minimax",
127        Provider::MiMo => "mimo",
128        Provider::Mistral => "mistral",
129        Provider::HuggingFace => "huggingface",
130        Provider::OpenCodeZen => "opencode-zen",
131        Provider::OpenCodeGo => "opencode-go",
132        Provider::Qwen => "qwen",
133        Provider::StepFun => "stepfun",
134        Provider::Poolside => "poolside",
135    }
136}
137
138fn generated_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
139    capability_generated::metadata_for(catalog_provider_key(provider), id).map(|entry| {
140        ModelCatalogEntry {
141            provider: entry.provider,
142            id: entry.id,
143            display_name: entry.display_name,
144            description: entry.description,
145            context_window: entry.context_window,
146            max_output_tokens: entry.max_output_tokens,
147            reasoning: entry.reasoning,
148            tool_call: entry.tool_call,
149            vision: entry.vision,
150            input_modalities: entry.input_modalities,
151            caching: entry.caching,
152            structured_output: entry.structured_output,
153            pricing: ModelPricing {
154                input: entry.pricing.input,
155                output: entry.pricing.output,
156                cache_read: entry.pricing.cache_read,
157                cache_write: entry.pricing.cache_write,
158            },
159        }
160    })
161}
162
163pub fn model_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
164    generated_catalog_entry(provider, id)
165}
166
167pub fn supported_models_for_provider(provider: &str) -> Option<&'static [&'static str]> {
168    capability_generated::models_for_provider(catalog_provider_key(provider))
169}
170
171pub fn catalog_provider_keys() -> &'static [&'static str] {
172    capability_generated::PROVIDERS
173}
174
175impl ModelId {
176    fn generated_capabilities(&self) -> Option<ModelCatalogEntry> {
177        generated_catalog_entry(capability_provider_key(self.provider()), self.as_str())
178    }
179
180    /// Preferred built-in lightweight sibling or lower-tier fallback for this model.
181    pub fn preferred_lightweight_variant(&self) -> Option<Self> {
182        match self {
183            ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => {
184                Some(ModelId::Gemini31FlashLitePreview)
185            }
186            ModelId::GPT55 | ModelId::GPT54 | ModelId::GPT54Pro => Some(ModelId::GPT54Mini),
187            ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
188            ModelId::GPT53Codex => Some(ModelId::GPT54Mini),
189            ModelId::ClaudeOpus48 | ModelId::ClaudeSonnet46 | ModelId::ClaudeMythosPreview => {
190                Some(ModelId::ClaudeHaiku45)
191            }
192            ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
193            ModelId::CopilotGPT52Codex | ModelId::CopilotGPT51CodexMax => {
194                Some(ModelId::CopilotGPT54Mini)
195            }
196            ModelId::DeepSeekV4Pro => Some(ModelId::DeepSeekV4Flash),
197            ModelId::HuggingFaceDeepseekV4ProTogether => {
198                Some(ModelId::HuggingFaceDeepseekV4FlashNovita)
199            }
200            ModelId::OllamaDeepseekV4ProCloud => Some(ModelId::OllamaDeepseekV4FlashCloud),
201            ModelId::ZaiGlm51 => Some(ModelId::ZaiGlm5),
202            ModelId::MinimaxM27 => Some(ModelId::MinimaxM25),
203            ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
204            ModelId::StepFun37Flash => None,
205            ModelId::PoolsideLagunaM1 => Some(ModelId::PoolsideLagunaXs2),
206            _ => None,
207        }
208    }
209
210    /// Attempt to find a non-reasoning variant for this model.
211    pub fn non_reasoning_variant(&self) -> Option<Self> {
212        if let Some(meta) = self.openrouter_metadata() {
213            if !meta.reasoning {
214                return None;
215            }
216
217            let vendor = meta.vendor;
218            let mut candidates: Vec<Self> = Self::openrouter_vendor_groups()
219                .into_iter()
220                .find(|(candidate_vendor, _)| *candidate_vendor == vendor)
221                .map(|(_, models)| {
222                    models
223                        .iter()
224                        .copied()
225                        .filter(|candidate| candidate != self)
226                        .filter(|candidate| {
227                            candidate
228                                .openrouter_metadata()
229                                .map(|other| !other.reasoning)
230                                .unwrap_or(false)
231                        })
232                        .collect()
233                })
234                .unwrap_or_default();
235
236            if candidates.is_empty() {
237                return None;
238            }
239
240            candidates.sort_by_key(|candidate| {
241                candidate
242                    .openrouter_metadata()
243                    .map(|data| (!data.efficient, data.display))
244                    .unwrap_or((true, ""))
245            });
246
247            return candidates.into_iter().next();
248        }
249
250        let direct = match self {
251            ModelId::Gemini31ProPreview
252            | ModelId::Gemini31ProPreviewCustomTools
253            | ModelId::Gemini31FlashLitePreview => Some(ModelId::Gemini35Flash),
254            ModelId::GPT55
255            | ModelId::GPT54
256            | ModelId::GPT54Pro
257            | ModelId::GPT54Nano
258            | ModelId::GPT54Mini => Some(ModelId::GPT54Mini),
259            ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
260            ModelId::CopilotGPT52Codex | ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
261            ModelId::DeepSeekV4Pro => Some(ModelId::DeepSeekV4Flash),
262            ModelId::HuggingFaceDeepseekV4ProTogether => {
263                Some(ModelId::HuggingFaceDeepseekV4FlashNovita)
264            }
265            ModelId::OllamaDeepseekV4ProCloud => Some(ModelId::OllamaDeepseekV4FlashCloud),
266            ModelId::ZaiGlm5 | ModelId::ZaiGlm51 => Some(ModelId::OllamaGlm5Cloud),
267            ModelId::ClaudeOpus48 | ModelId::ClaudeSonnet46 | ModelId::ClaudeMythosPreview => {
268                Some(ModelId::ClaudeSonnet46)
269            }
270            ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
271            ModelId::MinimaxM27 | ModelId::MinimaxM25 => None,
272            _ => None,
273        };
274
275        direct.and_then(|candidate| {
276            if candidate.supports_reasoning_effort() {
277                None
278            } else {
279                Some(candidate)
280            }
281        })
282    }
283
284    /// Check if this is a "flash" variant (optimized for speed)
285    pub fn is_flash_variant(&self) -> bool {
286        matches!(
287            self,
288            ModelId::Gemini31FlashLitePreview
289                | ModelId::Gemini35Flash
290                | ModelId::OpenRouterStepfunStep35FlashFree
291                | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
292                | ModelId::HuggingFaceStep35Flash
293                | ModelId::StepFun37Flash
294                | ModelId::HuggingFaceDeepseekV4FlashNovita
295        )
296    }
297
298    /// Check if this is a "pro" variant (optimized for capability)
299    pub fn is_pro_variant(&self) -> bool {
300        matches!(
301            self,
302            ModelId::Gemini31ProPreview
303                | ModelId::Gemini31ProPreviewCustomTools
304                | ModelId::OpenRouterGoogleGemini31ProPreview
305                | ModelId::GPT55
306                | ModelId::GPT54
307                | ModelId::GPT54Pro
308                | ModelId::GPT53Codex
309                | ModelId::CopilotGPT52Codex
310                | ModelId::CopilotGPT51CodexMax
311                | ModelId::CopilotGPT54
312                | ModelId::CopilotClaudeSonnet46
313                | ModelId::ClaudeOpus48
314                | ModelId::ClaudeSonnet46
315                | ModelId::ClaudeMythosPreview
316                | ModelId::OpenCodeZenGPT54
317                | ModelId::OpenCodeZenClaudeSonnet46
318                | ModelId::OpenCodeZenGlm51
319                | ModelId::OpenCodeGoGlm51
320                | ModelId::OpenCodeGoMinimaxM27
321                | ModelId::DeepSeekV4Pro
322                | ModelId::ZaiGlm5
323                | ModelId::ZaiGlm51
324                | ModelId::OpenRouterStepfunStep35FlashFree
325                | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
326                | ModelId::MinimaxM27
327                | ModelId::MinimaxM25
328                | ModelId::OpenCodeGoMinimaxM25
329                | ModelId::OllamaGlm5Cloud
330                | ModelId::OllamaGlm51Cloud
331                | ModelId::OllamaNemotron3SuperCloud
332                | ModelId::OllamaMinimaxM25Cloud
333                | ModelId::HuggingFaceQwen3CoderNextNovita
334                | ModelId::HuggingFaceQwen35397BA17BTogether
335                | ModelId::HuggingFaceDeepseekV4ProTogether
336                | ModelId::OpenRouterMoonshotaiKimiK26
337                | ModelId::PoolsideLagunaM1
338        )
339    }
340
341    /// Check if this is an optimized/efficient variant
342    pub fn is_efficient_variant(&self) -> bool {
343        if let Some(meta) = self.openrouter_metadata() {
344            return meta.efficient;
345        }
346        matches!(
347            self,
348            ModelId::Gemini31FlashLitePreview
349                | ModelId::Gemini35Flash
350                | ModelId::GPT54Mini
351                | ModelId::CopilotGPT54Mini
352                | ModelId::ClaudeHaiku45
353                | ModelId::OpenCodeZenGPT54Mini
354                | ModelId::OpenCodeGoMinimaxM25
355                | ModelId::DeepSeekV4Flash
356                | ModelId::HuggingFaceStep35Flash
357                | ModelId::HuggingFaceDeepseekV4FlashNovita
358                | ModelId::PoolsideLagunaXs2
359        )
360    }
361
362    /// Check if this is a top-tier model
363    pub fn is_top_tier(&self) -> bool {
364        if let Some(meta) = self.openrouter_metadata() {
365            return meta.top_tier;
366        }
367        matches!(
368            self,
369            ModelId::Gemini31ProPreview
370                | ModelId::Gemini31ProPreviewCustomTools
371                | ModelId::OpenRouterGoogleGemini31ProPreview
372                | ModelId::Gemini31FlashLitePreview
373                | ModelId::Gemini35Flash
374                | ModelId::GPT55
375                | ModelId::GPT54
376                | ModelId::GPT54Pro
377                | ModelId::GPT53Codex
378                | ModelId::ClaudeOpus48
379                | ModelId::ClaudeSonnet46
380                | ModelId::ClaudeMythosPreview
381                | ModelId::OpenCodeZenGPT54
382                | ModelId::OpenCodeZenClaudeSonnet46
383                | ModelId::OpenCodeZenGlm51
384                | ModelId::OpenCodeGoGlm51
385                | ModelId::OpenCodeGoMinimaxM27
386                | ModelId::DeepSeekV4Pro
387                | ModelId::ZaiGlm5
388                | ModelId::ZaiGlm51
389                | ModelId::OpenRouterStepfunStep35FlashFree
390                | ModelId::HuggingFaceQwen3CoderNextNovita
391                | ModelId::HuggingFaceQwen35397BA17BTogether
392                | ModelId::HuggingFaceDeepseekV4FlashNovita
393                | ModelId::HuggingFaceDeepseekV4ProTogether
394                | ModelId::OpenRouterMoonshotaiKimiK26
395                | ModelId::PoolsideLagunaM1
396        )
397    }
398
399    /// Determine whether the model is a reasoning-capable variant
400    pub fn is_reasoning_variant(&self) -> bool {
401        if let Some(meta) = self.openrouter_metadata() {
402            return meta.reasoning;
403        }
404        self.provider().supports_reasoning_effort(self.as_str())
405    }
406
407    /// Determine whether the model supports tool calls/function execution
408    pub fn supports_tool_calls(&self) -> bool {
409        if let Some(meta) = self.generated_capabilities() {
410            return meta.tool_call;
411        }
412        if let Some(meta) = self.openrouter_metadata() {
413            return meta.tool_call;
414        }
415        true
416    }
417
418    /// Ordered list of supported input modalities when VT Code has metadata for this model.
419    pub fn input_modalities(&self) -> &'static [&'static str] {
420        self.generated_capabilities()
421            .map(|meta| meta.input_modalities)
422            .unwrap_or(&[])
423    }
424
425    /// Get the generation/version string for this model
426    pub fn generation(&self) -> &'static str {
427        if let Some(meta) = self.openrouter_metadata() {
428            return meta.generation;
429        }
430        match self {
431            // Gemini generations
432            ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => "3.1",
433            ModelId::Gemini31FlashLitePreview => "3.1-lite",
434            // OpenAI generations
435            ModelId::GPT55 => "5.5",
436            ModelId::GPT54 | ModelId::GPT54Pro | ModelId::GPT54Nano | ModelId::GPT54Mini => "5.4",
437            ModelId::GPT53Codex => "5.3",
438            ModelId::OpenAIGptOss20b | ModelId::OpenAIGptOss120b => "5",
439            // Anthropic generations
440            ModelId::ClaudeOpus48 => "4.8",
441            ModelId::ClaudeSonnet46 => "4.6",
442            ModelId::ClaudeHaiku45 => "4.5",
443            ModelId::ClaudeMythosPreview => "preview",
444            // DeepSeek generations
445            ModelId::DeepSeekV4Pro | ModelId::DeepSeekV4Flash => "4",
446            // Z.AI generations
447            ModelId::ZaiGlm5 => "5",
448            ModelId::ZaiGlm51 => "5.1",
449            ModelId::Gemini35Flash => "3.5",
450            ModelId::OpenCodeZenGPT54 | ModelId::OpenCodeZenGPT54Mini => "5.4",
451            ModelId::OpenCodeZenClaudeSonnet46 => "4.6",
452            ModelId::OpenCodeZenGlm51 | ModelId::OpenCodeGoGlm51 => "5.1",
453            ModelId::OpenCodeGoMinimaxM25 => "m2.5",
454            ModelId::OpenCodeGoMinimaxM27 => "m2.7",
455            ModelId::OllamaGptOss20b => "oss",
456            ModelId::OllamaGptOss20bCloud => "oss-cloud",
457            ModelId::OllamaGptOss120bCloud => "oss-cloud",
458            ModelId::OllamaQwen317b => "oss",
459            ModelId::OllamaQwen3CoderNext => "qwen3-coder-next:cloud",
460            ModelId::OllamaDeepseekV4FlashCloud => "deepseek-v4-flash",
461            ModelId::OllamaDeepseekV4ProCloud => "deepseek-v4-pro",
462            ModelId::OllamaQwen3Next80bCloud => "qwen3-next",
463            ModelId::OllamaMinimaxM2Cloud => "minimax-m2",
464            ModelId::OllamaMinimaxM27Cloud => "minimax-m2.7",
465            ModelId::OllamaGlm5Cloud => "glm-5",
466            ModelId::OllamaGlm51Cloud => "glm-5.1",
467            ModelId::OllamaMinimaxM25Cloud => "minimax-m2.5",
468            ModelId::OllamaKimiK26Cloud => "kimi-k2.6",
469            ModelId::OllamaNemotron3SuperCloud => "nemotron-3",
470            ModelId::OllamaLagunaXs2 => "laguna-xs.2",
471            ModelId::LlamaCppQwen3627b => "3.6",
472            ModelId::LlamaCppQwen3635bA3b => "3.6",
473            ModelId::LlamaCppGemma426bA4b => "4",
474            ModelId::LlamaCppGemma4E4b => "4",
475            ModelId::LlamaCppGptOss20b => "oss",
476            ModelId::LlamaCppStep35Flash => "3.5",
477            // MiniMax models
478            ModelId::MinimaxM27 => "M2.7",
479            ModelId::MinimaxM25 => "M2.5",
480            // Moonshot models
481            ModelId::MoonshotKimiK26 => "k2.6",
482            // Hugging Face generations
483            ModelId::HuggingFaceOpenAIGptOss20b => "oss",
484            ModelId::HuggingFaceOpenAIGptOss120b => "oss",
485            ModelId::HuggingFaceMinimaxM25Novita => "m2.5",
486            ModelId::HuggingFaceGlm5Novita => "5",
487            ModelId::HuggingFaceGlm51ZaiOrg => "5.1",
488            ModelId::HuggingFaceKimiK26Novita => "k2.6",
489            ModelId::HuggingFaceDeepseekV4FlashNovita => "v4-flash",
490            ModelId::HuggingFaceDeepseekV4ProTogether => "v4-pro",
491            ModelId::HuggingFaceStep35Flash => "3.5",
492            ModelId::HuggingFaceQwen3CoderNextNovita | ModelId::OpenRouterQwen3CoderNext => {
493                "qwen3-coder-next"
494            }
495            // Poolside models
496            ModelId::PoolsideLagunaM1 => "laguna-m.1",
497            ModelId::PoolsideLagunaXs2 => "laguna-xs.2",
498            // Qwen models
499            ModelId::Qwen37Max => "3.7",
500            ModelId::Qwen36Flash | ModelId::Qwen36Plus => "3.6",
501            ModelId::QwenDeepSeekV4Flash | ModelId::QwenDeepSeekV4Pro => "v4",
502            ModelId::QwenGlm51 => "5.1",
503            _ => "unknown",
504        }
505    }
506
507    /// Determine if this model supports GPT-5.1+/5.2+/5.3+ shell tool type
508    pub fn supports_shell_tool(&self) -> bool {
509        matches!(
510            self,
511            ModelId::GPT55 | ModelId::GPT54 | ModelId::GPT54Pro | ModelId::GPT53Codex
512        )
513    }
514
515    /// Determine if this model supports optimized apply_patch tool
516    pub fn supports_apply_patch_tool(&self) -> bool {
517        false // Placeholder for future optimization
518    }
519}