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