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