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