vtcode_config/models/model_id/
capabilities.rs1use 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#[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 Provider::OpenCodeZen => "opencode-zen",
120 Provider::OpenCodeGo => "opencode-go",
121 }
122}
123
124fn generated_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
125 capability_generated::metadata_for(catalog_provider_key(provider), id).map(|entry| {
126 ModelCatalogEntry {
127 provider: entry.provider,
128 id: entry.id,
129 display_name: entry.display_name,
130 description: entry.description,
131 context_window: entry.context_window,
132 max_output_tokens: entry.max_output_tokens,
133 reasoning: entry.reasoning,
134 tool_call: entry.tool_call,
135 vision: entry.vision,
136 input_modalities: entry.input_modalities,
137 caching: entry.caching,
138 structured_output: entry.structured_output,
139 pricing: ModelPricing {
140 input: entry.pricing.input,
141 output: entry.pricing.output,
142 cache_read: entry.pricing.cache_read,
143 cache_write: entry.pricing.cache_write,
144 },
145 }
146 })
147}
148
149pub fn model_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
150 generated_catalog_entry(provider, id)
151}
152
153pub fn supported_models_for_provider(provider: &str) -> Option<&'static [&'static str]> {
154 capability_generated::models_for_provider(catalog_provider_key(provider))
155}
156
157pub fn catalog_provider_keys() -> &'static [&'static str] {
158 capability_generated::PROVIDERS
159}
160
161impl ModelId {
162 fn generated_capabilities(&self) -> Option<ModelCatalogEntry> {
163 generated_catalog_entry(capability_provider_key(self.provider()), self.as_str())
164 }
165
166 pub fn preferred_lightweight_variant(&self) -> Option<Self> {
168 match self {
169 ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => {
170 Some(ModelId::Gemini31FlashLitePreview)
171 }
172 ModelId::GPT54 | ModelId::GPT54Pro => Some(ModelId::GPT54Mini),
173 ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
174 ModelId::GPT52
175 | ModelId::GPT52Codex
176 | ModelId::GPT53Codex
177 | ModelId::GPT51Codex
178 | ModelId::GPT51CodexMax
179 | ModelId::GPT5
180 | ModelId::GPT5Codex => Some(ModelId::GPT5Mini),
181 ModelId::ClaudeOpus47
182 | ModelId::ClaudeOpus46
183 | ModelId::ClaudeSonnet46
184 | ModelId::ClaudeMythosPreview => Some(ModelId::ClaudeHaiku45),
185 ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
186 ModelId::CopilotGPT52Codex | ModelId::CopilotGPT51CodexMax => {
187 Some(ModelId::CopilotGPT54Mini)
188 }
189 ModelId::DeepSeekReasoner => Some(ModelId::DeepSeekChat),
190 ModelId::ZaiGlm51 => Some(ModelId::ZaiGlm5),
191 ModelId::MinimaxM27 => Some(ModelId::MinimaxM25),
192 ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
193 _ => None,
194 }
195 }
196
197 pub fn non_reasoning_variant(&self) -> Option<Self> {
199 if let Some(meta) = self.openrouter_metadata() {
200 if !meta.reasoning {
201 return None;
202 }
203
204 let vendor = meta.vendor;
205 let mut candidates: Vec<Self> = Self::openrouter_vendor_groups()
206 .into_iter()
207 .find(|(candidate_vendor, _)| *candidate_vendor == vendor)
208 .map(|(_, models)| {
209 models
210 .iter()
211 .copied()
212 .filter(|candidate| candidate != self)
213 .filter(|candidate| {
214 candidate
215 .openrouter_metadata()
216 .map(|other| !other.reasoning)
217 .unwrap_or(false)
218 })
219 .collect()
220 })
221 .unwrap_or_default();
222
223 if candidates.is_empty() {
224 return None;
225 }
226
227 candidates.sort_by_key(|candidate| {
228 candidate
229 .openrouter_metadata()
230 .map(|data| (!data.efficient, data.display))
231 .unwrap_or((true, ""))
232 });
233
234 return candidates.into_iter().next();
235 }
236
237 let direct = match self {
238 ModelId::Gemini31ProPreview
239 | ModelId::Gemini31ProPreviewCustomTools
240 | ModelId::Gemini31FlashLitePreview => Some(ModelId::Gemini3FlashPreview),
241 ModelId::GPT52
242 | ModelId::GPT54
243 | ModelId::GPT54Pro
244 | ModelId::GPT54Nano
245 | ModelId::GPT54Mini
246 | ModelId::GPT5 => Some(ModelId::GPT5Mini),
247 ModelId::OpenCodeZenGPT54 => Some(ModelId::OpenCodeZenGPT54Mini),
248 ModelId::CopilotGPT52Codex | ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
249 ModelId::DeepSeekReasoner => Some(ModelId::DeepSeekChat),
250 ModelId::ZaiGlm5 | ModelId::ZaiGlm51 => Some(ModelId::OllamaGlm5Cloud),
251 ModelId::ClaudeOpus47
252 | ModelId::ClaudeOpus46
253 | ModelId::ClaudeSonnet46
254 | ModelId::ClaudeMythosPreview => Some(ModelId::ClaudeSonnet46),
255 ModelId::OpenCodeGoMinimaxM27 => Some(ModelId::OpenCodeGoMinimaxM25),
256 ModelId::MinimaxM27 | ModelId::MinimaxM25 => None,
257 _ => None,
258 };
259
260 direct.and_then(|candidate| {
261 if candidate.supports_reasoning_effort() {
262 None
263 } else {
264 Some(candidate)
265 }
266 })
267 }
268
269 pub fn is_flash_variant(&self) -> bool {
271 matches!(
272 self,
273 ModelId::Gemini3FlashPreview
274 | ModelId::Gemini31FlashLitePreview
275 | ModelId::OpenRouterStepfunStep35FlashFree
276 | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
277 | ModelId::OllamaGemini3FlashPreviewCloud
278 | ModelId::HuggingFaceStep35Flash
279 )
280 }
281
282 pub fn is_pro_variant(&self) -> bool {
284 matches!(
285 self,
286 ModelId::Gemini31ProPreview
287 | ModelId::Gemini31ProPreviewCustomTools
288 | ModelId::OpenRouterGoogleGemini31ProPreview
289 | ModelId::GPT5
290 | ModelId::GPT52
291 | ModelId::GPT52Codex
292 | ModelId::GPT54
293 | ModelId::GPT54Pro
294 | ModelId::GPT53Codex
295 | ModelId::GPT51Codex
296 | ModelId::GPT51CodexMax
297 | ModelId::CopilotGPT52Codex
298 | ModelId::CopilotGPT51CodexMax
299 | ModelId::CopilotGPT54
300 | ModelId::CopilotClaudeSonnet46
301 | ModelId::GPT5Codex
302 | ModelId::ClaudeOpus47
303 | ModelId::ClaudeOpus46
304 | ModelId::ClaudeSonnet46
305 | ModelId::ClaudeMythosPreview
306 | ModelId::OpenCodeZenGPT54
307 | ModelId::OpenCodeZenClaudeSonnet46
308 | ModelId::OpenCodeZenGlm51
309 | ModelId::OpenCodeZenKimiK25
310 | ModelId::OpenCodeGoGlm51
311 | ModelId::OpenCodeGoKimiK25
312 | ModelId::OpenCodeGoMinimaxM27
313 | ModelId::DeepSeekReasoner
314 | ModelId::ZaiGlm5
315 | ModelId::ZaiGlm51
316 | ModelId::OpenRouterStepfunStep35FlashFree
317 | ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
318 | ModelId::MinimaxM27
319 | ModelId::MinimaxM25
320 | ModelId::OpenCodeGoMinimaxM25
321 | ModelId::OllamaGlm5Cloud
322 | ModelId::OllamaGlm51Cloud
323 | ModelId::OllamaNemotron3SuperCloud
324 | ModelId::OllamaMinimaxM25Cloud
325 | ModelId::HuggingFaceQwen3CoderNextNovita
326 | ModelId::HuggingFaceQwen35397BA17BTogether
327 )
328 }
329
330 pub fn is_efficient_variant(&self) -> bool {
332 if let Some(meta) = self.openrouter_metadata() {
333 return meta.efficient;
334 }
335 matches!(
336 self,
337 ModelId::Gemini3FlashPreview
338 | ModelId::Gemini31FlashLitePreview
339 | ModelId::GPT5Mini
340 | ModelId::GPT5Nano
341 | ModelId::CopilotGPT54Mini
342 | ModelId::ClaudeHaiku45
343 | ModelId::OpenCodeZenGPT54Mini
344 | ModelId::OpenCodeGoMinimaxM25
345 | ModelId::DeepSeekChat
346 | ModelId::HuggingFaceStep35Flash
347 )
348 }
349
350 pub fn is_top_tier(&self) -> bool {
352 if let Some(meta) = self.openrouter_metadata() {
353 return meta.top_tier;
354 }
355 matches!(
356 self,
357 ModelId::Gemini31ProPreview
358 | ModelId::Gemini31ProPreviewCustomTools
359 | ModelId::OpenRouterGoogleGemini31ProPreview
360 | ModelId::Gemini3FlashPreview
361 | ModelId::Gemini31FlashLitePreview
362 | ModelId::GPT5
363 | ModelId::GPT52
364 | ModelId::GPT52Codex
365 | ModelId::GPT54
366 | ModelId::GPT54Pro
367 | ModelId::GPT53Codex
368 | ModelId::GPT51Codex
369 | ModelId::GPT51CodexMax
370 | ModelId::GPT5Codex
371 | ModelId::ClaudeOpus47
372 | ModelId::ClaudeOpus46
373 | ModelId::ClaudeSonnet46
374 | ModelId::ClaudeMythosPreview
375 | ModelId::OpenCodeZenGPT54
376 | ModelId::OpenCodeZenClaudeSonnet46
377 | ModelId::OpenCodeZenGlm51
378 | ModelId::OpenCodeZenKimiK25
379 | ModelId::OpenCodeGoGlm51
380 | ModelId::OpenCodeGoKimiK25
381 | ModelId::OpenCodeGoMinimaxM27
382 | ModelId::DeepSeekReasoner
383 | ModelId::ZaiGlm5
384 | ModelId::ZaiGlm51
385 | ModelId::OpenRouterStepfunStep35FlashFree
386 | ModelId::HuggingFaceQwen3CoderNextNovita
387 | ModelId::HuggingFaceQwen35397BA17BTogether
388 )
389 }
390
391 pub fn is_reasoning_variant(&self) -> bool {
393 if let Some(meta) = self.openrouter_metadata() {
394 return meta.reasoning;
395 }
396 self.provider().supports_reasoning_effort(self.as_str())
397 }
398
399 pub fn supports_tool_calls(&self) -> bool {
401 if let Some(meta) = self.generated_capabilities() {
402 return meta.tool_call;
403 }
404 if let Some(meta) = self.openrouter_metadata() {
405 return meta.tool_call;
406 }
407 true
408 }
409
410 pub fn input_modalities(&self) -> &'static [&'static str] {
412 self.generated_capabilities()
413 .map(|meta| meta.input_modalities)
414 .unwrap_or(&[])
415 }
416
417 pub fn generation(&self) -> &'static str {
419 if let Some(meta) = self.openrouter_metadata() {
420 return meta.generation;
421 }
422 match self {
423 ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => "3.1",
425 ModelId::Gemini31FlashLitePreview => "3.1-lite",
426 ModelId::Gemini3FlashPreview => "3",
427 ModelId::GPT52 | ModelId::GPT52Codex => "5.2",
429 ModelId::GPT54 | ModelId::GPT54Pro | ModelId::GPT54Nano | ModelId::GPT54Mini => "5.4",
430 ModelId::GPT53Codex => "5.3",
431 ModelId::GPT51Codex | ModelId::GPT51CodexMax => "5.1",
432 ModelId::GPT5
433 | ModelId::GPT5Codex
434 | ModelId::GPT5Mini
435 | ModelId::GPT5Nano
436 | ModelId::OpenAIGptOss20b
437 | ModelId::OpenAIGptOss120b => "5",
438 ModelId::ClaudeOpus47 => "4.7",
440 ModelId::ClaudeOpus46 => "4.6",
441 ModelId::ClaudeSonnet46 => "4.6",
442 ModelId::ClaudeHaiku45 => "4.5",
443 ModelId::ClaudeMythosPreview => "preview",
444 ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
446 ModelId::ZaiGlm5 => "5",
448 ModelId::ZaiGlm51 => "5.1",
449 ModelId::OpenCodeZenGPT54 | ModelId::OpenCodeZenGPT54Mini => "5.4",
450 ModelId::OpenCodeZenClaudeSonnet46 => "4.6",
451 ModelId::OpenCodeZenGlm51 | ModelId::OpenCodeGoGlm51 => "5.1",
452 ModelId::OpenCodeZenKimiK25 | ModelId::OpenCodeGoKimiK25 => "k2.5",
453 ModelId::OpenCodeGoMinimaxM25 => "m2.5",
454 ModelId::OpenCodeGoMinimaxM27 => "m2.7",
455 ModelId::OllamaGptOss20b => "oss",
456 ModelId::OllamaGptOss20bCloud => "oss-cloud",
457 ModelId::OllamaGptOss120bCloud => "oss-cloud",
458 ModelId::OllamaQwen317b => "oss",
459 ModelId::OllamaQwen3CoderNext => "qwen3-coder-next:cloud",
460 ModelId::OllamaDeepseekV32Cloud => "deepseek-v3.2",
461 ModelId::OllamaQwen3Next80bCloud => "qwen3-next",
462 ModelId::OllamaMinimaxM2Cloud => "minimax-m2",
463 ModelId::OllamaMinimaxM27Cloud => "minimax-m2.7",
464 ModelId::OllamaGlm5Cloud => "glm-5",
465 ModelId::OllamaGlm51Cloud => "glm-5.1",
466 ModelId::OllamaMinimaxM25Cloud => "minimax-m2.5",
467 ModelId::OllamaNemotron3SuperCloud => "nemotron-3",
468 ModelId::OllamaGemini3FlashPreviewCloud => "gemini-3",
469 ModelId::MinimaxM27 => "M2.7",
471 ModelId::MinimaxM25 => "M2.5",
472 ModelId::MoonshotKimiK25 => "k2.5",
474 ModelId::HuggingFaceDeepseekV32 => "V3.2-Exp",
476 ModelId::HuggingFaceOpenAIGptOss20b => "oss",
477 ModelId::HuggingFaceOpenAIGptOss120b => "oss",
478 ModelId::HuggingFaceMinimaxM25Novita => "m2.5",
479 ModelId::HuggingFaceDeepseekV32Novita => "v3.2",
480 ModelId::HuggingFaceXiaomiMimoV2FlashNovita => "v2-flash",
481 ModelId::HuggingFaceGlm5Novita => "5",
482 ModelId::HuggingFaceGlm51ZaiOrg => "5.1",
483 ModelId::HuggingFaceStep35Flash => "3.5",
484 ModelId::HuggingFaceQwen3CoderNextNovita | ModelId::OpenRouterQwen3CoderNext => {
485 "qwen3-coder-next"
486 }
487 _ => "unknown",
488 }
489 }
490
491 pub fn supports_shell_tool(&self) -> bool {
493 matches!(
494 self,
495 ModelId::GPT52
496 | ModelId::GPT52Codex
497 | ModelId::GPT54
498 | ModelId::GPT54Pro
499 | ModelId::GPT53Codex
500 | ModelId::GPT51Codex
501 | ModelId::GPT51CodexMax
502 | ModelId::GPT5Codex
503 )
504 }
505
506 pub fn supports_apply_patch_tool(&self) -> bool {
508 false }
510}