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 }
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 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 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 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 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 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 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 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 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 pub fn input_modalities(&self) -> &'static [&'static str] {
378 self.generated_capabilities()
379 .map(|meta| meta.input_modalities)
380 .unwrap_or(&[])
381 }
382
383 pub fn generation(&self) -> &'static str {
385 if let Some(meta) = self.openrouter_metadata() {
386 return meta.generation;
387 }
388 match self {
389 ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => "3.1",
391 ModelId::Gemini31FlashLitePreview => "3.1-lite",
392 ModelId::Gemini3FlashPreview => "3",
393 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 ModelId::ClaudeOpus46 | ModelId::ClaudeSonnet46 => "4.6",
406 ModelId::ClaudeHaiku45 => "4.5",
407 ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
409 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 ModelId::MinimaxM27 => "M2.7",
427 ModelId::MinimaxM25 => "M2.5",
428 ModelId::MoonshotKimiK25 => "k2.5",
430 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 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 pub fn supports_apply_patch_tool(&self) -> bool {
463 false }
465}