1use crate::models::{Provider, ProviderModelSupport};
2
3use super::ModelId;
4
5mod capability_generated {
6 include!(concat!(env!("OUT_DIR"), "/model_capabilities.rs"));
7}
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct ModelPricing {
12 pub input: Option<f64>,
13 pub output: Option<f64>,
14 pub cache_read: Option<f64>,
15 pub cache_write: Option<f64>,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct ModelCatalogEntry {
20 pub(crate) provider: &'static str,
21 id: &'static str,
22 pub display_name: &'static str,
23 pub description: &'static str,
24 pub context_window: usize,
25 max_output_tokens: Option<usize>,
26 pub reasoning: bool,
27 pub reasoning_efforts: &'static [&'static str],
28 pub is_pro: bool,
29 pub lightweight_model: Option<&'static str>,
30 pub tool_call: bool,
31 pub vision: bool,
32 pub input_modalities: &'static [&'static str],
33 pub caching: bool,
34 pub structured_output: bool,
35 pub supports_sampling: bool,
36 pub supports_logprobs: bool,
37 pub prompt_cache_ttl: Option<&'static str>,
38 pub prompt_contract: Option<&'static str>,
39 pub pricing: ModelPricing,
40}
41
42fn catalog_provider_key(provider: &str) -> &str {
43 if provider.eq_ignore_ascii_case("google") || provider.eq_ignore_ascii_case("gemini") {
44 "gemini"
45 } else if provider.eq_ignore_ascii_case("openai") {
46 "openai"
47 } else if provider.eq_ignore_ascii_case("anthropic") {
48 "anthropic"
49 } else if provider.eq_ignore_ascii_case("deepseek") {
50 "deepseek"
51 } else if provider.eq_ignore_ascii_case("meta") || provider.eq_ignore_ascii_case("meta-ai") {
52 "meta"
53 } else if provider.eq_ignore_ascii_case("openrouter") {
54 "openrouter"
55 } else if provider.eq_ignore_ascii_case("ollama") {
56 "ollama"
57 } else if provider.eq_ignore_ascii_case("lmstudio") {
58 "lmstudio"
59 } else if provider.eq_ignore_ascii_case("llamacpp") || provider.eq_ignore_ascii_case("llama.cpp") {
60 "llamacpp"
61 } else if provider.eq_ignore_ascii_case("moonshot") {
62 "moonshot"
63 } else if provider.eq_ignore_ascii_case("zai") {
64 "zai"
65 } else if provider.eq_ignore_ascii_case("minimax") {
66 "minimax"
67 } else if provider.eq_ignore_ascii_case("huggingface") {
68 "huggingface"
69 } else if provider.eq_ignore_ascii_case("stepfun") {
70 "stepfun"
71 } else if provider.eq_ignore_ascii_case("evolink") {
72 "evolink"
73 } else if provider.eq_ignore_ascii_case("poolside") {
74 "poolside"
75 } else if provider.eq_ignore_ascii_case("xai") {
76 "xai"
77 } else if provider.eq_ignore_ascii_case("nvidia") {
78 "nvidia"
79 } else if provider.eq_ignore_ascii_case("merge-gateway") {
80 "merge-gateway"
81 } else if provider.eq_ignore_ascii_case("vercel") || provider.eq_ignore_ascii_case("vercel-ai-gateway") {
82 "vercel"
83 } else {
84 provider
85 }
86}
87
88fn capability_provider_key(provider: Provider) -> &'static str {
89 match provider {
90 Provider::Gemini => "gemini",
91 Provider::OpenAI => "openai",
92 Provider::Anthropic => "anthropic",
93 Provider::Copilot => "copilot",
94 Provider::DeepSeek => "deepseek",
95 Provider::Meta => "meta",
96 Provider::OpenRouter => "openrouter",
97 Provider::Ollama => "ollama",
98 Provider::OllamaCloud => "ollama-cloud",
99 Provider::LmStudio => "lmstudio",
100 Provider::LlamaCpp => "llamacpp",
101 Provider::Moonshot => "moonshot",
102 Provider::ZAI => "zai",
103 Provider::Minimax => "minimax",
104 Provider::MiMo => "mimo",
105 Provider::Mistral => "mistral",
106 Provider::HuggingFace => "huggingface",
107 Provider::OpenCodeZen => "opencode-zen",
108 Provider::OpenCodeGo => "opencode-go",
109 Provider::Qwen => "qwen",
110 Provider::StepFun => "stepfun",
111 Provider::Evolink => "evolink",
112 Provider::Poolside => "poolside",
113 Provider::XAI => "xai",
114 Provider::NVIDIA => "nvidia",
115 Provider::MergeGateway => "merge-gateway",
116 Provider::Vercel => "vercel",
117 }
118}
119
120fn catalog_lookup_id<'a>(provider: &str, id: &'a str) -> &'a str {
121 let provider_key = catalog_provider_key(provider);
122 if provider_key == "evolink"
123 && let Some((prefix, model_id)) = id.split_once('/')
124 && prefix.eq_ignore_ascii_case(provider_key)
125 {
126 return model_id;
130 }
131 id
132}
133
134fn generated_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
135 let provider_key = catalog_provider_key(provider);
136 let lookup_id = catalog_lookup_id(provider_key, id);
137 let entry = capability_generated::metadata_for(provider_key, lookup_id).or_else(|| {
147 lookup_id
148 .split_once('/')
149 .filter(|(prefix, _)| prefix.eq_ignore_ascii_case(provider_key))
150 .and_then(|(_, rest)| capability_generated::metadata_for(provider_key, rest))
151 });
152 entry.map(|entry| ModelCatalogEntry {
153 provider: entry.provider,
154 id: entry.id,
155 display_name: entry.display_name,
156 description: entry.description,
157 context_window: entry.context_window,
158 max_output_tokens: entry.max_output_tokens,
159 reasoning: entry.reasoning,
160 reasoning_efforts: entry.reasoning_efforts,
161 is_pro: entry.is_pro,
162 lightweight_model: entry.lightweight_model,
163 tool_call: entry.tool_call,
164 vision: entry.vision,
165 input_modalities: entry.input_modalities,
166 caching: entry.caching,
167 structured_output: entry.structured_output,
168 supports_sampling: entry.supports_sampling,
169 supports_logprobs: entry.supports_logprobs,
170 prompt_cache_ttl: entry.prompt_cache_ttl,
171 prompt_contract: entry.prompt_contract,
172 pricing: ModelPricing {
173 input: entry.pricing.input,
174 output: entry.pricing.output,
175 cache_read: entry.pricing.cache_read,
176 cache_write: entry.pricing.cache_write,
177 },
178 })
179}
180
181pub fn model_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
182 generated_catalog_entry(provider, id)
183}
184
185pub fn supported_models_for_provider(provider: &str) -> Option<&'static [&'static str]> {
186 capability_generated::models_for_provider(catalog_provider_key(provider))
187}
188
189pub fn catalog_provider_keys() -> &'static [&'static str] {
190 capability_generated::PROVIDERS
191}
192
193impl ModelId {
194 fn generated_capabilities(&self) -> Option<ModelCatalogEntry> {
195 generated_catalog_entry(capability_provider_key(self.provider()), &self.as_str())
196 }
197
198 pub fn preferred_lightweight_variant(&self) -> Option<Self> {
200 let target_id = self.generated_capabilities()?.lightweight_model?;
201 Self::all_models().into_iter().find(|candidate| {
202 candidate != self && candidate.provider() == self.provider() && candidate.as_str() == target_id
203 })
204 }
205
206 pub fn non_reasoning_variant(&self) -> Option<Self> {
208 if let Some(meta) = self.openrouter_metadata() {
209 if !meta.reasoning {
210 return None;
211 }
212
213 let vendor = meta.vendor;
214 let mut candidates: Vec<Self> = Self::openrouter_vendor_groups()
215 .into_iter()
216 .find(|(candidate_vendor, _)| *candidate_vendor == vendor)
217 .map(|(_, models)| {
218 models
219 .iter()
220 .filter(|&candidate| candidate != self)
221 .filter(|&candidate| {
222 candidate.openrouter_metadata().map(|other| !other.reasoning).unwrap_or(false)
223 })
224 .cloned()
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 self.preferred_lightweight_variant()
244 .filter(|candidate| !candidate.is_reasoning_variant())
245 }
246
247 pub fn is_flash_variant(&self) -> bool {
249 matches!(
250 self,
251 ModelId::Gemini38Flash
252 | ModelId::MergeGatewayGoogleGemini38Flash
253 | ModelId::VercelGoogleGemini38Flash
254 | ModelId::VercelDeepseekFlash
255 | ModelId::VercelOpenAiGpt56Luna
256 | ModelId::VercelAnthropicClaudeHaiku45
257 | ModelId::ClaudeHaiku55
258 | ModelId::MergeGatewayAnthropicClaudeHaiku4520251001
259 | ModelId::MergeGatewayAnthropicClaudeHaiku55
260 | ModelId::StepFun37Flash
261 | ModelId::MergeGatewayDeepseekFlash
262 | ModelId::DeepSeekFlash
263 | ModelId::OllamaDeepSeekV41FlashCloud
264 | ModelId::ZaiGlm53Flash
265 | ModelId::ZaiGlm53Flashx
266 | ModelId::OllamaGlm53FlashCloud
267 | ModelId::OllamaMinimaxM31FlashCloud
268 | ModelId::MergeGatewayZaiGlm53Flash
269 | ModelId::MergeGatewayZaiGlm53Flashx
270 | ModelId::HuggingFaceGlm53FlashTogether
271 | ModelId::MiMoV26Flash
272 | ModelId::MergeGatewayXiaomimimoMimoV26Flash
273 | ModelId::MinimaxM31Flash
274 | ModelId::MergeGatewayMinimaxM31Flash
275 | ModelId::OpenRouterMinimaxM31FlashFree
276 )
277 }
278
279 pub fn is_pro_variant(&self) -> bool {
281 self.generated_capabilities().is_some_and(|entry| entry.is_pro)
282 }
283
284 pub fn is_efficient_variant(&self) -> bool {
286 if let Some(meta) = self.openrouter_metadata() {
287 return meta.efficient;
288 }
289 matches!(
290 self,
291 ModelId::Gemini38Flash
292 | ModelId::MergeGatewayGoogleGemini38Flash
293 | ModelId::GPT56Luna
294 | ModelId::GPT6Luna
295 | ModelId::MergeGatewayOpenAIGpt56Luna
296 | ModelId::MergeGatewayOpenAIGpt6Luna
297 | ModelId::CopilotGPT54Mini
298 | ModelId::ClaudeHaiku55
299 | ModelId::MergeGatewayAnthropicClaudeHaiku55
300 | ModelId::DeepSeekFlash
301 | ModelId::OllamaDeepSeekV41FlashCloud
302 | ModelId::MergeGatewayDeepseekFlash
303 | ModelId::MetaMuseSpark11
304 | ModelId::MergeGatewayMinimaxH3
305 | ModelId::MiMoV26Flash
306 | ModelId::ZaiGlm53Flash
307 | ModelId::ZaiGlm53Flashx
308 | ModelId::OllamaGlm53FlashCloud
309 | ModelId::OllamaMinimaxM31FlashCloud
310 | ModelId::MergeGatewayZaiGlm53Flash
311 | ModelId::MergeGatewayZaiGlm53Flashx
312 | ModelId::HuggingFaceGlm53FlashTogether
313 | ModelId::VercelDeepseekFlash
314 | ModelId::MergeGatewayXiaomimimoMimoV26Flash
315 | ModelId::MinimaxM31Flash
316 | ModelId::MergeGatewayMinimaxM31Flash
317 | ModelId::OpenRouterMinimaxM31FlashFree
318 )
319 }
320
321 pub fn is_top_tier(&self) -> bool {
323 if let Some(meta) = self.openrouter_metadata() {
324 return meta.top_tier;
325 }
326 matches!(
327 self,
328 ModelId::Gemini38Flash
329 | ModelId::MergeGatewayGoogleGemini38Flash
330 | ModelId::Gemini4Argon
331 | ModelId::MergeGatewayGoogleGemini4Argon
332 | ModelId::GPT6Astra
333 | ModelId::GPT6Sol
334 | ModelId::GPT61Sol
335 | ModelId::GPT56Sol
336 | ModelId::MergeGatewayOpenAIGpt56Sol
337 | ModelId::MergeGatewayOpenAIGpt56Terra
338 | ModelId::MergeGatewayOpenAIGpt56Luna
339 | ModelId::MergeGatewayOpenAIGpt6Astra
340 | ModelId::MergeGatewayOpenAIGpt6Sol
341 | ModelId::MergeGatewayOpenAIGpt61Sol
342 | ModelId::MergeGatewayOpenAIGpt6Luna
343 | ModelId::ClaudeSonnet5
344 | ModelId::ClaudeSonnet55
345 | ModelId::ClaudeFable5
346 | ModelId::ClaudeFable51
347 | ModelId::ClaudeOpus5
348 | ModelId::ClaudeOpus55
349 | ModelId::MistralLarge4
350 | ModelId::OpenCodeGoGlm53
351 | ModelId::MiMoV26Pro
352 | ModelId::MiMoV26ProUltraspeed
353 | ModelId::OpenCodeGoMinimaxM3
354 | ModelId::MergeGatewayDeepseekFlash
355 | ModelId::VercelDeepseekFlash
356 | ModelId::MetaMuseSpark13
357 | ModelId::MetaMuseSpark13Contributor
358 | ModelId::MetaMuseWonder10
359 | ModelId::MetaMuseWonderFull10
360 | ModelId::ZaiGlm53
361 | ModelId::ZaiGlm53Flash
362 | ModelId::ZaiGlm53Flashx
363 | ModelId::MergeGatewayZaiGlm53Flash
364 | ModelId::MergeGatewayZaiGlm53Flashx
365 | ModelId::HuggingFaceGlm53FlashTogether
366 | ModelId::HuggingFaceGlm53Together
367 | ModelId::HuggingFaceMinimaxM3Novita
368 | ModelId::StepFun5Preview
369 | ModelId::OpenRouterMoonshotaiKimiK3
370 | ModelId::MoonshotKimiK3
371 | ModelId::MergeGatewayMoonshotKimiK3
372 | ModelId::OllamaGlm53Cloud
373 | ModelId::OllamaGlm53FlashCloud
374 | ModelId::OllamaDeepSeekV41FlashCloud
375 | ModelId::OllamaMistralLarge4Cloud
376 | ModelId::OllamaMuseSparkOpenCloud
377 | ModelId::XaiGrok46
378 | ModelId::XaiGrok47
379 | ModelId::MergeGatewayXaiGrok46
380 | ModelId::MergeGatewayXaiGrok47
381 | ModelId::VercelSpacexaiGrok47
382 | ModelId::MergeGatewayXiaomimimoMimoV26Pro
383 | ModelId::MergeGatewayXiaomimimoMimoV26Flash
384 | ModelId::MergeGatewayMistralLarge4
385 | ModelId::MergeGatewayMetaMuseSparkOpen
386 | ModelId::HuggingFaceMuseSparkOpenBaseten
387 )
388 }
389
390 pub fn is_reasoning_variant(&self) -> bool {
392 if let Some(meta) = self.openrouter_metadata() {
393 return meta.reasoning;
394 }
395 self.provider().supports_reasoning(&self.as_str())
396 }
397
398 pub fn supports_tool_calls(&self) -> bool {
400 if let Some(meta) = self.generated_capabilities() {
401 return meta.tool_call;
402 }
403 if let Some(meta) = self.openrouter_metadata() {
404 return meta.tool_call;
405 }
406 true
407 }
408
409 pub fn input_modalities(&self) -> &'static [&'static str] {
411 self.generated_capabilities().map(|meta| meta.input_modalities).unwrap_or(&[])
412 }
413
414 pub fn generation(&self) -> &'static str {
416 if let Some(meta) = self.openrouter_metadata() {
417 return meta.generation;
418 }
419 match self {
420 ModelId::GPT6Astra | ModelId::GPT6Sol | ModelId::GPT6Luna => "6",
423 ModelId::GPT61Sol => "6.1",
424 ModelId::GPT56Sol | ModelId::GPT56Terra | ModelId::GPT56Luna => "5.6",
425 ModelId::OpenAIGptOss20b | ModelId::OpenAIGptOss120b => "5",
426 ModelId::ClaudeSonnet5 => "5",
428 ModelId::ClaudeSonnet55 => "5.5",
429 ModelId::ClaudeHaiku55 => "5.5",
430 ModelId::ClaudeFable5 => "5",
431 ModelId::ClaudeFable51 => "5.1",
432 ModelId::ClaudeOpus5 => "5",
433 ModelId::ClaudeOpus55 => "5.5",
434 ModelId::DeepSeekFlash => "4",
436 ModelId::OllamaDeepSeekV41FlashCloud => "4.1",
437 ModelId::MistralLarge3 => "3",
439 ModelId::MistralLarge4 => "4",
440 ModelId::OllamaMistralLarge4Cloud => "4",
441 ModelId::MergeGatewayDeepseekFlash => "4.1",
442 ModelId::MetaMuseSpark11 => "Muse-Spark-1.1",
443 ModelId::MetaMuseSpark13 | ModelId::MetaMuseSpark13Contributor => "Muse-Spark-1.3",
444 ModelId::MergeGatewayMetaMuseSparkOpen | ModelId::HuggingFaceMuseSparkOpenBaseten => "Muse-Spark-Open",
445 ModelId::OllamaMuseSparkOpenCloud => "Muse-Spark-Open",
446 ModelId::MetaMuseWonder10 => "Muse-Wonder-1.0",
447 ModelId::MetaMuseWonderFull10 => "Muse-Wonder-Full-1.0",
448 ModelId::ZaiGlm53
450 | ModelId::ZaiGlm53Flash
451 | ModelId::ZaiGlm53Flashx
452 | ModelId::MergeGatewayZaiGlm53Flash
453 | ModelId::MergeGatewayZaiGlm53Flashx => "5.3",
454 ModelId::OllamaGlm53FlashCloud => "5.3",
455 ModelId::Gemini38Flash => "3.8",
456 ModelId::MergeGatewayGoogleGemini38Flash => "3.8",
457 ModelId::Gemini4Argon => "4",
458 ModelId::MergeGatewayGoogleGemini4Argon => "4",
459 ModelId::OpenCodeGoGlm53 => "5.3",
460 ModelId::OpenCodeGoGpt56Luna => "5.6-luna",
461 ModelId::OpenCodeGoKimiK3 => "k3",
462 ModelId::MiMoV26Pro | ModelId::MiMoV26Flash | ModelId::MiMoV26ProUltraspeed => "v2.6",
463 ModelId::MergeGatewayXiaomimimoMimoV26Pro | ModelId::MergeGatewayXiaomimimoMimoV26Flash => "v2.6",
464 ModelId::MergeGatewayMistralLarge4 => "4",
465 ModelId::OpenCodeGoMinimaxM3 => "m3",
466 ModelId::OllamaGptOss20b => "oss",
467 ModelId::OllamaGptOss20bCloud => "oss-cloud",
468 ModelId::OllamaGptOss120bCloud => "oss-cloud",
469 ModelId::OllamaMinimaxM3Cloud => "minimax-m3",
470 ModelId::OllamaMinimaxM31FlashCloud => "minimax-m3.1",
471 ModelId::OllamaGlm53Cloud => "glm-5.3",
472 ModelId::OllamaGemma4 => "gemma-4",
473 ModelId::LlamaCppGemma426bA4b => "4",
474 ModelId::LlamaCppGemma4E4b => "4",
475 ModelId::LlamaCppGptOss20b => "oss",
476 ModelId::MinimaxM3 => "M3",
478 ModelId::MinimaxM31Flash | ModelId::MergeGatewayMinimaxM31Flash => "M3.1",
479 ModelId::StepFun37Flash => "3.7",
481 ModelId::StepFun5Preview => "5",
482 ModelId::MoonshotKimiK3 => "k3",
484 ModelId::MergeGatewayMoonshotKimiK3 => "k3",
485 ModelId::HuggingFaceOpenAIGptOss20b => "oss",
487 ModelId::HuggingFaceOpenAIGptOss120b => "oss",
488 ModelId::HuggingFaceMinimaxM3Novita => "m3",
489 ModelId::HuggingFaceGlm53FlashTogether => "5.3",
490 ModelId::HuggingFaceGlm53Together => "5.3",
491 ModelId::XaiGrok46 => "4.6",
493 ModelId::XaiGrok47 => "4.7",
494 ModelId::MergeGatewayXaiGrok46 => "4.6",
495 ModelId::MergeGatewayXaiGrok47 => "4.7",
496 ModelId::MergeGatewayDefaultRouting => "routing",
498 ModelId::MergeGatewayAnthropicClaudeOpus5 => "5",
499 ModelId::MergeGatewayAnthropicClaudeOpus55 => "5.5",
500 ModelId::MergeGatewayAnthropicClaudeSonnet5 => "5",
501 ModelId::MergeGatewayAnthropicClaudeSonnet55 => "5.5",
502 ModelId::MergeGatewayAnthropicClaudeHaiku55 => "5.5",
503 ModelId::MergeGatewayAnthropicClaudeFable51 => "5.1",
504 ModelId::MergeGatewayAnthropicClaudeHaiku4520251001 => "4.5",
505 ModelId::MergeGatewayMinimaxH3 => "H3",
506 ModelId::MergeGatewayThinkingMachinesInkling => "Inkling",
507 ModelId::MergeGatewayOpenAIGpt56Luna
508 | ModelId::MergeGatewayOpenAIGpt56Sol
509 | ModelId::MergeGatewayOpenAIGpt56Terra => "5.6",
510 ModelId::MergeGatewayOpenAIGpt6Astra
511 | ModelId::MergeGatewayOpenAIGpt6Sol
512 | ModelId::MergeGatewayOpenAIGpt6Luna => "6",
513 ModelId::MergeGatewayOpenAIGpt61Sol => "6.1",
514 ModelId::VercelAnthropicClaudeSonnet5 => "5",
516 ModelId::VercelAnthropicClaudeOpus5 => "5",
517 ModelId::VercelAnthropicClaudeOpus55 => "5.5",
518 ModelId::VercelAnthropicClaudeHaiku45 => "4.5",
519 ModelId::VercelOpenAiGpt56Sol | ModelId::VercelOpenAiGpt56Luna => "5.6",
520 ModelId::VercelOpenAiGpt6Sol | ModelId::VercelOpenAiGpt6Luna => "6",
521 ModelId::VercelOpenAiGpt6Astra => "6",
522 ModelId::VercelGoogleGemini38Flash => "3.8",
523 ModelId::VercelDeepseekFlash => "4.1",
524 ModelId::VercelMoonshotaiKimiK3 => "k3",
525 ModelId::VercelMinimaxM3 => "M3",
526 ModelId::VercelSpacexaiGrok47 => "4.7",
527 _ => "unknown",
528 }
529 }
530
531 pub(crate) fn supports_shell_tool(&self) -> bool {
533 matches!(
534 self,
535 ModelId::GPT6Astra
536 | ModelId::GPT6Sol
537 | ModelId::GPT61Sol
538 | ModelId::GPT6Luna
539 | ModelId::GPT56Sol
540 | ModelId::GPT56Terra
541 | ModelId::GPT56Luna
542 )
543 }
544
545 pub fn supports_apply_patch_tool(&self) -> bool {
547 false
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn prefixed_request_model_resolves_on_bare_provider() {
557 let bare = model_catalog_entry("openai", "gpt-5.6-luna").expect("bare openai entry exists");
562 let prefixed = model_catalog_entry("openai", "openai/gpt-5.6-luna").expect("prefixed fallback must resolve");
563 assert_eq!(bare.reasoning_efforts, prefixed.reasoning_efforts);
564 assert!(!prefixed.reasoning_efforts.is_empty());
565
566 let gateway =
567 model_catalog_entry("merge-gateway", "openai/gpt-5.6-luna").expect("gateway prefixed entry exists");
568 assert!(!gateway.reasoning_efforts.is_empty());
569
570 assert!(model_catalog_entry("openai", "no-such-model-xyz").is_none());
573 assert!(model_catalog_entry("openai", "openai/no-such-model-xyz").is_none());
574 assert!(model_catalog_entry("openai", "other-org/gpt-5.6-luna").is_none());
575 }
576}