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::MergeGatewayOpenAIGpt61Terra
343 | ModelId::MergeGatewayOpenAIGpt6Luna
344 | ModelId::ClaudeSonnet5
345 | ModelId::ClaudeSonnet55
346 | ModelId::ClaudeFable5
347 | ModelId::ClaudeFable51
348 | ModelId::ClaudeOpus5
349 | ModelId::ClaudeOpus55
350 | ModelId::MistralLarge4
351 | ModelId::OpenCodeGoGlm53
352 | ModelId::MiMoV26Pro
353 | ModelId::MiMoV26ProUltraspeed
354 | ModelId::OpenCodeGoMinimaxM3
355 | ModelId::MergeGatewayDeepseekFlash
356 | ModelId::VercelDeepseekFlash
357 | ModelId::MetaMuseSpark13
358 | ModelId::MetaMuseSpark13Contributor
359 | ModelId::MetaMuseWonder10
360 | ModelId::MetaMuseWonderFull10
361 | ModelId::ZaiGlm53
362 | ModelId::ZaiGlm53Flash
363 | ModelId::ZaiGlm53Flashx
364 | ModelId::MergeGatewayZaiGlm53Flash
365 | ModelId::MergeGatewayZaiGlm53Flashx
366 | ModelId::HuggingFaceGlm53FlashTogether
367 | ModelId::HuggingFaceGlm53Together
368 | ModelId::HuggingFaceMinimaxM3Novita
369 | ModelId::StepFun5Preview
370 | ModelId::OpenRouterMoonshotaiKimiK3
371 | ModelId::MoonshotKimiK31Pro
372 | ModelId::MoonshotKimiK3
373 | ModelId::MergeGatewayMoonshotKimiK3
374 | ModelId::MergeGatewayMoonshotKimiK31Pro
375 | ModelId::OllamaGlm53Cloud
376 | ModelId::OllamaGlm53FlashCloud
377 | ModelId::OllamaDeepSeekV41FlashCloud
378 | ModelId::OllamaMistralLarge4Cloud
379 | ModelId::OllamaMuseSparkOpenCloud
380 | ModelId::XaiGrok46
381 | ModelId::XaiGrok47
382 | ModelId::MergeGatewayXaiGrok46
383 | ModelId::MergeGatewayXaiGrok47
384 | ModelId::VercelSpacexaiGrok47
385 | ModelId::MergeGatewayXiaomimimoMimoV26Pro
386 | ModelId::MergeGatewayXiaomimimoMimoV26Flash
387 | ModelId::MergeGatewayMistralLarge4
388 | ModelId::MergeGatewayMetaMuseSparkOpen
389 | ModelId::HuggingFaceMuseSparkOpenBaseten
390 )
391 }
392
393 pub fn is_reasoning_variant(&self) -> bool {
395 if let Some(meta) = self.openrouter_metadata() {
396 return meta.reasoning;
397 }
398 self.provider().supports_reasoning(&self.as_str())
399 }
400
401 pub fn supports_tool_calls(&self) -> bool {
403 if let Some(meta) = self.generated_capabilities() {
404 return meta.tool_call;
405 }
406 if let Some(meta) = self.openrouter_metadata() {
407 return meta.tool_call;
408 }
409 true
410 }
411
412 pub fn input_modalities(&self) -> &'static [&'static str] {
414 self.generated_capabilities().map(|meta| meta.input_modalities).unwrap_or(&[])
415 }
416
417 pub fn generation(&self) -> &'static str {
419 if let Some(meta) = self.openrouter_metadata() {
420 return meta.generation;
421 }
422 match self {
423 ModelId::GPT6Astra | ModelId::GPT6Sol | ModelId::GPT6Luna => "6",
426 ModelId::GPT61Sol | ModelId::GPT61Terra => "6.1",
427 ModelId::GPT56Sol | ModelId::GPT56Terra | ModelId::GPT56Luna => "5.6",
428 ModelId::OpenAIGptOss20b | ModelId::OpenAIGptOss120b => "5",
429 ModelId::ClaudeSonnet5 => "5",
431 ModelId::ClaudeSonnet55 => "5.5",
432 ModelId::ClaudeHaiku55 => "5.5",
433 ModelId::ClaudeFable5 => "5",
434 ModelId::ClaudeFable51 => "5.1",
435 ModelId::ClaudeOpus5 => "5",
436 ModelId::ClaudeOpus55 => "5.5",
437 ModelId::DeepSeekFlash => "4",
439 ModelId::OllamaDeepSeekV41FlashCloud => "4.1",
440 ModelId::MistralLarge3 => "3",
442 ModelId::MistralLarge4 => "4",
443 ModelId::OllamaMistralLarge4Cloud => "4",
444 ModelId::MergeGatewayDeepseekFlash => "4.1",
445 ModelId::MetaMuseSpark11 => "Muse-Spark-1.1",
446 ModelId::MetaMuseSpark13 | ModelId::MetaMuseSpark13Contributor => "Muse-Spark-1.3",
447 ModelId::MergeGatewayMetaMuseSparkOpen | ModelId::HuggingFaceMuseSparkOpenBaseten => "Muse-Spark-Open",
448 ModelId::OllamaMuseSparkOpenCloud => "Muse-Spark-Open",
449 ModelId::MetaMuseWonder10 => "Muse-Wonder-1.0",
450 ModelId::MetaMuseWonderFull10 => "Muse-Wonder-Full-1.0",
451 ModelId::ZaiGlm53
453 | ModelId::ZaiGlm53Flash
454 | ModelId::ZaiGlm53Flashx
455 | ModelId::MergeGatewayZaiGlm53Flash
456 | ModelId::MergeGatewayZaiGlm53Flashx => "5.3",
457 ModelId::OllamaGlm53FlashCloud => "5.3",
458 ModelId::Gemini38Flash => "3.8",
459 ModelId::MergeGatewayGoogleGemini38Flash => "3.8",
460 ModelId::Gemini4Argon => "4",
461 ModelId::MergeGatewayGoogleGemini4Argon => "4",
462 ModelId::OpenCodeGoGlm53 => "5.3",
463 ModelId::OpenCodeGoGpt56Luna => "5.6-luna",
464 ModelId::OpenCodeGoKimiK3 => "k3",
465 ModelId::MiMoV26Pro | ModelId::MiMoV26Flash | ModelId::MiMoV26ProUltraspeed => "v2.6",
466 ModelId::MergeGatewayXiaomimimoMimoV26Pro | ModelId::MergeGatewayXiaomimimoMimoV26Flash => "v2.6",
467 ModelId::MergeGatewayMistralLarge4 => "4",
468 ModelId::OpenCodeGoMinimaxM3 => "m3",
469 ModelId::OllamaGptOss20b => "oss",
470 ModelId::OllamaGptOss20bCloud => "oss-cloud",
471 ModelId::OllamaGptOss120bCloud => "oss-cloud",
472 ModelId::OllamaMinimaxM3Cloud => "minimax-m3",
473 ModelId::OllamaMinimaxM31FlashCloud => "minimax-m3.1",
474 ModelId::OllamaGlm53Cloud => "glm-5.3",
475 ModelId::OllamaGemma4 => "gemma-4",
476 ModelId::LlamaCppGemma426bA4b => "4",
477 ModelId::LlamaCppGemma4E4b => "4",
478 ModelId::LlamaCppGptOss20b => "oss",
479 ModelId::MinimaxM3 => "M3",
481 ModelId::MinimaxM31Flash | ModelId::MergeGatewayMinimaxM31Flash => "M3.1",
482 ModelId::StepFun37Flash => "3.7",
484 ModelId::StepFun5Preview => "5",
485 ModelId::MoonshotKimiK31Pro => "k3.1",
487 ModelId::MoonshotKimiK3 => "k3",
488 ModelId::MergeGatewayMoonshotKimiK3 => "k3",
489 ModelId::MergeGatewayMoonshotKimiK31Pro => "k3.1",
490 ModelId::HuggingFaceOpenAIGptOss20b => "oss",
492 ModelId::HuggingFaceOpenAIGptOss120b => "oss",
493 ModelId::HuggingFaceMinimaxM3Novita => "m3",
494 ModelId::HuggingFaceGlm53FlashTogether => "5.3",
495 ModelId::HuggingFaceGlm53Together => "5.3",
496 ModelId::XaiGrok46 => "4.6",
498 ModelId::XaiGrok47 => "4.7",
499 ModelId::MergeGatewayXaiGrok46 => "4.6",
500 ModelId::MergeGatewayXaiGrok47 => "4.7",
501 ModelId::MergeGatewayDefaultRouting => "routing",
503 ModelId::MergeGatewayAnthropicClaudeOpus5 => "5",
504 ModelId::MergeGatewayAnthropicClaudeOpus55 => "5.5",
505 ModelId::MergeGatewayAnthropicClaudeSonnet5 => "5",
506 ModelId::MergeGatewayAnthropicClaudeSonnet55 => "5.5",
507 ModelId::MergeGatewayAnthropicClaudeHaiku55 => "5.5",
508 ModelId::MergeGatewayAnthropicClaudeFable51 => "5.1",
509 ModelId::MergeGatewayAnthropicClaudeHaiku4520251001 => "4.5",
510 ModelId::MergeGatewayMinimaxH3 => "H3",
511 ModelId::MergeGatewayThinkingMachinesInkling => "Inkling",
512 ModelId::MergeGatewayOpenAIGpt56Luna
513 | ModelId::MergeGatewayOpenAIGpt56Sol
514 | ModelId::MergeGatewayOpenAIGpt56Terra => "5.6",
515 ModelId::MergeGatewayOpenAIGpt6Astra
516 | ModelId::MergeGatewayOpenAIGpt6Sol
517 | ModelId::MergeGatewayOpenAIGpt6Luna => "6",
518 ModelId::MergeGatewayOpenAIGpt61Sol | ModelId::MergeGatewayOpenAIGpt61Terra => "6.1",
519 ModelId::VercelAnthropicClaudeSonnet5 => "5",
521 ModelId::VercelAnthropicClaudeOpus5 => "5",
522 ModelId::VercelAnthropicClaudeOpus55 => "5.5",
523 ModelId::VercelAnthropicClaudeHaiku45 => "4.5",
524 ModelId::VercelOpenAiGpt56Sol | ModelId::VercelOpenAiGpt56Luna => "5.6",
525 ModelId::VercelOpenAiGpt6Sol | ModelId::VercelOpenAiGpt6Luna => "6",
526 ModelId::VercelOpenAiGpt6Astra => "6",
527 ModelId::VercelGoogleGemini38Flash => "3.8",
528 ModelId::VercelDeepseekFlash => "4.1",
529 ModelId::VercelMoonshotaiKimiK3 => "k3",
530 ModelId::VercelMinimaxM3 => "M3",
531 ModelId::VercelSpacexaiGrok47 => "4.7",
532 _ => "unknown",
533 }
534 }
535
536 pub(crate) fn supports_shell_tool(&self) -> bool {
538 matches!(
539 self,
540 ModelId::GPT6Astra
541 | ModelId::GPT6Sol
542 | ModelId::GPT61Sol
543 | ModelId::GPT6Luna
544 | ModelId::GPT56Sol
545 | ModelId::GPT56Terra
546 | ModelId::GPT56Luna
547 )
548 }
549
550 pub fn supports_apply_patch_tool(&self) -> bool {
552 false
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn prefixed_request_model_resolves_on_bare_provider() {
562 let bare = model_catalog_entry("openai", "gpt-5.6-luna").expect("bare openai entry exists");
567 let prefixed = model_catalog_entry("openai", "openai/gpt-5.6-luna").expect("prefixed fallback must resolve");
568 assert_eq!(bare.reasoning_efforts, prefixed.reasoning_efforts);
569 assert!(!prefixed.reasoning_efforts.is_empty());
570
571 let gateway =
572 model_catalog_entry("merge-gateway", "openai/gpt-5.6-luna").expect("gateway prefixed entry exists");
573 assert!(!gateway.reasoning_efforts.is_empty());
574
575 assert!(model_catalog_entry("openai", "no-such-model-xyz").is_none());
578 assert!(model_catalog_entry("openai", "openai/no-such-model-xyz").is_none());
579 assert!(model_catalog_entry("openai", "other-org/gpt-5.6-luna").is_none());
580 }
581}