1use std::collections::HashMap;
2
3use codewhale_config::ProviderKind;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ModelInfo {
13 pub id: String,
15 pub provider: ProviderKind,
17 pub aliases: Vec<String>,
19 pub supports_tools: bool,
21 pub supports_reasoning: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ModelResolution {
31 pub requested: Option<String>,
33 pub resolved: ModelInfo,
35 pub used_fallback: bool,
37 pub fallback_chain: Vec<String>,
39}
40
41#[derive(Debug, Clone)]
47pub struct ModelRegistry {
48 models: Vec<ModelInfo>,
49 alias_map: HashMap<String, usize>,
50}
51
52impl Default for ModelRegistry {
54 fn default() -> Self {
55 let models = vec![
56 ModelInfo {
57 id: "deepseek-v4-pro".to_string(),
58 provider: ProviderKind::Deepseek,
59 aliases: vec![],
60 supports_tools: true,
61 supports_reasoning: true,
62 },
63 ModelInfo {
64 id: "deepseek-v4-flash".to_string(),
65 provider: ProviderKind::Deepseek,
66 aliases: vec![
67 "deepseek-chat".to_string(),
68 "deepseek-reasoner".to_string(),
69 "deepseek-r1".to_string(),
70 "deepseek-v3".to_string(),
71 "deepseek-v3.2".to_string(),
72 ],
73 supports_tools: true,
74 supports_reasoning: true,
75 },
76 ModelInfo {
77 id: "deepseek-ai/deepseek-v4-pro".to_string(),
78 provider: ProviderKind::NvidiaNim,
79 aliases: vec![
80 "deepseek-v4-pro".to_string(),
81 "nvidia-deepseek-v4-pro".to_string(),
82 "nim-deepseek-v4-pro".to_string(),
83 ],
84 supports_tools: true,
85 supports_reasoning: true,
86 },
87 ModelInfo {
88 id: "deepseek-ai/deepseek-v4-flash".to_string(),
89 provider: ProviderKind::NvidiaNim,
90 aliases: vec![
91 "deepseek-v4-flash".to_string(),
92 "deepseek-chat".to_string(),
93 "deepseek-reasoner".to_string(),
94 "nvidia-deepseek-v4-flash".to_string(),
95 "nim-deepseek-v4-flash".to_string(),
96 ],
97 supports_tools: true,
98 supports_reasoning: true,
99 },
100 ModelInfo {
101 id: "deepseek-v4-pro".to_string(),
102 provider: ProviderKind::Openai,
103 aliases: vec!["openai-compatible-deepseek-v4-pro".to_string()],
104 supports_tools: true,
105 supports_reasoning: true,
106 },
107 ModelInfo {
108 id: "deepseek-v4-flash".to_string(),
109 provider: ProviderKind::Openai,
110 aliases: vec!["openai-compatible-deepseek-v4-flash".to_string()],
111 supports_tools: true,
112 supports_reasoning: true,
113 },
114 ModelInfo {
115 id: "deepseek-ai/deepseek-v4-flash".to_string(),
116 provider: ProviderKind::Atlascloud,
117 aliases: vec![
118 "deepseek-v4-flash".to_string(),
119 "atlascloud-deepseek-v4-flash".to_string(),
120 ],
121 supports_tools: true,
122 supports_reasoning: true,
123 },
124 ModelInfo {
125 id: "deepseek-ai/deepseek-v4-pro".to_string(),
126 provider: ProviderKind::Atlascloud,
127 aliases: vec![
128 "deepseek-v4-pro".to_string(),
129 "atlascloud-deepseek-v4-pro".to_string(),
130 ],
131 supports_tools: true,
132 supports_reasoning: true,
133 },
134 ModelInfo {
135 id: "deepseek-reasoner".to_string(),
136 provider: ProviderKind::WanjieArk,
137 aliases: vec![
138 "wanjie-deepseek-reasoner".to_string(),
139 "ark-wanjie-deepseek-reasoner".to_string(),
140 ],
141 supports_tools: true,
142 supports_reasoning: true,
143 },
144 ModelInfo {
145 id: "DeepSeek-V4-Pro".to_string(),
146 provider: ProviderKind::Volcengine,
147 aliases: vec![
148 "deepseek-v4-pro".to_string(),
149 "volcengine-deepseek-v4-pro".to_string(),
150 "ark-deepseek-v4-pro".to_string(),
151 ],
152 supports_tools: true,
153 supports_reasoning: true,
154 },
155 ModelInfo {
156 id: "DeepSeek-V4-Flash".to_string(),
157 provider: ProviderKind::Volcengine,
158 aliases: vec![
159 "deepseek-v4-flash".to_string(),
160 "deepseek-chat".to_string(),
161 "volcengine-deepseek-v4-flash".to_string(),
162 "ark-deepseek-v4-flash".to_string(),
163 ],
164 supports_tools: true,
165 supports_reasoning: true,
166 },
167 ModelInfo {
168 id: "deepseek/deepseek-v4-pro".to_string(),
169 provider: ProviderKind::Openrouter,
170 aliases: vec![
171 "deepseek-v4-pro".to_string(),
172 "openrouter-deepseek-v4-pro".to_string(),
173 ],
174 supports_tools: true,
175 supports_reasoning: true,
176 },
177 ModelInfo {
178 id: "deepseek/deepseek-v4-flash".to_string(),
179 provider: ProviderKind::Openrouter,
180 aliases: vec![
181 "deepseek-v4-flash".to_string(),
182 "deepseek-chat".to_string(),
183 "deepseek-reasoner".to_string(),
184 "openrouter-deepseek-v4-flash".to_string(),
185 ],
186 supports_tools: true,
187 supports_reasoning: true,
188 },
189 ModelInfo {
190 id: "arcee-ai/trinity-large-thinking".to_string(),
191 provider: ProviderKind::Openrouter,
192 aliases: vec![
193 "trinity".to_string(),
194 "trinity-large-thinking".to_string(),
195 "arcee-trinity-large-thinking".to_string(),
196 ],
197 supports_tools: true,
198 supports_reasoning: true,
199 },
200 ModelInfo {
201 id: "xiaomi/mimo-v2.5-pro".to_string(),
202 provider: ProviderKind::Openrouter,
203 aliases: vec![
204 "openrouter-mimo-v2.5-pro".to_string(),
205 "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
206 ],
207 supports_tools: true,
208 supports_reasoning: true,
209 },
210 ModelInfo {
211 id: "xiaomi/mimo-v2.5".to_string(),
212 provider: ProviderKind::Openrouter,
213 aliases: vec![
214 "openrouter-mimo-v2.5".to_string(),
215 "openrouter-xiaomi-mimo-v2.5".to_string(),
216 ],
217 supports_tools: true,
218 supports_reasoning: true,
219 },
220 ModelInfo {
221 id: "qwen/qwen3.6-35b-a3b".to_string(),
222 provider: ProviderKind::Openrouter,
223 aliases: vec![
224 "qwen3.6-35b-a3b".to_string(),
225 "qwen-3.6-35b-a3b".to_string(),
226 ],
227 supports_tools: true,
228 supports_reasoning: true,
229 },
230 ModelInfo {
231 id: "qwen/qwen3.6-27b".to_string(),
232 provider: ProviderKind::Openrouter,
233 aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
234 supports_tools: true,
235 supports_reasoning: true,
236 },
237 ModelInfo {
238 id: "moonshotai/kimi-k2.6".to_string(),
239 provider: ProviderKind::Openrouter,
240 aliases: vec!["openrouter-kimi-k2.6".to_string()],
241 supports_tools: true,
242 supports_reasoning: true,
243 },
244 ModelInfo {
245 id: "minimax/minimax-m3".to_string(),
246 provider: ProviderKind::Openrouter,
247 aliases: vec![
248 "minimax-m3".to_string(),
249 "minimax-m-3".to_string(),
250 "openrouter-minimax-m3".to_string(),
251 ],
252 supports_tools: true,
253 supports_reasoning: true,
254 },
255 ModelInfo {
256 id: "z-ai/glm-5.1".to_string(),
257 provider: ProviderKind::Openrouter,
258 aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
259 supports_tools: true,
260 supports_reasoning: true,
261 },
262 ModelInfo {
263 id: "tencent/hy3-preview".to_string(),
264 provider: ProviderKind::Openrouter,
265 aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
266 supports_tools: true,
267 supports_reasoning: true,
268 },
269 ModelInfo {
270 id: "google/gemma-4-31b-it".to_string(),
271 provider: ProviderKind::Openrouter,
272 aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
273 supports_tools: true,
274 supports_reasoning: true,
275 },
276 ModelInfo {
277 id: "google/gemma-4-26b-a4b-it".to_string(),
278 provider: ProviderKind::Openrouter,
279 aliases: vec![
280 "gemma-4-26b-a4b".to_string(),
281 "gemma-4-26b-a4b-it".to_string(),
282 ],
283 supports_tools: true,
284 supports_reasoning: true,
285 },
286 ModelInfo {
287 id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
288 provider: ProviderKind::Openrouter,
289 aliases: vec![
290 "nemotron-3-nano-omni".to_string(),
291 "nemotron-3-nano-omni-reasoning".to_string(),
292 ],
293 supports_tools: true,
294 supports_reasoning: true,
295 },
296 ModelInfo {
297 id: "mimo-v2.5-pro".to_string(),
298 provider: ProviderKind::XiaomiMimo,
299 aliases: vec!["mimo".to_string()],
300 supports_tools: true,
301 supports_reasoning: true,
302 },
303 ModelInfo {
304 id: "mimo-v2.5".to_string(),
305 provider: ProviderKind::XiaomiMimo,
306 aliases: vec!["xiaomi-mimo-v2.5".to_string()],
307 supports_tools: true,
308 supports_reasoning: true,
309 },
310 ModelInfo {
311 id: "mimo-v2.5-tts".to_string(),
312 provider: ProviderKind::XiaomiMimo,
313 aliases: vec![
314 "tts".to_string(),
315 "speech".to_string(),
316 "mimo-tts".to_string(),
317 ],
318 supports_tools: false,
319 supports_reasoning: false,
320 },
321 ModelInfo {
322 id: "mimo-v2.5-tts-voicedesign".to_string(),
323 provider: ProviderKind::XiaomiMimo,
324 aliases: vec![
325 "voicedesign".to_string(),
326 "voice-design".to_string(),
327 "mimo-voice-design".to_string(),
328 ],
329 supports_tools: false,
330 supports_reasoning: false,
331 },
332 ModelInfo {
333 id: "mimo-v2.5-tts-voiceclone".to_string(),
334 provider: ProviderKind::XiaomiMimo,
335 aliases: vec![
336 "voiceclone".to_string(),
337 "voice-clone".to_string(),
338 "mimo-voice-clone".to_string(),
339 ],
340 supports_tools: false,
341 supports_reasoning: false,
342 },
343 ModelInfo {
344 id: "mimo-v2-tts".to_string(),
345 provider: ProviderKind::XiaomiMimo,
346 aliases: vec!["mimo-v2-speech".to_string()],
347 supports_tools: false,
348 supports_reasoning: false,
349 },
350 ModelInfo {
351 id: "deepseek/deepseek-v4-pro".to_string(),
352 provider: ProviderKind::Novita,
353 aliases: vec![
354 "deepseek-v4-pro".to_string(),
355 "novita-deepseek-v4-pro".to_string(),
356 ],
357 supports_tools: true,
358 supports_reasoning: true,
359 },
360 ModelInfo {
361 id: "deepseek/deepseek-v4-flash".to_string(),
362 provider: ProviderKind::Novita,
363 aliases: vec![
364 "deepseek-v4-flash".to_string(),
365 "deepseek-chat".to_string(),
366 "deepseek-reasoner".to_string(),
367 "novita-deepseek-v4-flash".to_string(),
368 ],
369 supports_tools: true,
370 supports_reasoning: true,
371 },
372 ModelInfo {
373 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
374 provider: ProviderKind::Fireworks,
375 aliases: vec![
376 "deepseek-v4-pro".to_string(),
377 "fireworks-deepseek-v4-pro".to_string(),
378 ],
379 supports_tools: true,
380 supports_reasoning: true,
381 },
382 ModelInfo {
383 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
384 provider: ProviderKind::Siliconflow,
385 aliases: vec![
386 "deepseek-v4-pro".to_string(),
387 "deepseek-reasoner".to_string(),
388 "deepseek-r1".to_string(),
389 "siliconflow-deepseek-v4-pro".to_string(),
390 ],
391 supports_tools: true,
392 supports_reasoning: true,
393 },
394 ModelInfo {
395 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
396 provider: ProviderKind::Siliconflow,
397 aliases: vec![
398 "deepseek-v4-flash".to_string(),
399 "deepseek-chat".to_string(),
400 "deepseek-v3".to_string(),
401 "siliconflow-deepseek-v4-flash".to_string(),
402 ],
403 supports_tools: true,
404 supports_reasoning: true,
405 },
406 ModelInfo {
407 id: "kimi-k2.6".to_string(),
408 provider: ProviderKind::Moonshot,
409 aliases: vec![
410 "kimi".to_string(),
411 "kimi-k2".to_string(),
412 "moonshot-kimi-k2.6".to_string(),
413 ],
414 supports_tools: true,
415 supports_reasoning: true,
416 },
417 ModelInfo {
418 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
419 provider: ProviderKind::Sglang,
420 aliases: vec![
421 "deepseek-v4-pro".to_string(),
422 "sglang-deepseek-v4-pro".to_string(),
423 ],
424 supports_tools: true,
425 supports_reasoning: true,
426 },
427 ModelInfo {
428 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
429 provider: ProviderKind::Sglang,
430 aliases: vec![
431 "deepseek-v4-flash".to_string(),
432 "deepseek-chat".to_string(),
433 "deepseek-reasoner".to_string(),
434 "sglang-deepseek-v4-flash".to_string(),
435 ],
436 supports_tools: true,
437 supports_reasoning: true,
438 },
439 ModelInfo {
440 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
441 provider: ProviderKind::Vllm,
442 aliases: vec![
443 "deepseek-v4-pro".to_string(),
444 "vllm-deepseek-v4-pro".to_string(),
445 ],
446 supports_tools: true,
447 supports_reasoning: true,
448 },
449 ModelInfo {
450 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
451 provider: ProviderKind::Vllm,
452 aliases: vec![
453 "deepseek-v4-flash".to_string(),
454 "deepseek-chat".to_string(),
455 "deepseek-reasoner".to_string(),
456 "vllm-deepseek-v4-flash".to_string(),
457 ],
458 supports_tools: true,
459 supports_reasoning: true,
460 },
461 ModelInfo {
462 id: "deepseek-coder:1.3b".to_string(),
463 provider: ProviderKind::Ollama,
464 aliases: vec![],
465 supports_tools: true,
466 supports_reasoning: false,
467 },
468 ];
469 Self::new(models)
470 }
471}
472
473impl ModelRegistry {
474 #[must_use]
480 pub fn new(models: Vec<ModelInfo>) -> Self {
481 let mut alias_map = HashMap::new();
482 for (idx, model) in models.iter().enumerate() {
483 alias_map.entry(normalize(&model.id)).or_insert(idx);
484 for alias in &model.aliases {
485 alias_map.entry(normalize(alias)).or_insert(idx);
486 }
487 }
488 Self { models, alias_map }
489 }
490
491 #[must_use]
493 pub fn list(&self) -> Vec<ModelInfo> {
494 self.models.clone()
495 }
496
497 #[must_use]
509 pub fn resolve(
510 &self,
511 requested: Option<&str>,
512 provider_hint: Option<ProviderKind>,
513 ) -> ModelResolution {
514 let mut fallback_chain = Vec::new();
515
516 if let Some(name) = requested {
517 fallback_chain.push(format!("requested:{name}"));
518 if provider_hint == Some(ProviderKind::Ollama) {
519 return ModelResolution {
520 requested: Some(name.to_string()),
521 resolved: ModelInfo {
522 id: name.trim().to_string(),
523 provider: ProviderKind::Ollama,
524 aliases: Vec::new(),
525 supports_tools: true,
526 supports_reasoning: false,
527 },
528 used_fallback: false,
529 fallback_chain,
530 };
531 }
532 if let Some(provider) = provider_hint
533 && let Some(model) = self
534 .models
535 .iter()
536 .find(|m| m.provider == provider && model_matches(m, name))
537 .cloned()
538 {
539 return ModelResolution {
540 requested: Some(name.to_string()),
541 resolved: model,
542 used_fallback: false,
543 fallback_chain,
544 };
545 }
546 if provider_hint == Some(ProviderKind::Atlascloud)
547 && let Some(model) = atlascloud_passthrough_model(name)
548 {
549 return ModelResolution {
550 requested: Some(name.to_string()),
551 resolved: model,
552 used_fallback: false,
553 fallback_chain,
554 };
555 }
556 if let Some(idx) = self.alias_map.get(&normalize(name)) {
557 return ModelResolution {
558 requested: Some(name.to_string()),
559 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
560 used_fallback: false,
561 fallback_chain,
562 };
563 }
564 }
565
566 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
567 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
568 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
569 return ModelResolution {
570 requested: requested.map(ToOwned::to_owned),
571 resolved: model,
572 used_fallback: true,
573 fallback_chain,
574 };
575 }
576
577 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
578 id: "deepseek-v4-pro".to_string(),
579 provider: ProviderKind::Deepseek,
580 aliases: Vec::new(),
581 supports_tools: true,
582 supports_reasoning: true,
583 });
584 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
585 ModelResolution {
586 requested: requested.map(ToOwned::to_owned),
587 resolved: final_fallback,
588 used_fallback: true,
589 fallback_chain,
590 }
591 }
592}
593
594fn normalize(value: &str) -> String {
595 value.trim().to_ascii_lowercase()
596}
597
598fn model_matches(model: &ModelInfo, requested: &str) -> bool {
599 let requested = normalize(requested);
600 normalize(&model.id) == requested
601 || model
602 .aliases
603 .iter()
604 .any(|alias| normalize(alias) == requested)
605}
606
607fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
608 let requested = requested.trim();
609 if model.id.eq_ignore_ascii_case(requested) {
610 model.id = requested.to_string();
611 }
612 model
613}
614
615fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
616 let requested = requested.trim();
617 if requested.is_empty() || !requested.contains('/') {
618 return None;
619 }
620
621 Some(ModelInfo {
622 id: requested.to_string(),
623 provider: ProviderKind::Atlascloud,
624 aliases: Vec::new(),
625 supports_tools: true,
626 supports_reasoning: true,
627 })
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[test]
635 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
636 let registry = ModelRegistry::default();
637 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
638
639 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
640 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
641 }
642
643 #[test]
644 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
645 let registry = ModelRegistry::default();
646 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
647
648 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
649 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
650 }
651
652 #[test]
653 fn nvidia_nim_default_uses_catalog_model_id() {
654 let registry = ModelRegistry::default();
655 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
656
657 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
658 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
659 }
660
661 #[test]
662 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
663 let registry = ModelRegistry::default();
664 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
665
666 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
667 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
668 }
669
670 #[test]
671 fn atlascloud_default_uses_namespaced_model_id() {
672 let registry = ModelRegistry::default();
673 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
674
675 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
676 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
677 assert!(resolved.resolved.supports_reasoning);
678 }
679
680 #[test]
681 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
682 let registry = ModelRegistry::default();
683 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
684
685 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
686 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
687 }
688
689 #[test]
690 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
691 let registry = ModelRegistry::default();
692 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
693
694 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
695 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
696 }
697
698 #[test]
699 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
700 let registry = ModelRegistry::default();
701 let resolved =
702 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
703
704 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
705 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
706 assert!(resolved.resolved.supports_tools);
707 assert!(resolved.resolved.supports_reasoning);
708 assert!(!resolved.used_fallback);
709 }
710
711 #[test]
712 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
713 let registry = ModelRegistry::default();
714 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
715
716 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
717 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
718 assert!(!resolved.used_fallback);
719 }
720
721 #[test]
722 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
723 let registry = ModelRegistry::default();
724 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
725
726 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
727 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
728 assert!(resolved.used_fallback);
729 }
730
731 #[test]
732 fn openrouter_default_uses_namespaced_model_id() {
733 let registry = ModelRegistry::default();
734 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
735
736 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
737 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
738 }
739
740 #[test]
741 fn xiaomi_mimo_default_uses_canonical_model_id() {
742 let registry = ModelRegistry::default();
743 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
744
745 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
746 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
747 assert!(resolved.resolved.supports_reasoning);
748 }
749
750 #[test]
751 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
752 let registry = ModelRegistry::default();
753 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
754 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
755 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
756 assert!(!resolved.resolved.supports_tools);
757 assert!(!resolved.resolved.supports_reasoning);
758
759 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
760 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
761
762 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
763 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
764 }
765
766 #[test]
767 fn wanjie_ark_default_uses_reasoner_model_id() {
768 let registry = ModelRegistry::default();
769 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
770
771 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
772 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
773 assert!(resolved.resolved.supports_reasoning);
774 }
775
776 #[test]
777 fn novita_default_uses_namespaced_model_id() {
778 let registry = ModelRegistry::default();
779 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
780
781 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
782 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
783 }
784
785 #[test]
786 fn fireworks_default_uses_canonical_model_id() {
787 let registry = ModelRegistry::default();
788 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
789
790 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
791 assert_eq!(
792 resolved.resolved.id,
793 "accounts/fireworks/models/deepseek-v4-pro"
794 );
795 }
796
797 #[test]
798 fn siliconflow_default_uses_canonical_pro_model_id() {
799 let registry = ModelRegistry::default();
800 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
801
802 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
803 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
804 assert!(resolved.resolved.supports_reasoning);
805 }
806
807 #[test]
808 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
809 let registry = ModelRegistry::default();
810 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
811
812 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
813 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
814 }
815
816 #[test]
817 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
818 let registry = ModelRegistry::default();
819 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
820
821 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
822 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
823 }
824
825 #[test]
826 fn sglang_default_uses_canonical_model_id() {
827 let registry = ModelRegistry::default();
828 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
829
830 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
831 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
832 }
833
834 #[test]
835 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
836 let registry = ModelRegistry::default();
837 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
838
839 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
840 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
841 }
842
843 #[test]
844 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
845 let registry = ModelRegistry::default();
846
847 for (alias, expected) in [
848 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
849 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
850 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
851 ("glm-5.1", "z-ai/glm-5.1"),
852 ("minimax-m3", "minimax/minimax-m3"),
853 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
854 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
855 ] {
856 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
857
858 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
859 assert_eq!(resolved.resolved.id, expected);
860 assert!(resolved.resolved.supports_tools);
861 assert!(resolved.resolved.supports_reasoning);
862 }
863 }
864
865 #[test]
866 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
867 let registry = ModelRegistry::default();
868 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
869
870 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
871 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
872 }
873
874 #[test]
875 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
876 let registry = ModelRegistry::default();
877 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
878
879 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
880 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
881 }
882
883 #[test]
884 fn vllm_default_uses_canonical_model_id() {
885 let registry = ModelRegistry::default();
886 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
887
888 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
889 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
890 }
891
892 #[test]
893 fn ollama_default_uses_small_local_model_id() {
894 let registry = ModelRegistry::default();
895 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
896
897 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
898 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
899 assert!(!resolved.resolved.supports_reasoning);
900 }
901
902 #[test]
903 fn ollama_requested_model_tag_is_preserved() {
904 let registry = ModelRegistry::default();
905 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
906
907 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
908 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
909 assert!(!resolved.used_fallback);
910 }
911
912 #[test]
913 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
914 let registry = ModelRegistry::default();
915 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
916
917 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
918 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
919 }
920
921 #[test]
922 fn preserves_requested_model_casing_for_third_party_providers() {
923 let registry = ModelRegistry::default();
924 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
925
926 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
927 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
928 }
929
930 #[test]
931 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
932 let registry = ModelRegistry::default();
933 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
934
935 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
936 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
938 }
939
940 #[test]
941 fn preserves_requested_model_casing_without_surrounding_whitespace() {
942 let registry = ModelRegistry::default();
943 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
944
945 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
946 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
947 }
948
949 #[test]
950 fn alias_match_does_not_override_requested_casing() {
951 let registry = ModelRegistry::default();
952 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
953
954 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
955 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
956 }
957}