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