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