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