Skip to main content

codewhale_agent/
lib.rs

1use std::collections::HashMap;
2
3use codewhale_config::{ProviderKind, opencode_go_chat_model_id};
4use serde::{Deserialize, Serialize};
5
6/// High-level model family used for shared identity affordances across clients.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ModelFamily {
9    DeepSeek,
10    Anthropic,
11    OpenAI,
12    Google,
13    Meta,
14    Mistral,
15    Qwen,
16    Grok,
17    Cohere,
18    GptOss,
19    Inferencer,
20}
21
22/// Metadata for a single model entry in the registry.
23///
24/// Each model has a canonical `id` used by the provider, a list of `aliases`
25/// that users may reference, and capability flags indicating whether the model
26/// supports tool use and reasoning.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ModelInfo {
29    /// The canonical model identifier used by the provider (e.g. `"deepseek-v4-pro"`).
30    pub id: String,
31    /// The provider that serves this model.
32    pub provider: ProviderKind,
33    /// Alternative names that users can use to reference this model (case-insensitive).
34    pub aliases: Vec<String>,
35    /// Whether this model supports tool/function calling.
36    pub supports_tools: bool,
37    /// Whether this model supports extended reasoning.
38    pub supports_reasoning: bool,
39}
40
41/// The result of resolving a user-requested model name to a concrete model entry.
42///
43/// Contains the resolved [`ModelInfo`], whether a fallback was used, and the
44/// chain of resolution strategies that were attempted.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelResolution {
47    /// The original model name requested by the user, if any.
48    pub requested: Option<String>,
49    /// The concrete model that was resolved.
50    pub resolved: ModelInfo,
51    /// Whether a fallback was used because the requested model was not found.
52    pub used_fallback: bool,
53    /// The ordered list of resolution strategies that were attempted.
54    pub fallback_chain: Vec<String>,
55}
56
57/// A registry of supported models and their aliases, used to resolve user-facing
58/// model names to concrete provider-specific model entries.
59///
60/// The default registry is populated with all built-in models across supported
61/// providers (DeepSeek, NVIDIA NIM, OpenAI-compatible, and others).
62#[derive(Debug, Clone)]
63pub struct ModelRegistry {
64    models: Vec<ModelInfo>,
65    alias_map: HashMap<String, usize>,
66}
67
68/// Creates a registry pre-populated with all built-in models and their aliases.
69impl Default for ModelRegistry {
70    fn default() -> Self {
71        let models = vec![
72            ModelInfo {
73                id: "deepseek-v4-pro".to_string(),
74                provider: ProviderKind::Deepseek,
75                aliases: vec![],
76                supports_tools: true,
77                supports_reasoning: true,
78            },
79            ModelInfo {
80                id: "deepseek-v4-flash".to_string(),
81                provider: ProviderKind::Deepseek,
82                aliases: vec![
83                    "deepseek-chat".to_string(),
84                    "deepseek-reasoner".to_string(),
85                    "deepseek-r1".to_string(),
86                    "deepseek-v3".to_string(),
87                    "deepseek-v3.2".to_string(),
88                ],
89                supports_tools: true,
90                supports_reasoning: true,
91            },
92            ModelInfo {
93                id: "deepseek-ai/deepseek-v4-pro".to_string(),
94                provider: ProviderKind::NvidiaNim,
95                aliases: vec![
96                    "deepseek-v4-pro".to_string(),
97                    "nvidia-deepseek-v4-pro".to_string(),
98                    "nim-deepseek-v4-pro".to_string(),
99                ],
100                supports_tools: true,
101                supports_reasoning: true,
102            },
103            ModelInfo {
104                id: "deepseek-ai/deepseek-v4-flash".to_string(),
105                provider: ProviderKind::NvidiaNim,
106                aliases: vec![
107                    "deepseek-v4-flash".to_string(),
108                    "deepseek-chat".to_string(),
109                    "deepseek-reasoner".to_string(),
110                    "nvidia-deepseek-v4-flash".to_string(),
111                    "nim-deepseek-v4-flash".to_string(),
112                ],
113                supports_tools: true,
114                supports_reasoning: true,
115            },
116            ModelInfo {
117                id: "deepseek-v4-pro".to_string(),
118                provider: ProviderKind::Openai,
119                aliases: vec!["openai-compatible-deepseek-v4-pro".to_string()],
120                supports_tools: true,
121                supports_reasoning: true,
122            },
123            ModelInfo {
124                id: "deepseek-v4-flash".to_string(),
125                provider: ProviderKind::Openai,
126                aliases: vec!["openai-compatible-deepseek-v4-flash".to_string()],
127                supports_tools: true,
128                supports_reasoning: true,
129            },
130            // OpenAI public API models carried by the bundled catalog.
131            ModelInfo {
132                id: "gpt-5.3-codex".to_string(),
133                provider: ProviderKind::Openai,
134                aliases: vec!["gpt53-codex".to_string()],
135                supports_tools: true,
136                supports_reasoning: true,
137            },
138            ModelInfo {
139                id: "gpt-5.5".to_string(),
140                provider: ProviderKind::Openai,
141                aliases: vec!["openai-gpt-5.5".to_string()],
142                supports_tools: true,
143                supports_reasoning: true,
144            },
145            ModelInfo {
146                id: "gpt-5.5-pro".to_string(),
147                provider: ProviderKind::Openai,
148                aliases: vec!["openai-gpt-5.5-pro".to_string()],
149                supports_tools: true,
150                supports_reasoning: true,
151            },
152            // OpenAI public API GPT-5.6 family.
153            ModelInfo {
154                id: "gpt-5.6".to_string(),
155                provider: ProviderKind::Openai,
156                aliases: vec!["gpt56".to_string()],
157                supports_tools: true,
158                supports_reasoning: true,
159            },
160            ModelInfo {
161                id: "gpt-5.6-sol".to_string(),
162                provider: ProviderKind::Openai,
163                aliases: vec!["gpt56-sol".to_string()],
164                supports_tools: true,
165                supports_reasoning: true,
166            },
167            ModelInfo {
168                id: "gpt-5.6-terra".to_string(),
169                provider: ProviderKind::Openai,
170                aliases: vec!["gpt56-terra".to_string()],
171                supports_tools: true,
172                supports_reasoning: true,
173            },
174            ModelInfo {
175                id: "gpt-5.6-luna".to_string(),
176                provider: ProviderKind::Openai,
177                aliases: vec!["gpt56-luna".to_string()],
178                supports_tools: true,
179                supports_reasoning: true,
180            },
181            ModelInfo {
182                id: "deepseek-ai/deepseek-v4-flash".to_string(),
183                provider: ProviderKind::Atlascloud,
184                aliases: vec![
185                    "deepseek-v4-flash".to_string(),
186                    "atlascloud-deepseek-v4-flash".to_string(),
187                ],
188                supports_tools: true,
189                supports_reasoning: true,
190            },
191            ModelInfo {
192                id: "deepseek-ai/deepseek-v4-pro".to_string(),
193                provider: ProviderKind::Atlascloud,
194                aliases: vec![
195                    "deepseek-v4-pro".to_string(),
196                    "atlascloud-deepseek-v4-pro".to_string(),
197                ],
198                supports_tools: true,
199                supports_reasoning: true,
200            },
201            ModelInfo {
202                id: "deepseek-reasoner".to_string(),
203                provider: ProviderKind::WanjieArk,
204                aliases: vec![
205                    "wanjie-deepseek-reasoner".to_string(),
206                    "ark-wanjie-deepseek-reasoner".to_string(),
207                ],
208                supports_tools: true,
209                supports_reasoning: true,
210            },
211            ModelInfo {
212                id: "DeepSeek-V4-Pro".to_string(),
213                provider: ProviderKind::Volcengine,
214                aliases: vec![
215                    "deepseek-v4-pro".to_string(),
216                    "volcengine-deepseek-v4-pro".to_string(),
217                    "ark-deepseek-v4-pro".to_string(),
218                ],
219                supports_tools: true,
220                supports_reasoning: true,
221            },
222            ModelInfo {
223                id: "DeepSeek-V4-Flash".to_string(),
224                provider: ProviderKind::Volcengine,
225                aliases: vec![
226                    "deepseek-v4-flash".to_string(),
227                    "deepseek-chat".to_string(),
228                    "volcengine-deepseek-v4-flash".to_string(),
229                    "ark-deepseek-v4-flash".to_string(),
230                ],
231                supports_tools: true,
232                supports_reasoning: true,
233            },
234            ModelInfo {
235                id: "trinity-large-thinking".to_string(),
236                provider: ProviderKind::Arcee,
237                aliases: vec![
238                    "trinity".to_string(),
239                    "arcee-trinity".to_string(),
240                    "arcee-trinity-large-thinking".to_string(),
241                ],
242                supports_tools: true,
243                supports_reasoning: true,
244            },
245            ModelInfo {
246                id: "trinity-mini".to_string(),
247                provider: ProviderKind::Arcee,
248                aliases: vec!["arcee-trinity-mini".to_string()],
249                supports_tools: true,
250                supports_reasoning: true,
251            },
252            ModelInfo {
253                id: "deepseek/deepseek-v4-pro".to_string(),
254                provider: ProviderKind::Openrouter,
255                aliases: vec![
256                    "deepseek-v4-pro".to_string(),
257                    "openrouter-deepseek-v4-pro".to_string(),
258                ],
259                supports_tools: true,
260                supports_reasoning: true,
261            },
262            ModelInfo {
263                id: "deepseek/deepseek-v4-flash".to_string(),
264                provider: ProviderKind::Openrouter,
265                aliases: vec![
266                    "deepseek-v4-flash".to_string(),
267                    "deepseek-chat".to_string(),
268                    "deepseek-reasoner".to_string(),
269                    "openrouter-deepseek-v4-flash".to_string(),
270                ],
271                supports_tools: true,
272                supports_reasoning: true,
273            },
274            ModelInfo {
275                id: "arcee-ai/trinity-large-thinking".to_string(),
276                provider: ProviderKind::Openrouter,
277                aliases: vec![
278                    "trinity".to_string(),
279                    "trinity-large-thinking".to_string(),
280                    "arcee-trinity-large-thinking".to_string(),
281                ],
282                supports_tools: true,
283                supports_reasoning: true,
284            },
285            ModelInfo {
286                id: "xiaomi/mimo-v2.5-pro".to_string(),
287                provider: ProviderKind::Openrouter,
288                aliases: vec![
289                    "openrouter-mimo-v2.5-pro".to_string(),
290                    "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
291                ],
292                supports_tools: true,
293                supports_reasoning: true,
294            },
295            ModelInfo {
296                id: "xiaomi/mimo-v2.5".to_string(),
297                provider: ProviderKind::Openrouter,
298                aliases: vec![
299                    "openrouter-mimo-v2.5".to_string(),
300                    "openrouter-xiaomi-mimo-v2.5".to_string(),
301                ],
302                supports_tools: true,
303                supports_reasoning: true,
304            },
305            ModelInfo {
306                id: "qwen/qwen3.6-flash".to_string(),
307                provider: ProviderKind::Openrouter,
308                aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()],
309                supports_tools: true,
310                supports_reasoning: true,
311            },
312            ModelInfo {
313                id: "qwen/qwen3.6-35b-a3b".to_string(),
314                provider: ProviderKind::Openrouter,
315                aliases: vec![
316                    "qwen3.6-35b-a3b".to_string(),
317                    "qwen-3.6-35b-a3b".to_string(),
318                ],
319                supports_tools: true,
320                supports_reasoning: true,
321            },
322            ModelInfo {
323                id: "qwen/qwen3.6-max-preview".to_string(),
324                provider: ProviderKind::Openrouter,
325                aliases: vec![
326                    "qwen3.6-max-preview".to_string(),
327                    "qwen-3.6-max-preview".to_string(),
328                    "qwen-max-preview".to_string(),
329                ],
330                supports_tools: true,
331                supports_reasoning: true,
332            },
333            ModelInfo {
334                id: "qwen/qwen3.6-27b".to_string(),
335                provider: ProviderKind::Openrouter,
336                aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
337                supports_tools: true,
338                supports_reasoning: true,
339            },
340            ModelInfo {
341                id: "qwen/qwen3.6-plus".to_string(),
342                provider: ProviderKind::Openrouter,
343                aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()],
344                supports_tools: true,
345                supports_reasoning: true,
346            },
347            ModelInfo {
348                id: "qwen/qwen3.7-plus".to_string(),
349                provider: ProviderKind::Openrouter,
350                aliases: vec!["qwen3.7-plus".to_string(), "qwen-3.7-plus".to_string()],
351                supports_tools: true,
352                supports_reasoning: true,
353            },
354            ModelInfo {
355                id: "moonshotai/kimi-k2.7-code".to_string(),
356                provider: ProviderKind::Openrouter,
357                aliases: vec![
358                    "kimi-k2.7-code".to_string(),
359                    "openrouter-kimi-k2.7-code".to_string(),
360                ],
361                supports_tools: true,
362                supports_reasoning: true,
363            },
364            ModelInfo {
365                id: "moonshotai/kimi-k2.6".to_string(),
366                provider: ProviderKind::Openrouter,
367                aliases: vec!["openrouter-kimi-k2.6".to_string()],
368                supports_tools: true,
369                supports_reasoning: true,
370            },
371            ModelInfo {
372                id: "minimax/minimax-m3".to_string(),
373                provider: ProviderKind::Openrouter,
374                aliases: vec![
375                    "minimax-m3".to_string(),
376                    "minimax-m-3".to_string(),
377                    "openrouter-minimax-m3".to_string(),
378                ],
379                supports_tools: true,
380                supports_reasoning: true,
381            },
382            ModelInfo {
383                id: "z-ai/glm-5.1".to_string(),
384                provider: ProviderKind::Openrouter,
385                aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
386                supports_tools: true,
387                supports_reasoning: true,
388            },
389            ModelInfo {
390                id: "z-ai/glm-5.2".to_string(),
391                provider: ProviderKind::Openrouter,
392                aliases: vec!["glm-5.2".to_string(), "zai-glm-5.2".to_string()],
393                supports_tools: true,
394                supports_reasoning: true,
395            },
396            ModelInfo {
397                id: "z-ai/glm-5-turbo".to_string(),
398                provider: ProviderKind::Openrouter,
399                aliases: vec!["glm-5-turbo".to_string(), "zai-glm-5-turbo".to_string()],
400                supports_tools: true,
401                supports_reasoning: true,
402            },
403            ModelInfo {
404                id: "GLM-5.2".to_string(),
405                provider: ProviderKind::Zai,
406                aliases: vec![
407                    "glm-5.2".to_string(),
408                    "glm-5-2".to_string(),
409                    "zai-glm-5.2".to_string(),
410                    "zai-glm-5-2".to_string(),
411                ],
412                supports_tools: true,
413                supports_reasoning: true,
414            },
415            ModelInfo {
416                id: "GLM-5.1".to_string(),
417                provider: ProviderKind::Zai,
418                aliases: vec![
419                    "glm-5.1".to_string(),
420                    "glm-5-1".to_string(),
421                    "zai-glm-5.1".to_string(),
422                    "zai-glm-5-1".to_string(),
423                ],
424                supports_tools: true,
425                supports_reasoning: true,
426            },
427            ModelInfo {
428                id: "GLM-5-Turbo".to_string(),
429                provider: ProviderKind::Zai,
430                aliases: vec![
431                    "glm-5-turbo".to_string(),
432                    "glm-5turbo".to_string(),
433                    "zai-glm-5-turbo".to_string(),
434                ],
435                supports_tools: true,
436                supports_reasoning: true,
437            },
438            ModelInfo {
439                id: "tencent/hy3-preview".to_string(),
440                provider: ProviderKind::Openrouter,
441                aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
442                supports_tools: true,
443                supports_reasoning: true,
444            },
445            ModelInfo {
446                id: "google/gemma-4-31b-it".to_string(),
447                provider: ProviderKind::Openrouter,
448                aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
449                supports_tools: true,
450                supports_reasoning: true,
451            },
452            ModelInfo {
453                id: "google/gemma-4-26b-a4b-it".to_string(),
454                provider: ProviderKind::Openrouter,
455                aliases: vec![
456                    "gemma-4-26b-a4b".to_string(),
457                    "gemma-4-26b-a4b-it".to_string(),
458                ],
459                supports_tools: true,
460                supports_reasoning: true,
461            },
462            ModelInfo {
463                id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
464                provider: ProviderKind::Openrouter,
465                aliases: vec![
466                    "nemotron-3-nano-omni".to_string(),
467                    "nemotron-3-nano-omni-reasoning".to_string(),
468                ],
469                supports_tools: true,
470                supports_reasoning: true,
471            },
472            ModelInfo {
473                id: "mimo-v2.5-pro".to_string(),
474                provider: ProviderKind::XiaomiMimo,
475                aliases: vec![
476                    "mimo".to_string(),
477                    "pro".to_string(),
478                    "xiaomi-mimo-v2.5-pro".to_string(),
479                    "xiaomi-mimo-v2-5-pro".to_string(),
480                ],
481                supports_tools: true,
482                supports_reasoning: true,
483            },
484            ModelInfo {
485                id: "mimo-v2.5".to_string(),
486                provider: ProviderKind::XiaomiMimo,
487                aliases: vec![
488                    "omni".to_string(),
489                    "mimo-omni".to_string(),
490                    "v2.5-omni".to_string(),
491                    "mimo-v2.5-omni".to_string(),
492                    "xiaomi-mimo-v2.5".to_string(),
493                    "xiaomi-mimo-v2.5-omni".to_string(),
494                ],
495                supports_tools: true,
496                supports_reasoning: true,
497            },
498            ModelInfo {
499                id: "mimo-v2.5-asr".to_string(),
500                provider: ProviderKind::XiaomiMimo,
501                aliases: vec![
502                    "asr".to_string(),
503                    "speech-to-text".to_string(),
504                    "transcribe".to_string(),
505                ],
506                supports_tools: false,
507                supports_reasoning: false,
508            },
509            ModelInfo {
510                id: "mimo-v2.5-tts".to_string(),
511                provider: ProviderKind::XiaomiMimo,
512                aliases: vec![
513                    "tts".to_string(),
514                    "speech".to_string(),
515                    "mimo-tts".to_string(),
516                ],
517                supports_tools: false,
518                supports_reasoning: false,
519            },
520            ModelInfo {
521                id: "mimo-v2.5-tts-voicedesign".to_string(),
522                provider: ProviderKind::XiaomiMimo,
523                aliases: vec![
524                    "voicedesign".to_string(),
525                    "voice-design".to_string(),
526                    "mimo-voice-design".to_string(),
527                ],
528                supports_tools: false,
529                supports_reasoning: false,
530            },
531            ModelInfo {
532                id: "mimo-v2.5-tts-voiceclone".to_string(),
533                provider: ProviderKind::XiaomiMimo,
534                aliases: vec![
535                    "voiceclone".to_string(),
536                    "voice-clone".to_string(),
537                    "mimo-voice-clone".to_string(),
538                ],
539                supports_tools: false,
540                supports_reasoning: false,
541            },
542            ModelInfo {
543                id: "mimo-v2-tts".to_string(),
544                provider: ProviderKind::XiaomiMimo,
545                aliases: vec!["mimo-v2-speech".to_string()],
546                supports_tools: false,
547                supports_reasoning: false,
548            },
549            ModelInfo {
550                id: "deepseek/deepseek-v4-pro".to_string(),
551                provider: ProviderKind::Novita,
552                aliases: vec![
553                    "deepseek-v4-pro".to_string(),
554                    "novita-deepseek-v4-pro".to_string(),
555                ],
556                supports_tools: true,
557                supports_reasoning: true,
558            },
559            ModelInfo {
560                id: "deepseek/deepseek-v4-flash".to_string(),
561                provider: ProviderKind::Novita,
562                aliases: vec![
563                    "deepseek-v4-flash".to_string(),
564                    "deepseek-chat".to_string(),
565                    "deepseek-reasoner".to_string(),
566                    "novita-deepseek-v4-flash".to_string(),
567                ],
568                supports_tools: true,
569                supports_reasoning: true,
570            },
571            ModelInfo {
572                id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
573                provider: ProviderKind::Fireworks,
574                aliases: vec![
575                    "deepseek-v4-pro".to_string(),
576                    "fireworks-deepseek-v4-pro".to_string(),
577                ],
578                supports_tools: true,
579                supports_reasoning: true,
580            },
581            ModelInfo {
582                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
583                provider: ProviderKind::Siliconflow,
584                aliases: vec![
585                    "deepseek-v4-pro".to_string(),
586                    "deepseek-reasoner".to_string(),
587                    "deepseek-r1".to_string(),
588                    "siliconflow-deepseek-v4-pro".to_string(),
589                ],
590                supports_tools: true,
591                supports_reasoning: true,
592            },
593            ModelInfo {
594                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
595                provider: ProviderKind::Siliconflow,
596                aliases: vec![
597                    "deepseek-v4-flash".to_string(),
598                    "deepseek-chat".to_string(),
599                    "deepseek-v3".to_string(),
600                    "siliconflow-deepseek-v4-flash".to_string(),
601                ],
602                supports_tools: true,
603                supports_reasoning: true,
604            },
605            ModelInfo {
606                id: "trinity-large-preview".to_string(),
607                provider: ProviderKind::Arcee,
608                aliases: vec!["arcee-trinity-large-preview".to_string()],
609                supports_tools: true,
610                supports_reasoning: false,
611            },
612            ModelInfo {
613                id: "kimi-k2.7-code".to_string(),
614                provider: ProviderKind::Moonshot,
615                aliases: vec![
616                    "kimi".to_string(),
617                    "kimi-k2".to_string(),
618                    "kimi-k2.7".to_string(),
619                    "kimi-code".to_string(),
620                    "moonshot-kimi-k2.7-code".to_string(),
621                ],
622                supports_tools: true,
623                supports_reasoning: true,
624            },
625            ModelInfo {
626                id: "kimi-k2.6".to_string(),
627                provider: ProviderKind::Moonshot,
628                aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
629                supports_tools: true,
630                supports_reasoning: true,
631            },
632            ModelInfo {
633                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
634                provider: ProviderKind::Sglang,
635                aliases: vec![
636                    "deepseek-v4-pro".to_string(),
637                    "sglang-deepseek-v4-pro".to_string(),
638                ],
639                supports_tools: true,
640                supports_reasoning: true,
641            },
642            ModelInfo {
643                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
644                provider: ProviderKind::Sglang,
645                aliases: vec![
646                    "deepseek-v4-flash".to_string(),
647                    "deepseek-chat".to_string(),
648                    "deepseek-reasoner".to_string(),
649                    "sglang-deepseek-v4-flash".to_string(),
650                ],
651                supports_tools: true,
652                supports_reasoning: true,
653            },
654            ModelInfo {
655                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
656                provider: ProviderKind::Vllm,
657                aliases: vec![
658                    "deepseek-v4-pro".to_string(),
659                    "vllm-deepseek-v4-pro".to_string(),
660                ],
661                supports_tools: true,
662                supports_reasoning: true,
663            },
664            ModelInfo {
665                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
666                provider: ProviderKind::Vllm,
667                aliases: vec![
668                    "deepseek-v4-flash".to_string(),
669                    "deepseek-chat".to_string(),
670                    "deepseek-reasoner".to_string(),
671                    "vllm-deepseek-v4-flash".to_string(),
672                ],
673                supports_tools: true,
674                supports_reasoning: true,
675            },
676            ModelInfo {
677                id: "deepseek-v4-flash".to_string(),
678                provider: ProviderKind::Ollama,
679                aliases: vec![],
680                supports_tools: true,
681                supports_reasoning: true,
682            },
683            ModelInfo {
684                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
685                provider: ProviderKind::Huggingface,
686                aliases: vec![
687                    "deepseek-v4-pro".to_string(),
688                    "hf-deepseek-v4-pro".to_string(),
689                ],
690                supports_tools: true,
691                supports_reasoning: true,
692            },
693            ModelInfo {
694                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
695                provider: ProviderKind::Huggingface,
696                aliases: vec![
697                    "deepseek-v4-flash".to_string(),
698                    "deepseek-chat".to_string(),
699                    "deepseek-reasoner".to_string(),
700                    "hf-deepseek-v4-flash".to_string(),
701                ],
702                supports_tools: true,
703                supports_reasoning: true,
704            },
705            // Together AI provider models
706            ModelInfo {
707                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
708                provider: ProviderKind::Together,
709                aliases: vec![
710                    "deepseek-v4-pro".to_string(),
711                    "together-deepseek-v4-pro".to_string(),
712                ],
713                supports_tools: true,
714                supports_reasoning: true,
715            },
716            ModelInfo {
717                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
718                provider: ProviderKind::Together,
719                aliases: vec![
720                    "deepseek-v4-flash".to_string(),
721                    "deepseek-chat".to_string(),
722                    "together-deepseek-v4-flash".to_string(),
723                ],
724                supports_tools: true,
725                supports_reasoning: true,
726            },
727            ModelInfo {
728                // Together's published hosted endpoint is lowercase even though
729                // the open-weight Hugging Face repository uses `Inkling`.
730                id: "thinkingmachines/inkling".to_string(),
731                provider: ProviderKind::Together,
732                aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
733                supports_tools: true,
734                supports_reasoning: true,
735            },
736            // Qwen 3.7 Max (OpenRouter)
737            ModelInfo {
738                id: "qwen/qwen3.7-max".to_string(),
739                provider: ProviderKind::Openrouter,
740                aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
741                supports_tools: true,
742                supports_reasoning: true,
743            },
744            // OpenAI Codex (ChatGPT OAuth) models
745            ModelInfo {
746                id: "gpt-5.5".to_string(),
747                provider: ProviderKind::OpenaiCodex,
748                aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
749                supports_tools: true,
750                supports_reasoning: true,
751            },
752            // Anthropic native Messages API models (#3014)
753            ModelInfo {
754                id: "claude-opus-4-8".to_string(),
755                provider: ProviderKind::Anthropic,
756                aliases: vec!["opus".to_string(), "claude-opus".to_string()],
757                supports_tools: true,
758                supports_reasoning: true,
759            },
760            ModelInfo {
761                id: "claude-sonnet-4-6".to_string(),
762                provider: ProviderKind::Anthropic,
763                aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
764                supports_tools: true,
765                supports_reasoning: true,
766            },
767            ModelInfo {
768                id: "claude-haiku-4-5".to_string(),
769                provider: ProviderKind::Anthropic,
770                aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
771                supports_tools: true,
772                supports_reasoning: false,
773            },
774            ModelInfo {
775                id: "claude-sonnet-5".to_string(),
776                provider: ProviderKind::Anthropic,
777                aliases: vec!["sonnet-5".to_string()],
778                supports_tools: true,
779                supports_reasoning: true,
780            },
781            ModelInfo {
782                id: "claude-fable-5".to_string(),
783                provider: ProviderKind::Anthropic,
784                aliases: vec!["fable".to_string(), "fable-5".to_string()],
785                supports_tools: true,
786                supports_reasoning: true,
787            },
788            // OpenModel Anthropic-compatible Messages route
789            ModelInfo {
790                id: "deepseek-v4-flash".to_string(),
791                provider: ProviderKind::Openmodel,
792                aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
793                supports_tools: true,
794                supports_reasoning: true,
795            },
796            // MiniMax 2.7 (OpenRouter)
797            ModelInfo {
798                id: "minimax/minimax-m2.7".to_string(),
799                provider: ProviderKind::Openrouter,
800                aliases: vec![
801                    "minimax-2.7".to_string(),
802                    "minimax-2-7".to_string(),
803                    "openrouter-minimax-2.7".to_string(),
804                ],
805                supports_tools: true,
806                supports_reasoning: true,
807            },
808            ModelInfo {
809                id: "step-3.7-flash".to_string(),
810                provider: ProviderKind::Stepfun,
811                aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
812                supports_tools: true,
813                supports_reasoning: false,
814            },
815            ModelInfo {
816                id: "MiniMax-M3".to_string(),
817                provider: ProviderKind::Minimax,
818                aliases: vec![
819                    "minimax".to_string(),
820                    "minimax-m3".to_string(),
821                    "minimax-m-3".to_string(),
822                ],
823                supports_tools: true,
824                supports_reasoning: true,
825            },
826            ModelInfo {
827                id: "MiniMax-M2.7".to_string(),
828                provider: ProviderKind::Minimax,
829                aliases: vec![
830                    "minimax-m2.7".to_string(),
831                    "minimax-m2-7".to_string(),
832                    "minimax-m-2.7".to_string(),
833                    "minimax-m-2-7".to_string(),
834                ],
835                supports_tools: true,
836                supports_reasoning: true,
837            },
838            ModelInfo {
839                id: "MiniMax-M3".to_string(),
840                provider: ProviderKind::MinimaxAnthropic,
841                aliases: vec![
842                    "minimax-anthropic".to_string(),
843                    "minimax-anthropic-m3".to_string(),
844                    "minimax-m3".to_string(),
845                ],
846                supports_tools: true,
847                supports_reasoning: true,
848            },
849            ModelInfo {
850                id: "MiniMax-M2.7".to_string(),
851                provider: ProviderKind::MinimaxAnthropic,
852                aliases: vec![
853                    "minimax-anthropic-m2.7".to_string(),
854                    "minimax-anthropic-m2-7".to_string(),
855                    "minimax-m2.7".to_string(),
856                ],
857                supports_tools: true,
858                supports_reasoning: true,
859            },
860            ModelInfo {
861                id: "MiniMax-M2.7-highspeed".to_string(),
862                provider: ProviderKind::Minimax,
863                aliases: vec![
864                    "minimax-m2.7-highspeed".to_string(),
865                    "minimax-m2-7-highspeed".to_string(),
866                    "minimax-m-2.7-highspeed".to_string(),
867                    "minimax-m-2-7-highspeed".to_string(),
868                ],
869                supports_tools: true,
870                supports_reasoning: true,
871            },
872            ModelInfo {
873                id: "MiniMax-M2.5".to_string(),
874                provider: ProviderKind::Minimax,
875                aliases: vec![
876                    "minimax-m2.5".to_string(),
877                    "minimax-m2-5".to_string(),
878                    "minimax-m-2.5".to_string(),
879                    "minimax-m-2-5".to_string(),
880                ],
881                supports_tools: true,
882                supports_reasoning: true,
883            },
884            ModelInfo {
885                id: "MiniMax-M2.5-highspeed".to_string(),
886                provider: ProviderKind::Minimax,
887                aliases: vec![
888                    "minimax-m2.5-highspeed".to_string(),
889                    "minimax-m2-5-highspeed".to_string(),
890                    "minimax-m-2.5-highspeed".to_string(),
891                    "minimax-m-2-5-highspeed".to_string(),
892                ],
893                supports_tools: true,
894                supports_reasoning: true,
895            },
896            ModelInfo {
897                id: "MiniMax-M2.1".to_string(),
898                provider: ProviderKind::Minimax,
899                aliases: vec![
900                    "minimax-m2.1".to_string(),
901                    "minimax-m2-1".to_string(),
902                    "minimax-m-2.1".to_string(),
903                    "minimax-m-2-1".to_string(),
904                ],
905                supports_tools: true,
906                supports_reasoning: true,
907            },
908            ModelInfo {
909                id: "MiniMax-M2.1-highspeed".to_string(),
910                provider: ProviderKind::Minimax,
911                aliases: vec![
912                    "minimax-m2.1-highspeed".to_string(),
913                    "minimax-m2-1-highspeed".to_string(),
914                    "minimax-m-2.1-highspeed".to_string(),
915                    "minimax-m-2-1-highspeed".to_string(),
916                ],
917                supports_tools: true,
918                supports_reasoning: true,
919            },
920            ModelInfo {
921                id: "MiniMax-M2".to_string(),
922                provider: ProviderKind::Minimax,
923                aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
924                supports_tools: true,
925                supports_reasoning: true,
926            },
927            // NVIDIA Nemotron 3 Ultra (OpenRouter)
928            ModelInfo {
929                id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
930                provider: ProviderKind::Openrouter,
931                aliases: vec![
932                    "nvidia/nemotron-3-ultra".to_string(),
933                    "nemotron-3-ultra".to_string(),
934                    "nemotron-3-ultra-550b-a55b".to_string(),
935                    "nvidia-nemotron-3-ultra".to_string(),
936                    "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
937                ],
938                supports_tools: true,
939                supports_reasoning: true,
940            },
941            // DeepInfra (https://deepinfra.com)
942            ModelInfo {
943                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
944                provider: ProviderKind::Deepinfra,
945                aliases: vec![
946                    "deepseek-v4-pro".to_string(),
947                    "di-deepseek-v4-pro".to_string(),
948                ],
949                supports_tools: true,
950                supports_reasoning: true,
951            },
952            ModelInfo {
953                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
954                provider: ProviderKind::Deepinfra,
955                aliases: vec![
956                    "deepseek-v4-flash".to_string(),
957                    "di-deepseek-v4-flash".to_string(),
958                ],
959                supports_tools: true,
960                supports_reasoning: true,
961            },
962            // Sakana AI Fugu (https://api.sakana.ai)
963            ModelInfo {
964                id: "fugu".to_string(),
965                provider: ProviderKind::Sakana,
966                aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
967                supports_tools: true,
968                supports_reasoning: false,
969            },
970            ModelInfo {
971                id: "fugu-ultra-20260615".to_string(),
972                provider: ProviderKind::Sakana,
973                aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
974                supports_tools: true,
975                supports_reasoning: true,
976            },
977            // Meituan LongCat (https://longcat.chat/platform)
978            ModelInfo {
979                id: "LongCat-2.0".to_string(),
980                provider: ProviderKind::LongCat,
981                aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
982                supports_tools: true,
983                supports_reasoning: true,
984            },
985            // OpenCode Go Chat Completions models (https://opencode.ai/docs/go/).
986            // Go models documented only on `/messages` are intentionally not
987            // advertised by this OpenAI-compatible provider slice.
988            ModelInfo {
989                id: "deepseek-v4-pro".to_string(),
990                provider: ProviderKind::OpencodeGo,
991                aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
992                supports_tools: true,
993                supports_reasoning: true,
994            },
995            ModelInfo {
996                id: "grok-4.5".to_string(),
997                provider: ProviderKind::OpencodeGo,
998                aliases: vec!["opencode-go/grok-4.5".to_string()],
999                supports_tools: true,
1000                supports_reasoning: true,
1001            },
1002            ModelInfo {
1003                id: "glm-5.2".to_string(),
1004                provider: ProviderKind::OpencodeGo,
1005                aliases: vec!["opencode-go/glm-5.2".to_string()],
1006                supports_tools: true,
1007                supports_reasoning: true,
1008            },
1009            ModelInfo {
1010                id: "glm-5.1".to_string(),
1011                provider: ProviderKind::OpencodeGo,
1012                aliases: vec!["opencode-go/glm-5.1".to_string()],
1013                supports_tools: true,
1014                supports_reasoning: true,
1015            },
1016            ModelInfo {
1017                id: "kimi-k3".to_string(),
1018                provider: ProviderKind::OpencodeGo,
1019                aliases: vec!["opencode-go/kimi-k3".to_string()],
1020                supports_tools: true,
1021                supports_reasoning: true,
1022            },
1023            ModelInfo {
1024                id: "kimi-k2.7-code".to_string(),
1025                provider: ProviderKind::OpencodeGo,
1026                aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1027                supports_tools: true,
1028                supports_reasoning: true,
1029            },
1030            ModelInfo {
1031                id: "kimi-k2.6".to_string(),
1032                provider: ProviderKind::OpencodeGo,
1033                aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1034                supports_tools: true,
1035                supports_reasoning: true,
1036            },
1037            ModelInfo {
1038                id: "deepseek-v4-flash".to_string(),
1039                provider: ProviderKind::OpencodeGo,
1040                aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1041                supports_tools: true,
1042                supports_reasoning: true,
1043            },
1044            ModelInfo {
1045                id: "mimo-v2.5".to_string(),
1046                provider: ProviderKind::OpencodeGo,
1047                aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1048                supports_tools: true,
1049                supports_reasoning: true,
1050            },
1051            ModelInfo {
1052                id: "mimo-v2.5-pro".to_string(),
1053                provider: ProviderKind::OpencodeGo,
1054                aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1055                supports_tools: true,
1056                supports_reasoning: true,
1057            },
1058            // Meta Model API / Muse Spark.
1059            ModelInfo {
1060                id: "muse-spark-1.1".to_string(),
1061                provider: ProviderKind::Meta,
1062                aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1063                supports_tools: true,
1064                supports_reasoning: true,
1065            },
1066            // xAI / Grok (https://api.x.ai/v1)
1067            ModelInfo {
1068                id: "grok-4.5".to_string(),
1069                provider: ProviderKind::Xai,
1070                aliases: vec!["grok".to_string(), "xai-grok-4.5".to_string()],
1071                supports_tools: true,
1072                supports_reasoning: true,
1073            },
1074            ModelInfo {
1075                id: "grok-4.3".to_string(),
1076                provider: ProviderKind::Xai,
1077                aliases: vec!["xai-grok-4.3".to_string()],
1078                supports_tools: true,
1079                supports_reasoning: true,
1080            },
1081            ModelInfo {
1082                id: "grok-build".to_string(),
1083                provider: ProviderKind::Xai,
1084                aliases: vec!["xai-grok-build".to_string()],
1085                supports_tools: true,
1086                supports_reasoning: true,
1087            },
1088            ModelInfo {
1089                id: "grok-composer-2.5-fast".to_string(),
1090                provider: ProviderKind::Xai,
1091                aliases: vec!["xai-grok-composer".to_string()],
1092                supports_tools: true,
1093                supports_reasoning: false,
1094            },
1095            ModelInfo {
1096                id: "grok-4.20-0309-reasoning".to_string(),
1097                provider: ProviderKind::Xai,
1098                aliases: vec!["xai-grok-reasoning".to_string()],
1099                supports_tools: true,
1100                supports_reasoning: true,
1101            },
1102            ModelInfo {
1103                id: "grok-4.20-0309-non-reasoning".to_string(),
1104                provider: ProviderKind::Xai,
1105                aliases: vec!["xai-grok-fast".to_string()],
1106                supports_tools: true,
1107                supports_reasoning: false,
1108            },
1109        ];
1110        Self::new(models)
1111    }
1112}
1113
1114impl ModelRegistry {
1115    /// Creates a new registry from a list of [`ModelInfo`] entries.
1116    ///
1117    /// Builds an internal alias map for fast lookup by model id or alias.
1118    /// If multiple models share the same id or alias, the first one registered
1119    /// takes priority.
1120    #[must_use]
1121    pub fn new(models: Vec<ModelInfo>) -> Self {
1122        let mut alias_map = HashMap::new();
1123        for (idx, model) in models.iter().enumerate() {
1124            alias_map.entry(normalize(&model.id)).or_insert(idx);
1125            for alias in &model.aliases {
1126                alias_map.entry(normalize(alias)).or_insert(idx);
1127            }
1128        }
1129        Self { models, alias_map }
1130    }
1131
1132    /// Returns a clone of all models in the registry.
1133    #[must_use]
1134    pub fn list(&self) -> Vec<ModelInfo> {
1135        self.models.clone()
1136    }
1137
1138    /// Resolves a user-requested model name to a concrete [`ModelInfo`].
1139    ///
1140    /// Resolution follows this priority order:
1141    /// 1. If the provider is Ollama, the requested name is used as-is (to
1142    ///    support arbitrary local model tags like `qwen2.5-coder:7b`).
1143    /// 2. If a `provider_hint` is given, search for a model matching that
1144    ///    provider whose id or alias matches the request (case-insensitive).
1145    /// 3. Look up the alias map for a case-insensitive match.
1146    /// 4. Fall back to the first model belonging to the hinted provider
1147    ///    (or DeepSeek if no hint was given).
1148    /// 5. As a last resort, fall back to the first model in the registry.
1149    #[must_use]
1150    pub fn resolve(
1151        &self,
1152        requested: Option<&str>,
1153        provider_hint: Option<ProviderKind>,
1154    ) -> ModelResolution {
1155        let mut fallback_chain = Vec::new();
1156
1157        if let Some(name) = requested {
1158            fallback_chain.push(format!("requested:{name}"));
1159            if provider_hint == Some(ProviderKind::Ollama) {
1160                return ModelResolution {
1161                    requested: Some(name.to_string()),
1162                    resolved: ModelInfo {
1163                        id: name.trim().to_string(),
1164                        provider: ProviderKind::Ollama,
1165                        aliases: Vec::new(),
1166                        supports_tools: true,
1167                        supports_reasoning: false,
1168                    },
1169                    used_fallback: false,
1170                    fallback_chain,
1171                };
1172            }
1173            // OpenCode Go's catalog spans Chat Completions and Anthropic
1174            // Messages, while Codewhale's provider slice intentionally speaks
1175            // Chat only. Resolve a hinted Go model through the shared Chat
1176            // allowlist and never fall through to a same-named global alias on
1177            // OpenRouter or MiniMax.
1178            if provider_hint == Some(ProviderKind::OpencodeGo)
1179                && let Some(canonical) = opencode_go_chat_model_id(name)
1180                && let Some(model) = self
1181                    .models
1182                    .iter()
1183                    .find(|model| {
1184                        model.provider == ProviderKind::OpencodeGo
1185                            && model.id.eq_ignore_ascii_case(canonical)
1186                    })
1187                    .cloned()
1188            {
1189                return ModelResolution {
1190                    requested: Some(name.to_string()),
1191                    resolved: model,
1192                    used_fallback: false,
1193                    fallback_chain,
1194                };
1195            }
1196            if provider_hint != Some(ProviderKind::OpencodeGo)
1197                && let Some(provider) = provider_hint
1198                && let Some(model) = self
1199                    .models
1200                    .iter()
1201                    .find(|m| m.provider == provider && model_matches(m, name))
1202                    .cloned()
1203            {
1204                return ModelResolution {
1205                    requested: Some(name.to_string()),
1206                    resolved: model,
1207                    used_fallback: false,
1208                    fallback_chain,
1209                };
1210            }
1211            if provider_hint == Some(ProviderKind::Atlascloud)
1212                && let Some(model) = atlascloud_passthrough_model(name)
1213            {
1214                return ModelResolution {
1215                    requested: Some(name.to_string()),
1216                    resolved: model,
1217                    used_fallback: false,
1218                    fallback_chain,
1219                };
1220            }
1221            if provider_hint == Some(ProviderKind::Arcee)
1222                && let Some(model) = arcee_passthrough_model(name)
1223            {
1224                return ModelResolution {
1225                    requested: Some(name.to_string()),
1226                    resolved: model,
1227                    used_fallback: false,
1228                    fallback_chain,
1229                };
1230            }
1231            if provider_hint == Some(ProviderKind::XiaomiMimo)
1232                && let Some(model) = xiaomi_mimo_passthrough_model(name)
1233            {
1234                return ModelResolution {
1235                    requested: Some(name.to_string()),
1236                    resolved: model,
1237                    used_fallback: false,
1238                    fallback_chain,
1239                };
1240            }
1241            if provider_hint != Some(ProviderKind::OpencodeGo)
1242                && let Some(idx) = self.alias_map.get(&normalize(name))
1243            {
1244                return ModelResolution {
1245                    requested: Some(name.to_string()),
1246                    resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1247                    used_fallback: false,
1248                    fallback_chain,
1249                };
1250            }
1251        }
1252
1253        let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1254        fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1255        if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1256            return ModelResolution {
1257                requested: requested.map(ToOwned::to_owned),
1258                resolved: model,
1259                used_fallback: true,
1260                fallback_chain,
1261            };
1262        }
1263
1264        let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1265            id: "deepseek-v4-pro".to_string(),
1266            provider: ProviderKind::Deepseek,
1267            aliases: Vec::new(),
1268            supports_tools: true,
1269            supports_reasoning: true,
1270        });
1271        fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1272        ModelResolution {
1273            requested: requested.map(ToOwned::to_owned),
1274            resolved: final_fallback,
1275            used_fallback: true,
1276            fallback_chain,
1277        }
1278    }
1279}
1280
1281fn normalize(value: &str) -> String {
1282    value.trim().to_ascii_lowercase()
1283}
1284
1285#[must_use]
1286/// Classify a model identifier by its underlying model family.
1287pub fn model_family(model_id: &str) -> ModelFamily {
1288    let normalized = normalize(model_id);
1289    if normalized.is_empty() {
1290        return ModelFamily::Inferencer;
1291    }
1292
1293    if normalized.contains("deepseek") {
1294        return ModelFamily::DeepSeek;
1295    }
1296    if normalized.contains("claude") || normalized.contains("anthropic") {
1297        return ModelFamily::Anthropic;
1298    }
1299    if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1300        return ModelFamily::GptOss;
1301    }
1302    if normalized.starts_with("gpt-")
1303        || normalized.contains("/gpt-")
1304        || normalized.contains("openai/")
1305    {
1306        return ModelFamily::OpenAI;
1307    }
1308    if normalized.contains("gemini")
1309        || normalized.contains("gemma")
1310        || normalized.contains("google/")
1311    {
1312        return ModelFamily::Google;
1313    }
1314    if normalized.contains("llama")
1315        || normalized.contains("muse-spark")
1316        || normalized.contains("meta-")
1317        || normalized.contains("meta/")
1318    {
1319        return ModelFamily::Meta;
1320    }
1321    if normalized.contains("mistral")
1322        || normalized.contains("mixtral")
1323        || normalized.contains("codestral")
1324    {
1325        return ModelFamily::Mistral;
1326    }
1327    if normalized.contains("qwen") {
1328        return ModelFamily::Qwen;
1329    }
1330    if normalized.contains("grok") {
1331        return ModelFamily::Grok;
1332    }
1333    if normalized.contains("cohere") || normalized.contains("command-r") {
1334        return ModelFamily::Cohere;
1335    }
1336
1337    ModelFamily::Inferencer
1338}
1339
1340fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1341    let requested = normalize(requested);
1342    normalize(&model.id) == requested
1343        || model
1344            .aliases
1345            .iter()
1346            .any(|alias| normalize(alias) == requested)
1347}
1348
1349fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1350    let requested = requested.trim();
1351    if model.id.eq_ignore_ascii_case(requested) {
1352        model.id = requested.to_string();
1353    }
1354    model
1355}
1356
1357fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1358    let requested = requested.trim();
1359    if requested.is_empty() || !requested.contains('/') {
1360        return None;
1361    }
1362
1363    Some(ModelInfo {
1364        id: requested.to_string(),
1365        provider: ProviderKind::Atlascloud,
1366        aliases: Vec::new(),
1367        supports_tools: true,
1368        supports_reasoning: true,
1369    })
1370}
1371
1372fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1373    let requested = requested.trim();
1374    if requested.is_empty() {
1375        return None;
1376    }
1377    let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1378
1379    Some(ModelInfo {
1380        id: requested.to_string(),
1381        provider: ProviderKind::Arcee,
1382        aliases: Vec::new(),
1383        supports_tools: true,
1384        supports_reasoning,
1385    })
1386}
1387
1388fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1389    let requested = requested.trim();
1390    if requested.is_empty() || requested.chars().any(char::is_control) {
1391        return None;
1392    }
1393
1394    Some(ModelInfo {
1395        id: requested.to_string(),
1396        provider: ProviderKind::XiaomiMimo,
1397        aliases: Vec::new(),
1398        supports_tools: true,
1399        supports_reasoning: true,
1400    })
1401}
1402
1403#[cfg(test)]
1404mod tests {
1405    use super::*;
1406
1407    #[test]
1408    fn model_registry_new_builds_alias_map_correctly() {
1409        let models = vec![
1410            ModelInfo {
1411                id: "Model-A".to_string(),
1412                provider: ProviderKind::Deepseek,
1413                aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1414                supports_tools: true,
1415                supports_reasoning: false,
1416            },
1417            ModelInfo {
1418                id: "model-b".to_string(),
1419                provider: ProviderKind::Deepseek,
1420                aliases: vec!["alias-1".to_string()], // Duplicate alias, should not override
1421                supports_tools: true,
1422                supports_reasoning: true,
1423            },
1424        ];
1425
1426        let registry = ModelRegistry::new(models);
1427
1428        assert_eq!(registry.alias_map.len(), 4); // "model-a", "alias-1", "alias-2", "model-b"
1429        assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1430        assert_eq!(registry.alias_map.get("alias-1"), Some(&0)); // First one wins
1431        assert_eq!(registry.alias_map.get("alias-2"), Some(&0)); // Normalized
1432        assert_eq!(registry.alias_map.get("model-b"), Some(&1));
1433    }
1434
1435    #[test]
1436    fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1437        let registry = ModelRegistry::default();
1438        let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1439
1440        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1441        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1442    }
1443
1444    #[test]
1445    fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1446        let registry = ModelRegistry::default();
1447        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1448
1449        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1450        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1451    }
1452
1453    #[test]
1454    fn nvidia_nim_default_uses_catalog_model_id() {
1455        let registry = ModelRegistry::default();
1456        let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1457
1458        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1459        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1460    }
1461
1462    #[test]
1463    fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1464        let registry = ModelRegistry::default();
1465        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1466
1467        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1468        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1469    }
1470
1471    #[test]
1472    fn atlascloud_default_uses_namespaced_model_id() {
1473        let registry = ModelRegistry::default();
1474        let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1475
1476        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1477        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1478        assert!(resolved.resolved.supports_reasoning);
1479    }
1480
1481    #[test]
1482    fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1483        let registry = ModelRegistry::default();
1484        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1485
1486        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1487        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1488    }
1489
1490    #[test]
1491    fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1492        let registry = ModelRegistry::default();
1493        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1494
1495        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1496        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1497    }
1498
1499    #[test]
1500    fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1501        let registry = ModelRegistry::default();
1502        let resolved =
1503            registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1504
1505        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1506        assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1507        assert!(resolved.resolved.supports_tools);
1508        assert!(resolved.resolved.supports_reasoning);
1509        assert!(!resolved.used_fallback);
1510    }
1511
1512    #[test]
1513    fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1514        let registry = ModelRegistry::default();
1515        let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1516
1517        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1518        assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1519        assert!(!resolved.used_fallback);
1520    }
1521
1522    #[test]
1523    fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1524        let registry = ModelRegistry::default();
1525        let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1526
1527        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1528        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1529        assert!(resolved.used_fallback);
1530    }
1531
1532    #[test]
1533    fn openrouter_default_uses_namespaced_model_id() {
1534        let registry = ModelRegistry::default();
1535        let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1536
1537        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1538        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1539    }
1540
1541    #[test]
1542    fn xiaomi_mimo_default_uses_canonical_model_id() {
1543        let registry = ModelRegistry::default();
1544        let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1545
1546        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1547        assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1548        assert!(resolved.resolved.supports_reasoning);
1549    }
1550
1551    #[test]
1552    fn moonshot_default_and_aliases_use_kimi_k27_code() {
1553        let registry = ModelRegistry::default();
1554
1555        for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1556            let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1557
1558            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1559            assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1560            assert!(resolved.resolved.supports_tools);
1561            assert!(resolved.resolved.supports_reasoning);
1562        }
1563    }
1564
1565    #[test]
1566    fn moonshot_explicit_kimi_k26_remains_available() {
1567        let registry = ModelRegistry::default();
1568        let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1569
1570        assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1571        assert_eq!(resolved.resolved.id, "kimi-k2.6");
1572        assert!(resolved.resolved.supports_reasoning);
1573    }
1574
1575    #[test]
1576    fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1577        let registry = ModelRegistry::default();
1578        let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1579        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1580        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1581        assert!(!resolved.resolved.supports_tools);
1582        assert!(!resolved.resolved.supports_reasoning);
1583
1584        let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1585        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1586
1587        let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1588        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1589    }
1590
1591    #[test]
1592    fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1593        let registry = ModelRegistry::default();
1594
1595        let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1596        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1597        assert_eq!(resolved.resolved.id, "mimo-v2.5");
1598        assert!(resolved.resolved.supports_tools);
1599    }
1600
1601    #[test]
1602    fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1603        let registry = ModelRegistry::default();
1604        let resolved =
1605            registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1606
1607        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1608        assert_eq!(resolved.resolved.id, "account-custom-mimo");
1609        assert!(!resolved.used_fallback);
1610    }
1611
1612    #[test]
1613    fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1614        let registry = ModelRegistry::default();
1615        let resolved = registry.resolve(
1616            Some("deepseek/deepseek-v4-pro"),
1617            Some(ProviderKind::XiaomiMimo),
1618        );
1619
1620        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1621        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1622        assert!(!resolved.used_fallback);
1623    }
1624
1625    #[test]
1626    fn wanjie_ark_default_uses_reasoner_model_id() {
1627        let registry = ModelRegistry::default();
1628        let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1629
1630        assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1631        assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1632        assert!(resolved.resolved.supports_reasoning);
1633    }
1634
1635    #[test]
1636    fn novita_default_uses_namespaced_model_id() {
1637        let registry = ModelRegistry::default();
1638        let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1639
1640        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1641        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1642    }
1643
1644    #[test]
1645    fn fireworks_default_uses_canonical_model_id() {
1646        let registry = ModelRegistry::default();
1647        let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1648
1649        assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1650        assert_eq!(
1651            resolved.resolved.id,
1652            "accounts/fireworks/models/deepseek-v4-pro"
1653        );
1654    }
1655
1656    #[test]
1657    fn siliconflow_default_uses_canonical_pro_model_id() {
1658        let registry = ModelRegistry::default();
1659        let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1660
1661        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1662        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1663        assert!(resolved.resolved.supports_reasoning);
1664    }
1665
1666    #[test]
1667    fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1668        let registry = ModelRegistry::default();
1669        let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1670
1671        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1672        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1673        assert!(resolved.resolved.supports_reasoning);
1674    }
1675
1676    #[test]
1677    fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1678        let registry = ModelRegistry::default();
1679        let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1680
1681        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1682        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1683        assert!(resolved.resolved.supports_reasoning);
1684    }
1685
1686    #[test]
1687    fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1688        let registry = ModelRegistry::default();
1689        let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1690
1691        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1692        assert_eq!(resolved.resolved.id, "trinity-mini");
1693        assert!(resolved.resolved.supports_reasoning);
1694        assert!(!resolved.used_fallback);
1695    }
1696
1697    #[test]
1698    fn arcee_provider_hint_preserves_explicit_future_model_id() {
1699        let registry = ModelRegistry::default();
1700        let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1701
1702        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1703        assert_eq!(resolved.resolved.id, "trinity-large-next");
1704        assert!(!resolved.resolved.supports_reasoning);
1705        assert!(!resolved.used_fallback);
1706    }
1707
1708    #[test]
1709    fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1710        let registry = ModelRegistry::default();
1711        let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1712
1713        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1714        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1715    }
1716
1717    #[test]
1718    fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1719        let registry = ModelRegistry::default();
1720        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1721
1722        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1723        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1724    }
1725
1726    #[test]
1727    fn sglang_default_uses_canonical_model_id() {
1728        let registry = ModelRegistry::default();
1729        let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1730
1731        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1732        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1733    }
1734
1735    #[test]
1736    fn zai_direct_models_resolve_when_provider_hinted() {
1737        let registry = ModelRegistry::default();
1738
1739        // GLM-5.2 is now the default direct Z.AI model.
1740        let default = registry.resolve(None, Some(ProviderKind::Zai));
1741        assert_eq!(default.resolved.provider, ProviderKind::Zai);
1742        assert_eq!(default.resolved.id, "GLM-5.2");
1743
1744        for (alias, expected) in [
1745            ("GLM-5.1", "GLM-5.1"),
1746            ("glm-5-1", "GLM-5.1"),
1747            ("GLM-5.2", "GLM-5.2"),
1748            ("glm-5.2", "GLM-5.2"),
1749            ("zai-glm-5-2", "GLM-5.2"),
1750            ("GLM-5-Turbo", "GLM-5-Turbo"),
1751            ("glm-5-turbo", "GLM-5-Turbo"),
1752            ("zai-glm-5-turbo", "GLM-5-Turbo"),
1753        ] {
1754            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1755
1756            assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1757            assert_eq!(resolved.resolved.id, expected);
1758            assert!(!resolved.used_fallback);
1759            assert!(resolved.resolved.supports_tools);
1760            assert!(resolved.resolved.supports_reasoning);
1761        }
1762    }
1763
1764    #[test]
1765    fn first_party_recent_provider_models_are_listed() {
1766        let registry = ModelRegistry::default();
1767        let models = registry.list();
1768
1769        for (provider, id) in [
1770            (ProviderKind::Zai, "GLM-5.2"),
1771            (ProviderKind::Stepfun, "step-3.7-flash"),
1772            (ProviderKind::Minimax, "MiniMax-M2.1"),
1773            (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
1774            (ProviderKind::Openmodel, "deepseek-v4-flash"),
1775            (ProviderKind::Meta, "muse-spark-1.1"),
1776            (ProviderKind::Xai, "grok-4.5"),
1777        ] {
1778            assert!(
1779                models
1780                    .iter()
1781                    .any(|model| model.provider == provider && model.id == id),
1782                "expected {provider:?} model {id} in registry"
1783            );
1784        }
1785    }
1786
1787    #[test]
1788    fn opencode_go_lists_only_current_chat_completions_models() {
1789        let registry = ModelRegistry::default();
1790        let listed = registry.list();
1791        let models: Vec<&str> = listed
1792            .iter()
1793            .filter(|model| model.provider == ProviderKind::OpencodeGo)
1794            .map(|model| model.id.as_str())
1795            .collect();
1796
1797        assert_eq!(
1798            models,
1799            vec![
1800                "deepseek-v4-pro",
1801                "grok-4.5",
1802                "glm-5.2",
1803                "glm-5.1",
1804                "kimi-k3",
1805                "kimi-k2.7-code",
1806                "kimi-k2.6",
1807                "deepseek-v4-flash",
1808                "mimo-v2.5",
1809                "mimo-v2.5-pro",
1810            ]
1811        );
1812
1813        let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
1814        assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
1815        assert_eq!(default.resolved.id, "deepseek-v4-pro");
1816
1817        for model in ["grok-4.5", "kimi-k3"] {
1818            for requested in [model.to_string(), format!("opencode-go/{model}")] {
1819                let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1820                assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
1821                assert_eq!(resolved.resolved.id, model);
1822                assert!(!resolved.used_fallback);
1823            }
1824        }
1825
1826        for messages_only in [
1827            "minimax-m3",
1828            "minimax-m2.7",
1829            "minimax-m2.5",
1830            "qwen3.7-max",
1831            "qwen3.7-plus",
1832            "qwen3.6-plus",
1833        ] {
1834            for requested in [
1835                messages_only.to_string(),
1836                format!("opencode-go/{messages_only}"),
1837            ] {
1838                let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1839                assert!(rejected.used_fallback, "{requested}");
1840                assert_eq!(
1841                    rejected.resolved.provider,
1842                    ProviderKind::OpencodeGo,
1843                    "{requested} must not cross-route"
1844                );
1845                assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
1846            }
1847        }
1848    }
1849
1850    #[test]
1851    fn xai_grok_models_resolve_when_provider_hinted() {
1852        let registry = ModelRegistry::default();
1853
1854        let default = registry.resolve(None, Some(ProviderKind::Xai));
1855        assert_eq!(default.resolved.provider, ProviderKind::Xai);
1856        assert_eq!(default.resolved.id, "grok-4.5");
1857        assert!(default.used_fallback);
1858
1859        let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
1860        assert_eq!(alias.resolved.provider, ProviderKind::Xai);
1861        assert_eq!(alias.resolved.id, "grok-4.5");
1862        assert!(!alias.used_fallback);
1863
1864        let fast = registry.resolve(
1865            Some("grok-4.20-0309-non-reasoning"),
1866            Some(ProviderKind::Xai),
1867        );
1868        assert_eq!(fast.resolved.provider, ProviderKind::Xai);
1869        assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
1870        assert!(!fast.resolved.supports_reasoning);
1871    }
1872
1873    #[test]
1874    fn meta_muse_spark_resolves_when_provider_hinted() {
1875        let registry = ModelRegistry::default();
1876
1877        let default = registry.resolve(None, Some(ProviderKind::Meta));
1878        assert_eq!(default.resolved.provider, ProviderKind::Meta);
1879        assert_eq!(default.resolved.id, "muse-spark-1.1");
1880        assert!(default.used_fallback);
1881
1882        let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
1883        assert_eq!(alias.resolved.provider, ProviderKind::Meta);
1884        assert_eq!(alias.resolved.id, "muse-spark-1.1");
1885        assert!(!alias.used_fallback);
1886        assert_eq!(model_family("muse-spark-1.1"), ModelFamily::Meta);
1887    }
1888
1889    #[test]
1890    fn openai_gpt56_family_resolves_when_provider_hinted() {
1891        let registry = ModelRegistry::default();
1892        for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
1893            let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
1894            assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
1895            assert_eq!(resolved.resolved.id, model, "{model}");
1896            assert!(resolved.resolved.supports_tools, "{model}");
1897            assert!(resolved.resolved.supports_reasoning, "{model}");
1898            assert!(!resolved.used_fallback, "{model}");
1899        }
1900    }
1901
1902    #[test]
1903    fn grok_ids_stay_in_grok_family() {
1904        assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
1905        assert_eq!(
1906            model_family("grok-4.20-0309-non-reasoning"),
1907            ModelFamily::Grok
1908        );
1909    }
1910
1911    #[test]
1912    fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
1913        let registry = ModelRegistry::default();
1914
1915        let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
1916        assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
1917        assert_eq!(stepfun.resolved.id, "step-3.7-flash");
1918
1919        for (alias, expected) in [
1920            ("minimax", "MiniMax-M3"),
1921            ("minimax-m3", "MiniMax-M3"),
1922            ("minimax-m2.7", "MiniMax-M2.7"),
1923            ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
1924            ("minimax-m2.1", "MiniMax-M2.1"),
1925            ("minimax-m2", "MiniMax-M2"),
1926        ] {
1927            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
1928
1929            assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
1930            assert_eq!(resolved.resolved.id, expected);
1931            assert!(!resolved.used_fallback);
1932            assert!(resolved.resolved.supports_tools);
1933            assert!(resolved.resolved.supports_reasoning);
1934        }
1935    }
1936
1937    #[test]
1938    fn minimax_anthropic_models_resolve_when_provider_hinted() {
1939        let registry = ModelRegistry::default();
1940
1941        for (alias, expected) in [
1942            ("minimax-anthropic", "MiniMax-M3"),
1943            ("minimax-m3", "MiniMax-M3"),
1944            ("minimax-m2.7", "MiniMax-M2.7"),
1945        ] {
1946            let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
1947
1948            assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
1949            assert_eq!(resolved.resolved.id, expected);
1950            assert!(!resolved.used_fallback);
1951            assert!(resolved.resolved.supports_tools);
1952            assert!(resolved.resolved.supports_reasoning);
1953        }
1954    }
1955
1956    #[test]
1957    fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1958        let registry = ModelRegistry::default();
1959        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1960
1961        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1962        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1963    }
1964
1965    #[test]
1966    fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1967        let registry = ModelRegistry::default();
1968
1969        for (alias, expected) in [
1970            ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1971            ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1972            ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1973            ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1974            ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1975            ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1976            ("glm-5.1", "z-ai/glm-5.1"),
1977            ("glm-5.2", "z-ai/glm-5.2"),
1978            ("minimax-m3", "minimax/minimax-m3"),
1979            ("minimax-2.7", "minimax/minimax-m2.7"),
1980            ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1981            ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1982            ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1983            ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1984            (
1985                "nvidia/nemotron-3-ultra",
1986                "nvidia/nemotron-3-ultra-550b-a55b",
1987            ),
1988        ] {
1989            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1990
1991            assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1992            assert_eq!(resolved.resolved.id, expected);
1993            assert!(resolved.resolved.supports_tools);
1994            assert!(resolved.resolved.supports_reasoning);
1995        }
1996    }
1997
1998    #[test]
1999    fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2000        let registry = ModelRegistry::default();
2001        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2002
2003        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2004        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2005    }
2006
2007    #[test]
2008    fn together_inkling_keeps_published_wire_identity() {
2009        let registry = ModelRegistry::default();
2010        for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2011            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2012
2013            assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2014            assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2015            assert!(resolved.resolved.supports_tools);
2016            assert!(resolved.resolved.supports_reasoning);
2017            assert!(!resolved.used_fallback);
2018        }
2019
2020        let unscoped = registry.resolve(Some("inkling"), None);
2021        assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2022        assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2023        assert!(!unscoped.used_fallback);
2024    }
2025
2026    #[test]
2027    fn registry_lists_and_resolves_every_v090_catalog_addition() {
2028        let registry = ModelRegistry::default();
2029        let advertised = [
2030            (ProviderKind::Anthropic, "claude-sonnet-5"),
2031            (ProviderKind::Anthropic, "claude-fable-5"),
2032            (ProviderKind::Openai, "gpt-5.3-codex"),
2033            (ProviderKind::Openai, "gpt-5.5"),
2034            (ProviderKind::Openai, "gpt-5.5-pro"),
2035            (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2036            (ProviderKind::Arcee, "trinity-mini"),
2037        ];
2038
2039        let listed = registry.list();
2040        for (provider, model_id) in advertised {
2041            assert!(
2042                listed
2043                    .iter()
2044                    .any(|model| model.provider == provider && model.id == model_id),
2045                "missing {model_id} ({}) from model list",
2046                provider.as_str()
2047            );
2048            let resolved = registry.resolve(Some(model_id), Some(provider));
2049            assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2050            assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2051            assert!(!resolved.used_fallback, "{model_id}");
2052        }
2053    }
2054
2055    #[test]
2056    fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2057        let registry = ModelRegistry::default();
2058
2059        let unscoped = registry.resolve(Some("gpt-5.5"), None);
2060        assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2061        assert_eq!(unscoped.resolved.id, "gpt-5.5");
2062        assert!(!unscoped.used_fallback);
2063
2064        let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2065        assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2066        assert_eq!(codex.resolved.id, "gpt-5.5");
2067        assert!(!codex.used_fallback);
2068    }
2069
2070    #[test]
2071    fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2072        let registry = ModelRegistry::default();
2073        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2074
2075        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2076        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2077    }
2078
2079    #[test]
2080    fn vllm_default_uses_canonical_model_id() {
2081        let registry = ModelRegistry::default();
2082        let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2083
2084        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2085        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2086    }
2087
2088    #[test]
2089    fn ollama_default_uses_small_local_model_id() {
2090        let registry = ModelRegistry::default();
2091        let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2092
2093        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2094        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2095        assert!(resolved.resolved.supports_reasoning);
2096    }
2097
2098    #[test]
2099    fn ollama_requested_model_tag_is_preserved() {
2100        let registry = ModelRegistry::default();
2101        let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2102
2103        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2104        assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2105        assert!(!resolved.used_fallback);
2106    }
2107
2108    #[test]
2109    fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2110        let registry = ModelRegistry::default();
2111        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2112
2113        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2114        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2115    }
2116
2117    #[test]
2118    fn preserves_requested_model_casing_for_third_party_providers() {
2119        let registry = ModelRegistry::default();
2120        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2121
2122        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2123        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2124    }
2125
2126    #[test]
2127    fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2128        let registry = ModelRegistry::default();
2129        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2130
2131        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2132        // Registry's canonical id is used even when user provides different casing
2133        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2134    }
2135
2136    #[test]
2137    fn preserves_requested_model_casing_without_surrounding_whitespace() {
2138        let registry = ModelRegistry::default();
2139        let resolved = registry.resolve(Some("  DeepSeek-V4-Pro  "), None);
2140
2141        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2142        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2143    }
2144
2145    #[test]
2146    fn alias_match_does_not_override_requested_casing() {
2147        let registry = ModelRegistry::default();
2148        let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2149
2150        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2151        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2152    }
2153
2154    #[test]
2155    fn model_family_classifies_known_model_ids() {
2156        assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2157        assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2158        assert_eq!(
2159            model_family("anthropic/claude-opus-4-7"),
2160            ModelFamily::Anthropic
2161        );
2162        assert_eq!(
2163            model_family("meta-llama/llama-3.3-70b-instruct"),
2164            ModelFamily::Meta
2165        );
2166        assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2167    }
2168
2169    #[test]
2170    fn model_family_uses_underlying_model_for_router_ids() {
2171        assert_eq!(
2172            model_family("groq/llama-3.3-70b-versatile"),
2173            ModelFamily::Meta
2174        );
2175        assert_eq!(
2176            model_family("openrouter/openai/gpt-5.4"),
2177            ModelFamily::OpenAI
2178        );
2179        assert_eq!(
2180            model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2181            ModelFamily::DeepSeek
2182        );
2183    }
2184
2185    #[test]
2186    fn model_family_covers_prominent_google_and_mistral_model_names() {
2187        assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2188        assert_eq!(
2189            model_family("mistralai/mixtral-8x22b"),
2190            ModelFamily::Mistral
2191        );
2192        assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2193    }
2194
2195    #[test]
2196    fn model_family_falls_back_to_inferencer_for_unknown_models() {
2197        assert_eq!(
2198            model_family("custom-gateway/my-private-model"),
2199            ModelFamily::Inferencer
2200        );
2201        assert_eq!(model_family(""), ModelFamily::Inferencer);
2202    }
2203}