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_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_MINIMAX_BASE_URL,
14 DEFAULT_MINIMAX_MODEL, DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL,
15 DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL,
16 DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL,
17 DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL,
18 DEFAULT_OPENAI_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL,
19 DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL,
20 DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL,
21 DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL,
22 DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL,
23 DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL,
24 DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL,
25 DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL,
26 DEFAULT_ZAI_BASE_URL, 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
597provider!(
598 LongCat,
599 LongCat,
600 "longcat",
601 "Meituan LongCat",
602 DEFAULT_LONGCAT_BASE_URL,
603 DEFAULT_LONGCAT_MODEL,
604 ["LONGCAT_API_KEY"],
605 "longcat",
606 aliases: ["long-cat", "meituan-longcat", "meituan"]
607);
608
609pub struct Custom;
620
621impl Provider for Custom {
622 fn id(&self) -> &'static str {
623 "custom"
624 }
625
626 fn kind(&self) -> ProviderKind {
627 ProviderKind::Custom
628 }
629
630 fn display_name(&self) -> &'static str {
631 "Custom (OpenAI-compatible)"
632 }
633
634 fn default_base_url(&self) -> &'static str {
635 "http://localhost/v1"
639 }
640
641 fn default_model(&self) -> &'static str {
642 "custom-model"
645 }
646
647 fn env_vars(&self) -> &'static [&'static str] {
648 &[]
651 }
652
653 fn provider_config_key(&self) -> &'static str {
654 "custom"
655 }
656
657 fn wire(&self) -> WireFormat {
658 WireFormat::ChatCompletions
659 }
660}
661
662static DEEPSEEK: Deepseek = Deepseek;
663static DEEPSEEK_ANTHROPIC: DeepseekAnthropic = DeepseekAnthropic;
664static NVIDIA_NIM: NvidiaNim = NvidiaNim;
665static OPENAI: Openai = Openai;
666static ATLASCLOUD: Atlascloud = Atlascloud;
667static WANJIE_ARK: WanjieArk = WanjieArk;
668static VOLCENGINE: Volcengine = Volcengine;
669static OPENROUTER: Openrouter = Openrouter;
670static XIAOMI_MIMO: XiaomiMimo = XiaomiMimo;
671static NOVITA: Novita = Novita;
672static FIREWORKS: Fireworks = Fireworks;
673static SILICONFLOW: Siliconflow = Siliconflow;
674static SILICONFLOW_CN: SiliconflowCN = SiliconflowCN;
675static ARCEE: Arcee = Arcee;
676static MOONSHOT: Moonshot = Moonshot;
677static SGLANG: Sglang = Sglang;
678static VLLM: Vllm = Vllm;
679static OLLAMA: Ollama = Ollama;
680static HUGGINGFACE: Huggingface = Huggingface;
681static TOGETHER: Together = Together;
682static QIANFAN: Qianfan = Qianfan;
683static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
684static ANTHROPIC: Anthropic = Anthropic;
685static OPENMODEL: Openmodel = Openmodel;
686static ZAI: Zai = Zai;
687static STEPFUN: Stepfun = Stepfun;
688static MINIMAX: Minimax = Minimax;
689static DEEPINFRA: Deepinfra = Deepinfra;
690static SAKANA: Sakana = Sakana;
691static LONGCAT: LongCat = LongCat;
692static CUSTOM: Custom = Custom;
693
694static PROVIDER_REGISTRY: [&dyn Provider; 31] = [
695 &DEEPSEEK,
696 &DEEPSEEK_ANTHROPIC,
697 &NVIDIA_NIM,
698 &OPENAI,
699 &ATLASCLOUD,
700 &WANJIE_ARK,
701 &VOLCENGINE,
702 &OPENROUTER,
703 &XIAOMI_MIMO,
704 &NOVITA,
705 &FIREWORKS,
706 &SILICONFLOW,
707 &ARCEE,
708 &SILICONFLOW_CN,
709 &MOONSHOT,
710 &SGLANG,
711 &VLLM,
712 &OLLAMA,
713 &HUGGINGFACE,
714 &TOGETHER,
715 &QIANFAN,
716 &OPENAI_CODEX,
717 &ANTHROPIC,
718 &OPENMODEL,
719 &ZAI,
720 &STEPFUN,
721 &MINIMAX,
722 &DEEPINFRA,
723 &SAKANA,
724 &LONGCAT,
725 &CUSTOM,
726];
727
728#[must_use]
734pub fn all_providers() -> &'static [&'static dyn Provider] {
735 &PROVIDER_REGISTRY
736}
737
738#[must_use]
754pub fn providers_sorted_for_display() -> Vec<&'static dyn Provider> {
755 let mut providers = all_providers().to_vec();
756 providers.sort_by(|a, b| {
757 a.display_name()
758 .to_ascii_lowercase()
759 .cmp(&b.display_name().to_ascii_lowercase())
760 });
761 providers
762}
763
764#[must_use]
766pub fn lookup_provider(id: &str) -> Option<&'static dyn Provider> {
767 let id = id.trim();
768 all_providers()
769 .iter()
770 .copied()
771 .find(|provider| provider.id() == id)
772}
773
774#[must_use]
776pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
777 ProviderKind::parse(id_or_alias).map(provider_for_kind)
778}
779
780#[must_use]
782pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
783 PROVIDER_REGISTRY
784 .iter()
785 .find(|p| p.kind() == kind)
786 .copied()
787 .expect("ProviderKind variant missing from PROVIDER_REGISTRY")
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 #[test]
795 fn display_order_is_alphabetical_by_display_name() {
796 let display = providers_sorted_for_display();
797 let names: Vec<String> = display
798 .iter()
799 .map(|p| p.display_name().to_ascii_lowercase())
800 .collect();
801 let mut sorted = names.clone();
802 sorted.sort();
803 assert_eq!(
804 names, sorted,
805 "providers_sorted_for_display must be alphabetical (case-insensitive) by display name"
806 );
807 }
808
809 #[test]
810 fn display_order_differs_from_internal_all_order() {
811 let display_ids: Vec<&str> = providers_sorted_for_display()
814 .iter()
815 .map(|p| p.id())
816 .collect();
817 let internal_ids: Vec<&str> = all_providers().iter().map(|p| p.id()).collect();
818 assert_ne!(
819 display_ids, internal_ids,
820 "display order should not match internal ALL order"
821 );
822 }
823
824 #[test]
825 fn display_order_is_complete_and_unique() {
826 let display = providers_sorted_for_display();
828 assert_eq!(
829 display.len(),
830 all_providers().len(),
831 "display order must include every built-in provider"
832 );
833 let mut ids: Vec<&str> = display.iter().map(|p| p.id()).collect();
834 ids.sort_unstable();
835 let before = ids.len();
836 ids.dedup();
837 assert_eq!(
838 before,
839 ids.len(),
840 "display order must not contain duplicates"
841 );
842 }
843
844 #[test]
845 fn deepseek_is_present_but_not_first_in_display_order() {
846 let display = providers_sorted_for_display();
849 assert_eq!(
850 all_providers()[0].kind(),
851 ProviderKind::Deepseek,
852 "DeepSeek is expected to remain first in the stable internal order"
853 );
854 assert!(
855 display.iter().any(|p| p.kind() == ProviderKind::Deepseek),
856 "DeepSeek must remain present in display order"
857 );
858 assert_ne!(
859 display[0].kind(),
860 ProviderKind::Deepseek,
861 "DeepSeek must not be hard-coded first in display order"
862 );
863 assert_eq!(
866 display[0].display_name(),
867 "Anthropic",
868 "alphabetical display order should lead with Anthropic"
869 );
870 }
871}