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: "deepseek/deepseek-v4-pro".to_string(),
312 provider: ProviderKind::Novita,
313 aliases: vec![
314 "deepseek-v4-pro".to_string(),
315 "novita-deepseek-v4-pro".to_string(),
316 ],
317 supports_tools: true,
318 supports_reasoning: true,
319 },
320 ModelInfo {
321 id: "deepseek/deepseek-v4-flash".to_string(),
322 provider: ProviderKind::Novita,
323 aliases: vec![
324 "deepseek-v4-flash".to_string(),
325 "deepseek-chat".to_string(),
326 "deepseek-reasoner".to_string(),
327 "novita-deepseek-v4-flash".to_string(),
328 ],
329 supports_tools: true,
330 supports_reasoning: true,
331 },
332 ModelInfo {
333 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
334 provider: ProviderKind::Fireworks,
335 aliases: vec![
336 "deepseek-v4-pro".to_string(),
337 "fireworks-deepseek-v4-pro".to_string(),
338 ],
339 supports_tools: true,
340 supports_reasoning: true,
341 },
342 ModelInfo {
343 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
344 provider: ProviderKind::Siliconflow,
345 aliases: vec![
346 "deepseek-v4-pro".to_string(),
347 "deepseek-reasoner".to_string(),
348 "deepseek-r1".to_string(),
349 "siliconflow-deepseek-v4-pro".to_string(),
350 ],
351 supports_tools: true,
352 supports_reasoning: true,
353 },
354 ModelInfo {
355 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
356 provider: ProviderKind::Siliconflow,
357 aliases: vec![
358 "deepseek-v4-flash".to_string(),
359 "deepseek-chat".to_string(),
360 "deepseek-v3".to_string(),
361 "siliconflow-deepseek-v4-flash".to_string(),
362 ],
363 supports_tools: true,
364 supports_reasoning: true,
365 },
366 ModelInfo {
367 id: "kimi-k2.6".to_string(),
368 provider: ProviderKind::Moonshot,
369 aliases: vec![
370 "kimi".to_string(),
371 "kimi-k2".to_string(),
372 "moonshot-kimi-k2.6".to_string(),
373 ],
374 supports_tools: true,
375 supports_reasoning: true,
376 },
377 ModelInfo {
378 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
379 provider: ProviderKind::Sglang,
380 aliases: vec![
381 "deepseek-v4-pro".to_string(),
382 "sglang-deepseek-v4-pro".to_string(),
383 ],
384 supports_tools: true,
385 supports_reasoning: true,
386 },
387 ModelInfo {
388 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
389 provider: ProviderKind::Sglang,
390 aliases: vec![
391 "deepseek-v4-flash".to_string(),
392 "deepseek-chat".to_string(),
393 "deepseek-reasoner".to_string(),
394 "sglang-deepseek-v4-flash".to_string(),
395 ],
396 supports_tools: true,
397 supports_reasoning: true,
398 },
399 ModelInfo {
400 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
401 provider: ProviderKind::Vllm,
402 aliases: vec![
403 "deepseek-v4-pro".to_string(),
404 "vllm-deepseek-v4-pro".to_string(),
405 ],
406 supports_tools: true,
407 supports_reasoning: true,
408 },
409 ModelInfo {
410 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
411 provider: ProviderKind::Vllm,
412 aliases: vec![
413 "deepseek-v4-flash".to_string(),
414 "deepseek-chat".to_string(),
415 "deepseek-reasoner".to_string(),
416 "vllm-deepseek-v4-flash".to_string(),
417 ],
418 supports_tools: true,
419 supports_reasoning: true,
420 },
421 ModelInfo {
422 id: "deepseek-coder:1.3b".to_string(),
423 provider: ProviderKind::Ollama,
424 aliases: vec![],
425 supports_tools: true,
426 supports_reasoning: false,
427 },
428 ];
429 Self::new(models)
430 }
431}
432
433impl ModelRegistry {
434 #[must_use]
440 pub fn new(models: Vec<ModelInfo>) -> Self {
441 let mut alias_map = HashMap::new();
442 for (idx, model) in models.iter().enumerate() {
443 alias_map.entry(normalize(&model.id)).or_insert(idx);
444 for alias in &model.aliases {
445 alias_map.entry(normalize(alias)).or_insert(idx);
446 }
447 }
448 Self { models, alias_map }
449 }
450
451 #[must_use]
453 pub fn list(&self) -> Vec<ModelInfo> {
454 self.models.clone()
455 }
456
457 #[must_use]
469 pub fn resolve(
470 &self,
471 requested: Option<&str>,
472 provider_hint: Option<ProviderKind>,
473 ) -> ModelResolution {
474 let mut fallback_chain = Vec::new();
475
476 if let Some(name) = requested {
477 fallback_chain.push(format!("requested:{name}"));
478 if provider_hint == Some(ProviderKind::Ollama) {
479 return ModelResolution {
480 requested: Some(name.to_string()),
481 resolved: ModelInfo {
482 id: name.trim().to_string(),
483 provider: ProviderKind::Ollama,
484 aliases: Vec::new(),
485 supports_tools: true,
486 supports_reasoning: false,
487 },
488 used_fallback: false,
489 fallback_chain,
490 };
491 }
492 if let Some(provider) = provider_hint
493 && let Some(model) = self
494 .models
495 .iter()
496 .find(|m| m.provider == provider && model_matches(m, name))
497 .cloned()
498 {
499 return ModelResolution {
500 requested: Some(name.to_string()),
501 resolved: model,
502 used_fallback: false,
503 fallback_chain,
504 };
505 }
506 if let Some(idx) = self.alias_map.get(&normalize(name)) {
507 return ModelResolution {
508 requested: Some(name.to_string()),
509 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
510 used_fallback: false,
511 fallback_chain,
512 };
513 }
514 }
515
516 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
517 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
518 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
519 return ModelResolution {
520 requested: requested.map(ToOwned::to_owned),
521 resolved: model,
522 used_fallback: true,
523 fallback_chain,
524 };
525 }
526
527 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
528 id: "deepseek-v4-pro".to_string(),
529 provider: ProviderKind::Deepseek,
530 aliases: Vec::new(),
531 supports_tools: true,
532 supports_reasoning: true,
533 });
534 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
535 ModelResolution {
536 requested: requested.map(ToOwned::to_owned),
537 resolved: final_fallback,
538 used_fallback: true,
539 fallback_chain,
540 }
541 }
542}
543
544fn normalize(value: &str) -> String {
545 value.trim().to_ascii_lowercase()
546}
547
548fn model_matches(model: &ModelInfo, requested: &str) -> bool {
549 let requested = normalize(requested);
550 normalize(&model.id) == requested
551 || model
552 .aliases
553 .iter()
554 .any(|alias| normalize(alias) == requested)
555}
556
557fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
558 let requested = requested.trim();
559 if model.id.eq_ignore_ascii_case(requested) {
560 model.id = requested.to_string();
561 }
562 model
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
571 let registry = ModelRegistry::default();
572 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
573
574 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
575 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
576 }
577
578 #[test]
579 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
580 let registry = ModelRegistry::default();
581 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
582
583 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
584 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
585 }
586
587 #[test]
588 fn nvidia_nim_default_uses_catalog_model_id() {
589 let registry = ModelRegistry::default();
590 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
591
592 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
593 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
594 }
595
596 #[test]
597 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
598 let registry = ModelRegistry::default();
599 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
600
601 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
602 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
603 }
604
605 #[test]
606 fn atlascloud_default_uses_namespaced_model_id() {
607 let registry = ModelRegistry::default();
608 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
609
610 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
611 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
612 assert!(resolved.resolved.supports_reasoning);
613 }
614
615 #[test]
616 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
617 let registry = ModelRegistry::default();
618 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
619
620 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
621 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
622 }
623
624 #[test]
625 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
626 let registry = ModelRegistry::default();
627 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
628
629 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
630 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
631 }
632
633 #[test]
634 fn openrouter_default_uses_namespaced_model_id() {
635 let registry = ModelRegistry::default();
636 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
637
638 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
639 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
640 }
641
642 #[test]
643 fn xiaomi_mimo_default_uses_canonical_model_id() {
644 let registry = ModelRegistry::default();
645 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
646
647 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
648 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
649 assert!(resolved.resolved.supports_reasoning);
650 }
651
652 #[test]
653 fn wanjie_ark_default_uses_reasoner_model_id() {
654 let registry = ModelRegistry::default();
655 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
656
657 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
658 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
659 assert!(resolved.resolved.supports_reasoning);
660 }
661
662 #[test]
663 fn novita_default_uses_namespaced_model_id() {
664 let registry = ModelRegistry::default();
665 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
666
667 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
668 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
669 }
670
671 #[test]
672 fn fireworks_default_uses_canonical_model_id() {
673 let registry = ModelRegistry::default();
674 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
675
676 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
677 assert_eq!(
678 resolved.resolved.id,
679 "accounts/fireworks/models/deepseek-v4-pro"
680 );
681 }
682
683 #[test]
684 fn siliconflow_default_uses_canonical_pro_model_id() {
685 let registry = ModelRegistry::default();
686 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
687
688 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
689 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
690 assert!(resolved.resolved.supports_reasoning);
691 }
692
693 #[test]
694 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
695 let registry = ModelRegistry::default();
696 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
697
698 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
699 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
700 }
701
702 #[test]
703 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
704 let registry = ModelRegistry::default();
705 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
706
707 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
708 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
709 }
710
711 #[test]
712 fn sglang_default_uses_canonical_model_id() {
713 let registry = ModelRegistry::default();
714 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
715
716 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
717 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
718 }
719
720 #[test]
721 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
722 let registry = ModelRegistry::default();
723 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
724
725 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
726 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
727 }
728
729 #[test]
730 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
731 let registry = ModelRegistry::default();
732
733 for (alias, expected) in [
734 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
735 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
736 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
737 ("glm-5.1", "z-ai/glm-5.1"),
738 ("minimax-m3", "minimax/minimax-m3"),
739 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
740 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
741 ] {
742 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
743
744 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
745 assert_eq!(resolved.resolved.id, expected);
746 assert!(resolved.resolved.supports_tools);
747 assert!(resolved.resolved.supports_reasoning);
748 }
749 }
750
751 #[test]
752 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
753 let registry = ModelRegistry::default();
754 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
755
756 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
757 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
758 }
759
760 #[test]
761 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
762 let registry = ModelRegistry::default();
763 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
764
765 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
766 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
767 }
768
769 #[test]
770 fn vllm_default_uses_canonical_model_id() {
771 let registry = ModelRegistry::default();
772 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
773
774 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
775 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
776 }
777
778 #[test]
779 fn ollama_default_uses_small_local_model_id() {
780 let registry = ModelRegistry::default();
781 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
782
783 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
784 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
785 assert!(!resolved.resolved.supports_reasoning);
786 }
787
788 #[test]
789 fn ollama_requested_model_tag_is_preserved() {
790 let registry = ModelRegistry::default();
791 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
792
793 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
794 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
795 assert!(!resolved.used_fallback);
796 }
797
798 #[test]
799 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
800 let registry = ModelRegistry::default();
801 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
802
803 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
804 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
805 }
806
807 #[test]
808 fn preserves_requested_model_casing_for_third_party_providers() {
809 let registry = ModelRegistry::default();
810 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
811
812 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
813 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
814 }
815
816 #[test]
817 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
818 let registry = ModelRegistry::default();
819 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
820
821 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
822 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
824 }
825
826 #[test]
827 fn preserves_requested_model_casing_without_surrounding_whitespace() {
828 let registry = ModelRegistry::default();
829 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
830
831 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
832 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
833 }
834
835 #[test]
836 fn alias_match_does_not_override_requested_casing() {
837 let registry = ModelRegistry::default();
838 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
839
840 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
841 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
842 }
843}