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("moonshot") {
93        "moonshot"
94    } else if provider.eq_ignore_ascii_case("zai") {
95        "zai"
96    } else if provider.eq_ignore_ascii_case("minimax") {
97        "minimax"
98    } else if provider.eq_ignore_ascii_case("huggingface") {
99        "huggingface"
100    } else {
101        provider
102    }
103}
104
105fn capability_provider_key(provider: Provider) -> &'static str {
106    match provider {
107        Provider::Gemini => "gemini",
108        Provider::OpenAI => "openai",
109        Provider::Anthropic => "anthropic",
110        Provider::Copilot => "copilot",
111        Provider::DeepSeek => "deepseek",
112        Provider::OpenRouter => "openrouter",
113        Provider::Ollama => "ollama",
114        Provider::LmStudio => "lmstudio",
115        Provider::Moonshot => "moonshot",
116        Provider::ZAI => "zai",
117        Provider::Minimax => "minimax",
118        Provider::Mistral => "mistral",
119        Provider::HuggingFace => "huggingface",
120        Provider::OpenCodeZen => "opencode-zen",
121        Provider::OpenCodeGo => "opencode-go",
122    }
123}
124
125fn generated_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
126    capability_generated::metadata_for(catalog_provider_key(provider), id).map(|entry| {
127        ModelCatalogEntry {
128            provider: entry.provider,
129            id: entry.id,
130            display_name: entry.display_name,
131            description: entry.description,
132            context_window: entry.context_window,
133            max_output_tokens: entry.max_output_tokens,
134            reasoning: entry.reasoning,
135            tool_call: entry.tool_call,
136            vision: entry.vision,
137            input_modalities: entry.input_modalities,
138            caching: entry.caching,
139            structured_output: entry.structured_output,
140            pricing: ModelPricing {
141                input: entry.pricing.input,
142                output: entry.pricing.output,
143                cache_read: entry.pricing.cache_read,
144                cache_write: entry.pricing.cache_write,
145            },
146        }
147    })
148}
149
150pub fn model_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
151    generated_catalog_entry(provider, id)
152}
153
154pub fn supported_models_for_provider(provider: &str) -> Option<&'static [&'static str]> {
155    capability_generated::models_for_provider(catalog_provider_key(provider))
156}
157
158pub fn catalog_provider_keys() -> &'static [&'static str] {
159    capability_generated::PROVIDERS
160}
161
162impl ModelId {
163    fn generated_capabilities(&self) -> Option<ModelCatalogEntry> {
164        generated_catalog_entry(capability_provider_key(self.provider()), self.as_str())
165    }
166
167    /// Preferred built-in lightweight sibling or lower-tier fallback for this model.
168    pub fn preferred_lightweight_variant(&self) -> Option<Self> {
169        match self {
170            ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => {
171                Some(ModelId::Gemini31FlashLitePreview)
172            }
173            ModelId::GPT55 | ModelId::GPT54 | ModelId::GPT54Pro => Some(ModelId::GPT54Mini),
174            ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
175            ModelId::GPT52
176            | ModelId::GPT52Codex
177            | ModelId::GPT53Codex
178            | ModelId::GPT51Codex
179            | ModelId::GPT51CodexMax
180            | ModelId::GPT5
181            | ModelId::GPT5Codex => Some(ModelId::GPT5Mini),
182            ModelId::ClaudeOpus47
183            | ModelId::ClaudeOpus46
184            | ModelId::ClaudeSonnet46
185            | ModelId::ClaudeMythosPreview => Some(ModelId::ClaudeHaiku45),
186            ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
187            ModelId::CopilotGPT52Codex | ModelId::CopilotGPT51CodexMax => {
188                Some(ModelId::CopilotGPT54Mini)
189            }
190            ModelId::DeepSeekV4Pro => Some(ModelId::DeepSeekV4Flash),
191            ModelId::HuggingFaceDeepseekV4ProTogether => {
192                Some(ModelId::HuggingFaceDeepseekV4FlashNovita)
193            }
194            ModelId::OllamaDeepseekV4ProCloud => Some(ModelId::OllamaDeepseekV4FlashCloud),
195            ModelId::ZaiGlm51 => Some(ModelId::ZaiGlm5),
196            ModelId::MinimaxM27 => Some(ModelId::MinimaxM25),
197            ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
198            _ => None,
199        }
200    }
201
202    /// Attempt to find a non-reasoning variant for this model.
203    pub fn non_reasoning_variant(&self) -> Option<Self> {
204        if let Some(meta) = self.openrouter_metadata() {
205            if !meta.reasoning {
206                return None;
207            }
208
209            let vendor = meta.vendor;
210            let mut candidates: Vec<Self> = Self::openrouter_vendor_groups()
211                .into_iter()
212                .find(|(candidate_vendor, _)| *candidate_vendor == vendor)
213                .map(|(_, models)| {
214                    models
215                        .iter()
216                        .copied()
217                        .filter(|candidate| candidate != self)
218                        .filter(|candidate| {
219                            candidate
220                                .openrouter_metadata()
221                                .map(|other| !other.reasoning)
222                                .unwrap_or(false)
223                        })
224                        .collect()
225                })
226                .unwrap_or_default();
227
228            if candidates.is_empty() {
229                return None;
230            }
231
232            candidates.sort_by_key(|candidate| {
233                candidate
234                    .openrouter_metadata()
235                    .map(|data| (!data.efficient, data.display))
236                    .unwrap_or((true, ""))
237            });
238
239            return candidates.into_iter().next();
240        }
241
242        let direct = match self {
243            ModelId::Gemini31ProPreview
244            | ModelId::Gemini31ProPreviewCustomTools
245            | ModelId::Gemini31FlashLitePreview => Some(ModelId::Gemini3FlashPreview),
246            ModelId::GPT55
247            | ModelId::GPT52
248            | ModelId::GPT54
249            | ModelId::GPT54Pro
250            | ModelId::GPT54Nano
251            | ModelId::GPT54Mini
252            | ModelId::GPT5 => Some(ModelId::GPT5Mini),
253            ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
254            ModelId::CopilotGPT52Codex | ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
255            ModelId::DeepSeekV4Pro => Some(ModelId::DeepSeekV4Flash),
256            ModelId::HuggingFaceDeepseekV4ProTogether => {
257                Some(ModelId::HuggingFaceDeepseekV4FlashNovita)
258            }
259            ModelId::OllamaDeepseekV4ProCloud => Some(ModelId::OllamaDeepseekV4FlashCloud),
260            ModelId::ZaiGlm5 | ModelId::ZaiGlm51 => Some(ModelId::OllamaGlm5Cloud),
261            ModelId::ClaudeOpus47
262            | ModelId::ClaudeOpus46
263            | ModelId::ClaudeSonnet46
264            | ModelId::ClaudeMythosPreview => Some(ModelId::ClaudeSonnet46),
265            ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
266            ModelId::MinimaxM27 | ModelId::MinimaxM25 => None,
267            _ => None,
268        };
269
270        direct.and_then(|candidate| {
271            if candidate.supports_reasoning_effort() {
272                None
273            } else {
274                Some(candidate)
275            }
276        })
277    }
278
279    /// Check if this is a "flash" variant (optimized for speed)
280    pub fn is_flash_variant(&self) -> bool {
281        matches!(
282            self,
283            ModelId::Gemini3FlashPreview
284                | ModelId::Gemini31FlashLitePreview
285                | ModelId::OpenRouterStepfunStep35FlashFree
286                | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
287                | ModelId::OllamaGemini3FlashPreviewCloud
288                | ModelId::HuggingFaceStep35Flash
289                | ModelId::HuggingFaceDeepseekV4FlashNovita
290        )
291    }
292
293    /// Check if this is a "pro" variant (optimized for capability)
294    pub fn is_pro_variant(&self) -> bool {
295        matches!(
296            self,
297            ModelId::Gemini31ProPreview
298                | ModelId::Gemini31ProPreviewCustomTools
299                | ModelId::OpenRouterGoogleGemini31ProPreview
300                | ModelId::GPT55
301                | ModelId::GPT5
302                | ModelId::GPT52
303                | ModelId::GPT52Codex
304                | ModelId::GPT54
305                | ModelId::GPT54Pro
306                | ModelId::GPT53Codex
307                | ModelId::GPT51Codex
308                | ModelId::GPT51CodexMax
309                | ModelId::CopilotGPT52Codex
310                | ModelId::CopilotGPT51CodexMax
311                | ModelId::CopilotGPT54
312                | ModelId::CopilotClaudeSonnet46
313                | ModelId::GPT5Codex
314                | ModelId::ClaudeOpus47
315                | ModelId::ClaudeOpus46
316                | ModelId::ClaudeSonnet46
317                | ModelId::ClaudeMythosPreview
318                | ModelId::OpenCodeZenGPT54
319                | ModelId::OpenCodeZenClaudeSonnet46
320                | ModelId::OpenCodeZenGlm51
321                | ModelId::OpenCodeZenKimiK25
322                | ModelId::OpenCodeGoGlm51
323                | ModelId::OpenCodeGoKimiK25
324                | ModelId::OpenCodeGoMinimaxM27
325                | ModelId::DeepSeekV4Pro
326                | ModelId::ZaiGlm5
327                | ModelId::ZaiGlm51
328                | ModelId::OpenRouterStepfunStep35FlashFree
329                | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
330                | ModelId::MinimaxM27
331                | ModelId::MinimaxM25
332                | ModelId::OpenCodeGoMinimaxM25
333                | ModelId::OllamaGlm5Cloud
334                | ModelId::OllamaGlm51Cloud
335                | ModelId::OllamaNemotron3SuperCloud
336                | ModelId::OllamaMinimaxM25Cloud
337                | ModelId::HuggingFaceQwen3CoderNextNovita
338                | ModelId::HuggingFaceQwen35397BA17BTogether
339                | ModelId::HuggingFaceDeepseekV4ProTogether
340                | ModelId::OpenRouterMoonshotaiKimiK26
341        )
342    }
343
344    /// Check if this is an optimized/efficient variant
345    pub fn is_efficient_variant(&self) -> bool {
346        if let Some(meta) = self.openrouter_metadata() {
347            return meta.efficient;
348        }
349        matches!(
350            self,
351            ModelId::Gemini3FlashPreview
352                | ModelId::Gemini31FlashLitePreview
353                | ModelId::GPT5Mini
354                | ModelId::GPT5Nano
355                | ModelId::CopilotGPT54Mini
356                | ModelId::ClaudeHaiku45
357                | ModelId::OpenCodeZenGPT54Mini
358                | ModelId::OpenCodeGoMinimaxM25
359                | ModelId::DeepSeekV4Flash
360                | ModelId::HuggingFaceStep35Flash
361                | ModelId::HuggingFaceDeepseekV4FlashNovita
362        )
363    }
364
365    /// Check if this is a top-tier model
366    pub fn is_top_tier(&self) -> bool {
367        if let Some(meta) = self.openrouter_metadata() {
368            return meta.top_tier;
369        }
370        matches!(
371            self,
372            ModelId::Gemini31ProPreview
373                | ModelId::Gemini31ProPreviewCustomTools
374                | ModelId::OpenRouterGoogleGemini31ProPreview
375                | ModelId::Gemini3FlashPreview
376                | ModelId::Gemini31FlashLitePreview
377                | ModelId::GPT55
378                | ModelId::GPT5
379                | ModelId::GPT52
380                | ModelId::GPT52Codex
381                | ModelId::GPT54
382                | ModelId::GPT54Pro
383                | ModelId::GPT53Codex
384                | ModelId::GPT51Codex
385                | ModelId::GPT51CodexMax
386                | ModelId::GPT5Codex
387                | ModelId::ClaudeOpus47
388                | ModelId::ClaudeOpus46
389                | ModelId::ClaudeSonnet46
390                | ModelId::ClaudeMythosPreview
391                | ModelId::OpenCodeZenGPT54
392                | ModelId::OpenCodeZenClaudeSonnet46
393                | ModelId::OpenCodeZenGlm51
394                | ModelId::OpenCodeZenKimiK25
395                | ModelId::OpenCodeGoGlm51
396                | ModelId::OpenCodeGoKimiK25
397                | ModelId::OpenCodeGoMinimaxM27
398                | ModelId::DeepSeekV4Pro
399                | ModelId::ZaiGlm5
400                | ModelId::ZaiGlm51
401                | ModelId::OpenRouterStepfunStep35FlashFree
402                | ModelId::HuggingFaceQwen3CoderNextNovita
403                | ModelId::HuggingFaceQwen35397BA17BTogether
404                | ModelId::HuggingFaceDeepseekV4FlashNovita
405                | ModelId::HuggingFaceDeepseekV4ProTogether
406                | ModelId::OpenRouterMoonshotaiKimiK26
407        )
408    }
409
410    /// Determine whether the model is a reasoning-capable variant
411    pub fn is_reasoning_variant(&self) -> bool {
412        if let Some(meta) = self.openrouter_metadata() {
413            return meta.reasoning;
414        }
415        self.provider().supports_reasoning_effort(self.as_str())
416    }
417
418    /// Determine whether the model supports tool calls/function execution
419    pub fn supports_tool_calls(&self) -> bool {
420        if let Some(meta) = self.generated_capabilities() {
421            return meta.tool_call;
422        }
423        if let Some(meta) = self.openrouter_metadata() {
424            return meta.tool_call;
425        }
426        true
427    }
428
429    /// Ordered list of supported input modalities when VT Code has metadata for this model.
430    pub fn input_modalities(&self) -> &'static [&'static str] {
431        self.generated_capabilities()
432            .map(|meta| meta.input_modalities)
433            .unwrap_or(&[])
434    }
435
436    /// Get the generation/version string for this model
437    pub fn generation(&self) -> &'static str {
438        if let Some(meta) = self.openrouter_metadata() {
439            return meta.generation;
440        }
441        match self {
442            // Gemini generations
443            ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => "3.1",
444            ModelId::Gemini31FlashLitePreview => "3.1-lite",
445            ModelId::Gemini3FlashPreview => "3",
446            // OpenAI generations
447            ModelId::GPT55 => "5.5",
448            ModelId::GPT52 | ModelId::GPT52Codex => "5.2",
449            ModelId::GPT54 | ModelId::GPT54Pro | ModelId::GPT54Nano | ModelId::GPT54Mini => "5.4",
450            ModelId::GPT53Codex => "5.3",
451            ModelId::GPT51Codex | ModelId::GPT51CodexMax => "5.1",
452            ModelId::GPT5
453            | ModelId::GPT5Codex
454            | ModelId::GPT5Mini
455            | ModelId::GPT5Nano
456            | ModelId::OpenAIGptOss20b
457            | ModelId::OpenAIGptOss120b => "5",
458            // Anthropic generations
459            ModelId::ClaudeOpus47 => "4.7",
460            ModelId::ClaudeOpus46 => "4.6",
461            ModelId::ClaudeSonnet46 => "4.6",
462            ModelId::ClaudeHaiku45 => "4.5",
463            ModelId::ClaudeMythosPreview => "preview",
464            // DeepSeek generations
465            ModelId::DeepSeekV4Pro | ModelId::DeepSeekV4Flash => "4",
466            // Z.AI generations
467            ModelId::ZaiGlm5 => "5",
468            ModelId::ZaiGlm51 => "5.1",
469            ModelId::OpenCodeZenGPT54 | ModelId::OpenCodeZenGPT54Mini => "5.4",
470            ModelId::OpenCodeZenClaudeSonnet46 => "4.6",
471            ModelId::OpenCodeZenGlm51 | ModelId::OpenCodeGoGlm51 => "5.1",
472            ModelId::OpenCodeZenKimiK25 | ModelId::OpenCodeGoKimiK25 => "k2.5",
473            ModelId::OpenCodeGoMinimaxM25 => "m2.5",
474            ModelId::OpenCodeGoMinimaxM27 => "m2.7",
475            ModelId::OllamaGptOss20b => "oss",
476            ModelId::OllamaGptOss20bCloud => "oss-cloud",
477            ModelId::OllamaGptOss120bCloud => "oss-cloud",
478            ModelId::OllamaQwen317b => "oss",
479            ModelId::OllamaQwen3CoderNext => "qwen3-coder-next:cloud",
480            ModelId::OllamaDeepseekV32Cloud => "deepseek-v3.2",
481            ModelId::OllamaDeepseekV4FlashCloud => "deepseek-v4-flash",
482            ModelId::OllamaDeepseekV4ProCloud => "deepseek-v4-pro",
483            ModelId::OllamaQwen3Next80bCloud => "qwen3-next",
484            ModelId::OllamaMinimaxM2Cloud => "minimax-m2",
485            ModelId::OllamaMinimaxM27Cloud => "minimax-m2.7",
486            ModelId::OllamaGlm5Cloud => "glm-5",
487            ModelId::OllamaGlm51Cloud => "glm-5.1",
488            ModelId::OllamaMinimaxM25Cloud => "minimax-m2.5",
489            ModelId::OllamaKimiK26Cloud => "kimi-k2.6",
490            ModelId::OllamaNemotron3SuperCloud => "nemotron-3",
491            ModelId::OllamaGemini3FlashPreviewCloud => "gemini-3",
492            // MiniMax models
493            ModelId::MinimaxM27 => "M2.7",
494            ModelId::MinimaxM25 => "M2.5",
495            // Moonshot models
496            ModelId::MoonshotKimiK26 => "k2.6",
497            ModelId::MoonshotKimiK25 => "k2.5",
498            // Hugging Face generations
499            ModelId::HuggingFaceDeepseekV32 => "V3.2-Exp",
500            ModelId::HuggingFaceOpenAIGptOss20b => "oss",
501            ModelId::HuggingFaceOpenAIGptOss120b => "oss",
502            ModelId::HuggingFaceMinimaxM25Novita => "m2.5",
503            ModelId::HuggingFaceDeepseekV32Novita => "v3.2",
504            ModelId::HuggingFaceXiaomiMimoV2FlashNovita => "v2-flash",
505            ModelId::HuggingFaceGlm5Novita => "5",
506            ModelId::HuggingFaceGlm51ZaiOrg => "5.1",
507            ModelId::HuggingFaceKimiK26Novita => "k2.6",
508            ModelId::HuggingFaceDeepseekV4FlashNovita => "v4-flash",
509            ModelId::HuggingFaceDeepseekV4ProTogether => "v4-pro",
510            ModelId::HuggingFaceStep35Flash => "3.5",
511            ModelId::HuggingFaceQwen3CoderNextNovita | ModelId::OpenRouterQwen3CoderNext => {
512                "qwen3-coder-next"
513            }
514            _ => "unknown",
515        }
516    }
517
518    /// Determine if this model supports GPT-5.1+/5.2+/5.3+ shell tool type
519    pub fn supports_shell_tool(&self) -> bool {
520        matches!(
521            self,
522            ModelId::GPT55
523                | ModelId::GPT52
524                | ModelId::GPT52Codex
525                | ModelId::GPT54
526                | ModelId::GPT54Pro
527                | ModelId::GPT53Codex
528                | ModelId::GPT51Codex
529                | ModelId::GPT51CodexMax
530                | ModelId::GPT5Codex
531        )
532    }
533
534    /// Determine if this model supports optimized apply_patch tool
535    pub fn supports_apply_patch_tool(&self) -> bool {
536        false // Placeholder for future optimization
537    }
538}