1use super::{
8 DEFAULT_ARCEE_BASE_URL, DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL,
9 DEFAULT_ATLASCLOUD_MODEL, DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL,
10 DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
11 DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL,
12 DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
13 DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MOONSHOT_BASE_URL,
14 DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL,
15 DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL,
16 DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL,
17 DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENMODEL_BASE_URL,
18 DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL,
19 DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL,
20 DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL,
21 DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL,
22 DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL,
23 DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
24 DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
25 DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
26 DEFAULT_ZAI_MODEL, ProviderKind,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum WireFormat {
33 ChatCompletions,
35 Responses,
37 AnthropicMessages,
39}
40
41pub trait Provider: Send + Sync {
43 fn kind(&self) -> ProviderKind;
45
46 fn id(&self) -> &'static str {
48 self.kind().as_str()
49 }
50
51 fn display_name(&self) -> &'static str;
53
54 fn default_base_url(&self) -> &'static str;
56
57 fn default_model(&self) -> &'static str;
59
60 fn env_vars(&self) -> &'static [&'static str];
62
63 fn provider_config_key(&self) -> &'static str;
65
66 fn aliases(&self) -> &'static [&'static str] {
68 &[]
69 }
70
71 fn wire(&self) -> WireFormat {
73 WireFormat::ChatCompletions
74 }
75}
76
77macro_rules! provider {
78 (
79 $struct_name:ident,
80 $kind:ident,
81 $id:literal,
82 $display_name:literal,
83 $base_url:ident,
84 $model:ident,
85 [$($env_var:literal),* $(,)?],
86 $config_key:literal,
87 aliases: [$($alias:literal),* $(,)?]
88 ) => {
89 pub struct $struct_name;
91
92 impl Provider for $struct_name {
93 fn id(&self) -> &'static str {
94 $id
95 }
96
97 fn kind(&self) -> ProviderKind {
98 ProviderKind::$kind
99 }
100
101 fn display_name(&self) -> &'static str {
102 $display_name
103 }
104
105 fn default_base_url(&self) -> &'static str {
106 $base_url
107 }
108
109 fn default_model(&self) -> &'static str {
110 $model
111 }
112
113 fn env_vars(&self) -> &'static [&'static str] {
114 &[$($env_var),*]
115 }
116
117 fn provider_config_key(&self) -> &'static str {
118 $config_key
119 }
120
121 fn aliases(&self) -> &'static [&'static str] {
122 &[$($alias),*]
123 }
124 }
125 };
126}
127
128provider!(
129 Deepseek,
130 Deepseek,
131 "deepseek",
132 "DeepSeek",
133 DEFAULT_DEEPSEEK_BASE_URL,
134 DEFAULT_DEEPSEEK_MODEL,
135 ["DEEPSEEK_API_KEY"],
136 "deepseek",
137 aliases: ["deep-seek", "deepseek-cn", "deepseek_china", "deepseekcn", "deepseek-china"]
138);
139
140pub struct DeepseekAnthropic;
142
143impl Provider for DeepseekAnthropic {
144 fn id(&self) -> &'static str {
145 "deepseek-anthropic"
146 }
147
148 fn kind(&self) -> ProviderKind {
149 ProviderKind::DeepseekAnthropic
150 }
151
152 fn display_name(&self) -> &'static str {
153 "DeepSeek (Anthropic-compatible)"
154 }
155
156 fn default_base_url(&self) -> &'static str {
157 DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
158 }
159
160 fn default_model(&self) -> &'static str {
161 DEFAULT_DEEPSEEK_ANTHROPIC_MODEL
162 }
163
164 fn env_vars(&self) -> &'static [&'static str] {
165 &["DEEPSEEK_API_KEY"]
166 }
167
168 fn provider_config_key(&self) -> &'static str {
169 "deepseek_anthropic"
170 }
171
172 fn aliases(&self) -> &'static [&'static str] {
173 &["deepseek_anthropic", "deepseek-claude", "deepseek_claude"]
174 }
175
176 fn wire(&self) -> WireFormat {
177 WireFormat::AnthropicMessages
178 }
179}
180provider!(
181 NvidiaNim,
182 NvidiaNim,
183 "nvidia-nim",
184 "NVIDIA NIM",
185 DEFAULT_NVIDIA_NIM_BASE_URL,
186 DEFAULT_NVIDIA_NIM_MODEL,
187 ["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
188 "nvidia_nim",
189 aliases: ["nvidia", "nvidia_nim", "nim"]
190);
191provider!(
192 Openai,
193 Openai,
194 "openai",
195 "OpenAI-compatible",
196 DEFAULT_OPENAI_BASE_URL,
197 DEFAULT_OPENAI_MODEL,
198 ["OPENAI_API_KEY"],
199 "openai",
200 aliases: ["open-ai"]
201);
202provider!(
203 Atlascloud,
204 Atlascloud,
205 "atlascloud",
206 "AtlasCloud",
207 DEFAULT_ATLASCLOUD_BASE_URL,
208 DEFAULT_ATLASCLOUD_MODEL,
209 ["ATLASCLOUD_API_KEY"],
210 "atlascloud",
211 aliases: ["atlas-cloud", "atlas_cloud", "atlas"]
212);
213provider!(
214 WanjieArk,
215 WanjieArk,
216 "wanjie-ark",
217 "Wanjie Ark",
218 DEFAULT_WANJIE_ARK_BASE_URL,
219 DEFAULT_WANJIE_ARK_MODEL,
220 [
221 "WANJIE_ARK_API_KEY",
222 "WANJIE_API_KEY",
223 "WANJIE_MAAS_API_KEY"
224 ],
225 "wanjie_ark",
226 aliases: ["wanjie", "wanjie_ark", "ark-wanjie", "ark_wanjie", "wanjieark", "wanjie-maas", "wanjie_maas", "wanjiemaas"]
227);
228provider!(
229 Volcengine,
230 Volcengine,
231 "volcengine",
232 "Volcengine Ark",
233 DEFAULT_VOLCENGINE_BASE_URL,
234 DEFAULT_VOLCENGINE_MODEL,
235 [
236 "VOLCENGINE_API_KEY",
237 "VOLCENGINE_ARK_API_KEY",
238 "ARK_API_KEY"
239 ],
240 "volcengine",
241 aliases: ["volcengine-ark", "volcengine_ark", "ark", "volc-ark", "volcengineark"]
242);
243provider!(
244 Openrouter,
245 Openrouter,
246 "openrouter",
247 "OpenRouter",
248 DEFAULT_OPENROUTER_BASE_URL,
249 DEFAULT_OPENROUTER_MODEL,
250 ["OPENROUTER_API_KEY"],
251 "openrouter",
252 aliases: ["open_router"]
253);
254provider!(
255 XiaomiMimo,
256 XiaomiMimo,
257 "xiaomi-mimo",
258 "Xiaomi MiMo",
259 DEFAULT_XIAOMI_MIMO_BASE_URL,
260 DEFAULT_XIAOMI_MIMO_MODEL,
261 [
262 "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
263 "MIMO_TOKEN_PLAN_API_KEY",
264 "XIAOMI_MIMO_API_KEY",
265 "XIAOMI_API_KEY",
266 "MIMO_API_KEY",
267 ],
268 "xiaomi_mimo",
269 aliases: ["xiaomi_mimo", "xiaomimimo", "mimo", "xiaomi"]
270);
271provider!(
272 Novita,
273 Novita,
274 "novita",
275 "Novita AI",
276 DEFAULT_NOVITA_BASE_URL,
277 DEFAULT_NOVITA_MODEL,
278 ["NOVITA_API_KEY"],
279 "novita",
280 aliases: []
281);
282provider!(
283 Fireworks,
284 Fireworks,
285 "fireworks",
286 "Fireworks AI",
287 DEFAULT_FIREWORKS_BASE_URL,
288 DEFAULT_FIREWORKS_MODEL,
289 ["FIREWORKS_API_KEY"],
290 "fireworks",
291 aliases: ["fireworks-ai"]
292);
293provider!(
294 Siliconflow,
295 Siliconflow,
296 "siliconflow",
297 "SiliconFlow",
298 DEFAULT_SILICONFLOW_BASE_URL,
299 DEFAULT_SILICONFLOW_MODEL,
300 ["SILICONFLOW_API_KEY"],
301 "siliconflow",
302 aliases: ["silicon-flow", "silicon_flow"]
303);
304provider!(
305 SiliconflowCN,
306 SiliconflowCN,
307 "siliconflow-CN",
308 "SiliconFlow (China)",
309 DEFAULT_SILICONFLOW_CN_BASE_URL,
310 DEFAULT_SILICONFLOW_MODEL,
311 ["SILICONFLOW_API_KEY"],
312 "siliconflow_cn",
313 aliases: [
314 "silicon-flow-cn",
315 "silicon-flow-CN",
316 "silicon_flow_cn",
317 "silicon_flow_CN",
318 "siliconflow-china",
319 ]
320);
321provider!(
322 Arcee,
323 Arcee,
324 "arcee",
325 "Arcee AI",
326 DEFAULT_ARCEE_BASE_URL,
327 DEFAULT_ARCEE_MODEL,
328 ["ARCEE_API_KEY"],
329 "arcee",
330 aliases: ["arcee-ai", "arcee_ai"]
331);
332provider!(
333 Moonshot,
334 Moonshot,
335 "moonshot",
336 "Moonshot/Kimi",
337 DEFAULT_MOONSHOT_BASE_URL,
338 DEFAULT_MOONSHOT_MODEL,
339 ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
340 "moonshot",
341 aliases: ["moonshot-ai", "kimi", "kimi-k2"]
342);
343provider!(
344 Sglang,
345 Sglang,
346 "sglang",
347 "SGLang",
348 DEFAULT_SGLANG_BASE_URL,
349 DEFAULT_SGLANG_MODEL,
350 ["SGLANG_API_KEY"],
351 "sglang",
352 aliases: ["sg-lang"]
353);
354provider!(
355 Vllm,
356 Vllm,
357 "vllm",
358 "vLLM",
359 DEFAULT_VLLM_BASE_URL,
360 DEFAULT_VLLM_MODEL,
361 ["VLLM_API_KEY"],
362 "vllm",
363 aliases: ["v-llm"]
364);
365provider!(
366 Ollama,
367 Ollama,
368 "ollama",
369 "Ollama",
370 DEFAULT_OLLAMA_BASE_URL,
371 DEFAULT_OLLAMA_MODEL,
372 ["OLLAMA_API_KEY"],
373 "ollama",
374 aliases: ["ollama-local"]
375);
376provider!(
377 Huggingface,
378 Huggingface,
379 "huggingface",
380 "Hugging Face",
381 DEFAULT_HUGGINGFACE_BASE_URL,
382 DEFAULT_HUGGINGFACE_MODEL,
383 ["HUGGINGFACE_API_KEY", "HF_TOKEN"],
384 "huggingface",
385 aliases: ["hugging-face", "hugging_face", "hf"]
386);
387provider!(
388 Together,
389 Together,
390 "together",
391 "Together AI",
392 DEFAULT_TOGETHER_BASE_URL,
393 DEFAULT_TOGETHER_MODEL,
394 ["TOGETHER_API_KEY"],
395 "together",
396 aliases: ["together-ai", "together_ai"]
397);
398provider!(
399 Qianfan,
400 Qianfan,
401 "qianfan",
402 "Baidu Qianfan",
403 DEFAULT_QIANFAN_BASE_URL,
404 DEFAULT_QIANFAN_MODEL,
405 ["QIANFAN_API_KEY", "BAIDU_QIANFAN_API_KEY"],
406 "qianfan",
407 aliases: ["baidu-qianfan", "baidu_qianfan", "baidu"]
408);
409
410pub struct OpenaiCodex;
412
413impl Provider for OpenaiCodex {
414 fn id(&self) -> &'static str {
415 "openai-codex"
416 }
417
418 fn kind(&self) -> ProviderKind {
419 ProviderKind::OpenaiCodex
420 }
421
422 fn display_name(&self) -> &'static str {
423 "OpenAI Codex (ChatGPT)"
424 }
425
426 fn default_base_url(&self) -> &'static str {
427 DEFAULT_OPENAI_CODEX_BASE_URL
428 }
429
430 fn default_model(&self) -> &'static str {
431 DEFAULT_OPENAI_CODEX_MODEL
432 }
433
434 fn env_vars(&self) -> &'static [&'static str] {
435 &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"]
436 }
437
438 fn provider_config_key(&self) -> &'static str {
439 "openai_codex"
440 }
441
442 fn aliases(&self) -> &'static [&'static str] {
443 &[
444 "openai_codex",
445 "openaicodex",
446 "codex",
447 "chatgpt",
448 "chatgpt-codex",
449 "chatgpt_codex",
450 "chatgptcodex",
451 ]
452 }
453
454 fn wire(&self) -> WireFormat {
455 WireFormat::Responses
456 }
457}
458
459pub struct Anthropic;
461
462impl Provider for Anthropic {
463 fn id(&self) -> &'static str {
464 "anthropic"
465 }
466
467 fn kind(&self) -> ProviderKind {
468 ProviderKind::Anthropic
469 }
470
471 fn display_name(&self) -> &'static str {
472 "Anthropic"
473 }
474
475 fn default_base_url(&self) -> &'static str {
476 crate::DEFAULT_ANTHROPIC_BASE_URL
477 }
478
479 fn default_model(&self) -> &'static str {
480 crate::DEFAULT_ANTHROPIC_MODEL
481 }
482
483 fn env_vars(&self) -> &'static [&'static str] {
484 &["ANTHROPIC_API_KEY"]
485 }
486
487 fn provider_config_key(&self) -> &'static str {
488 "anthropic"
489 }
490
491 fn wire(&self) -> WireFormat {
492 WireFormat::AnthropicMessages
493 }
494}
495
496pub struct Openmodel;
498
499impl Provider for Openmodel {
500 fn id(&self) -> &'static str {
501 "openmodel"
502 }
503
504 fn kind(&self) -> ProviderKind {
505 ProviderKind::Openmodel
506 }
507
508 fn display_name(&self) -> &'static str {
509 "OpenModel"
510 }
511
512 fn default_base_url(&self) -> &'static str {
513 DEFAULT_OPENMODEL_BASE_URL
514 }
515
516 fn default_model(&self) -> &'static str {
517 DEFAULT_OPENMODEL_MODEL
518 }
519
520 fn env_vars(&self) -> &'static [&'static str] {
521 &["OPENMODEL_API_KEY"]
522 }
523
524 fn provider_config_key(&self) -> &'static str {
525 "openmodel"
526 }
527
528 fn aliases(&self) -> &'static [&'static str] {
529 &["open-model", "open_model"]
530 }
531
532 fn wire(&self) -> WireFormat {
533 WireFormat::AnthropicMessages
534 }
535}
536
537provider!(
538 Zai,
539 Zai,
540 "zai",
541 "Zhipu AI / Z.ai",
542 DEFAULT_ZAI_BASE_URL,
543 DEFAULT_ZAI_MODEL,
544 ["ZAI_API_KEY", "Z_AI_API_KEY", "ZHIPU_API_KEY", "GLM_API_KEY"],
545 "zai",
546 aliases: ["z-ai", "z_ai", "z.ai", "zhipu", "zhipuai", "bigmodel", "big-model"]
547);
548
549provider!(
550 Stepfun,
551 Stepfun,
552 "stepfun",
553 "StepFun / StepFlash",
554 DEFAULT_STEPFUN_BASE_URL,
555 DEFAULT_STEPFUN_MODEL,
556 ["STEPFUN_API_KEY", "STEP_API_KEY"],
557 "stepfun",
558 aliases: ["step-fun", "step_fun", "stepflash", "step-flash", "step_flash"]
559);
560
561provider!(
562 Minimax,
563 Minimax,
564 "minimax",
565 "MiniMax",
566 DEFAULT_MINIMAX_BASE_URL,
567 DEFAULT_MINIMAX_MODEL,
568 ["MINIMAX_API_KEY"],
569 "minimax",
570 aliases: ["mini-max", "mini_max"]
571);
572
573provider!(
574 Deepinfra,
575 Deepinfra,
576 "deepinfra",
577 "DeepInfra",
578 DEFAULT_DEEPINFRA_BASE_URL,
579 DEFAULT_DEEPINFRA_MODEL,
580 ["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
581 "deepinfra",
582 aliases: ["deep-infra", "deep_infra"]
583);
584
585provider!(
586 Sakana,
587 Sakana,
588 "sakana",
589 "Sakana AI (Fugu)",
590 DEFAULT_SAKANA_BASE_URL,
591 DEFAULT_SAKANA_MODEL,
592 ["FUGU_API_KEY", "SAKANA_API_KEY"],
593 "sakana",
594 aliases: ["sakana-ai", "sakana_ai", "fugu"]
595);
596
597pub struct Custom;
608
609impl Provider for Custom {
610 fn id(&self) -> &'static str {
611 "custom"
612 }
613
614 fn kind(&self) -> ProviderKind {
615 ProviderKind::Custom
616 }
617
618 fn display_name(&self) -> &'static str {
619 "Custom (OpenAI-compatible)"
620 }
621
622 fn default_base_url(&self) -> &'static str {
623 "http://localhost/v1"
627 }
628
629 fn default_model(&self) -> &'static str {
630 "custom-model"
633 }
634
635 fn env_vars(&self) -> &'static [&'static str] {
636 &[]
639 }
640
641 fn provider_config_key(&self) -> &'static str {
642 "custom"
643 }
644
645 fn wire(&self) -> WireFormat {
646 WireFormat::ChatCompletions
647 }
648}
649
650static DEEPSEEK: Deepseek = Deepseek;
651static DEEPSEEK_ANTHROPIC: DeepseekAnthropic = DeepseekAnthropic;
652static NVIDIA_NIM: NvidiaNim = NvidiaNim;
653static OPENAI: Openai = Openai;
654static ATLASCLOUD: Atlascloud = Atlascloud;
655static WANJIE_ARK: WanjieArk = WanjieArk;
656static VOLCENGINE: Volcengine = Volcengine;
657static OPENROUTER: Openrouter = Openrouter;
658static XIAOMI_MIMO: XiaomiMimo = XiaomiMimo;
659static NOVITA: Novita = Novita;
660static FIREWORKS: Fireworks = Fireworks;
661static SILICONFLOW: Siliconflow = Siliconflow;
662static SILICONFLOW_CN: SiliconflowCN = SiliconflowCN;
663static ARCEE: Arcee = Arcee;
664static MOONSHOT: Moonshot = Moonshot;
665static SGLANG: Sglang = Sglang;
666static VLLM: Vllm = Vllm;
667static OLLAMA: Ollama = Ollama;
668static HUGGINGFACE: Huggingface = Huggingface;
669static TOGETHER: Together = Together;
670static QIANFAN: Qianfan = Qianfan;
671static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
672static ANTHROPIC: Anthropic = Anthropic;
673static OPENMODEL: Openmodel = Openmodel;
674static ZAI: Zai = Zai;
675static STEPFUN: Stepfun = Stepfun;
676static MINIMAX: Minimax = Minimax;
677static DEEPINFRA: Deepinfra = Deepinfra;
678static SAKANA: Sakana = Sakana;
679static CUSTOM: Custom = Custom;
680
681static PROVIDER_REGISTRY: [&dyn Provider; 30] = [
682 &DEEPSEEK,
683 &DEEPSEEK_ANTHROPIC,
684 &NVIDIA_NIM,
685 &OPENAI,
686 &ATLASCLOUD,
687 &WANJIE_ARK,
688 &VOLCENGINE,
689 &OPENROUTER,
690 &XIAOMI_MIMO,
691 &NOVITA,
692 &FIREWORKS,
693 &SILICONFLOW,
694 &ARCEE,
695 &SILICONFLOW_CN,
696 &MOONSHOT,
697 &SGLANG,
698 &VLLM,
699 &OLLAMA,
700 &HUGGINGFACE,
701 &TOGETHER,
702 &QIANFAN,
703 &OPENAI_CODEX,
704 &ANTHROPIC,
705 &OPENMODEL,
706 &ZAI,
707 &STEPFUN,
708 &MINIMAX,
709 &DEEPINFRA,
710 &SAKANA,
711 &CUSTOM,
712];
713
714#[must_use]
720pub fn all_providers() -> &'static [&'static dyn Provider] {
721 &PROVIDER_REGISTRY
722}
723
724#[must_use]
740pub fn providers_sorted_for_display() -> Vec<&'static dyn Provider> {
741 let mut providers = all_providers().to_vec();
742 providers.sort_by(|a, b| {
743 a.display_name()
744 .to_ascii_lowercase()
745 .cmp(&b.display_name().to_ascii_lowercase())
746 });
747 providers
748}
749
750#[must_use]
752pub fn lookup_provider(id: &str) -> Option<&'static dyn Provider> {
753 let id = id.trim();
754 all_providers()
755 .iter()
756 .copied()
757 .find(|provider| provider.id() == id)
758}
759
760#[must_use]
762pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
763 ProviderKind::parse(id_or_alias).map(provider_for_kind)
764}
765
766#[must_use]
768pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
769 PROVIDER_REGISTRY
770 .iter()
771 .find(|p| p.kind() == kind)
772 .copied()
773 .expect("ProviderKind variant missing from PROVIDER_REGISTRY")
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779
780 #[test]
781 fn display_order_is_alphabetical_by_display_name() {
782 let display = providers_sorted_for_display();
783 let names: Vec<String> = display
784 .iter()
785 .map(|p| p.display_name().to_ascii_lowercase())
786 .collect();
787 let mut sorted = names.clone();
788 sorted.sort();
789 assert_eq!(
790 names, sorted,
791 "providers_sorted_for_display must be alphabetical (case-insensitive) by display name"
792 );
793 }
794
795 #[test]
796 fn display_order_differs_from_internal_all_order() {
797 let display_ids: Vec<&str> = providers_sorted_for_display()
800 .iter()
801 .map(|p| p.id())
802 .collect();
803 let internal_ids: Vec<&str> = all_providers().iter().map(|p| p.id()).collect();
804 assert_ne!(
805 display_ids, internal_ids,
806 "display order should not match internal ALL order"
807 );
808 }
809
810 #[test]
811 fn display_order_is_complete_and_unique() {
812 let display = providers_sorted_for_display();
814 assert_eq!(
815 display.len(),
816 all_providers().len(),
817 "display order must include every built-in provider"
818 );
819 let mut ids: Vec<&str> = display.iter().map(|p| p.id()).collect();
820 ids.sort_unstable();
821 let before = ids.len();
822 ids.dedup();
823 assert_eq!(
824 before,
825 ids.len(),
826 "display order must not contain duplicates"
827 );
828 }
829
830 #[test]
831 fn deepseek_is_present_but_not_first_in_display_order() {
832 let display = providers_sorted_for_display();
835 assert_eq!(
836 all_providers()[0].kind(),
837 ProviderKind::Deepseek,
838 "DeepSeek is expected to remain first in the stable internal order"
839 );
840 assert!(
841 display.iter().any(|p| p.kind() == ProviderKind::Deepseek),
842 "DeepSeek must remain present in display order"
843 );
844 assert_ne!(
845 display[0].kind(),
846 ProviderKind::Deepseek,
847 "DeepSeek must not be hard-coded first in display order"
848 );
849 assert_eq!(
852 display[0].display_name(),
853 "Anthropic",
854 "alphabetical display order should lead with Anthropic"
855 );
856 }
857}