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