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_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL,
11 DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
12 DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MOONSHOT_BASE_URL,
13 DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL,
14 DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL,
15 DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL,
16 DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENROUTER_BASE_URL,
17 DEFAULT_OPENROUTER_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL,
18 DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL,
19 DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL,
20 DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
21 DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
22 DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
23 DEFAULT_ZAI_MODEL, ProviderKind,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WireFormat {
29 ChatCompletions,
31 Responses,
33 AnthropicMessages,
35}
36
37pub trait Provider: Send + Sync {
39 fn kind(&self) -> ProviderKind;
41
42 fn id(&self) -> &'static str {
44 self.kind().as_str()
45 }
46
47 fn display_name(&self) -> &'static str;
49
50 fn default_base_url(&self) -> &'static str;
52
53 fn default_model(&self) -> &'static str;
55
56 fn env_vars(&self) -> &'static [&'static str];
58
59 fn provider_config_key(&self) -> &'static str;
61
62 fn aliases(&self) -> &'static [&'static str] {
64 &[]
65 }
66
67 fn wire(&self) -> WireFormat {
69 WireFormat::ChatCompletions
70 }
71}
72
73macro_rules! provider {
74 (
75 $struct_name:ident,
76 $kind:ident,
77 $id:literal,
78 $display_name:literal,
79 $base_url:ident,
80 $model:ident,
81 [$($env_var:literal),* $(,)?],
82 $config_key:literal,
83 aliases: [$($alias:literal),* $(,)?]
84 ) => {
85 pub struct $struct_name;
87
88 impl Provider for $struct_name {
89 fn id(&self) -> &'static str {
90 $id
91 }
92
93 fn kind(&self) -> ProviderKind {
94 ProviderKind::$kind
95 }
96
97 fn display_name(&self) -> &'static str {
98 $display_name
99 }
100
101 fn default_base_url(&self) -> &'static str {
102 $base_url
103 }
104
105 fn default_model(&self) -> &'static str {
106 $model
107 }
108
109 fn env_vars(&self) -> &'static [&'static str] {
110 &[$($env_var),*]
111 }
112
113 fn provider_config_key(&self) -> &'static str {
114 $config_key
115 }
116
117 fn aliases(&self) -> &'static [&'static str] {
118 &[$($alias),*]
119 }
120 }
121 };
122}
123
124provider!(
125 Deepseek,
126 Deepseek,
127 "deepseek",
128 "DeepSeek",
129 DEFAULT_DEEPSEEK_BASE_URL,
130 DEFAULT_DEEPSEEK_MODEL,
131 ["DEEPSEEK_API_KEY"],
132 "deepseek",
133 aliases: ["deep-seek", "deepseek-cn", "deepseek_china", "deepseekcn", "deepseek-china"]
134);
135provider!(
136 NvidiaNim,
137 NvidiaNim,
138 "nvidia-nim",
139 "NVIDIA NIM",
140 DEFAULT_NVIDIA_NIM_BASE_URL,
141 DEFAULT_NVIDIA_NIM_MODEL,
142 ["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
143 "nvidia_nim",
144 aliases: ["nvidia", "nvidia_nim", "nim"]
145);
146provider!(
147 Openai,
148 Openai,
149 "openai",
150 "OpenAI-compatible",
151 DEFAULT_OPENAI_BASE_URL,
152 DEFAULT_OPENAI_MODEL,
153 ["OPENAI_API_KEY"],
154 "openai",
155 aliases: ["open-ai"]
156);
157provider!(
158 Atlascloud,
159 Atlascloud,
160 "atlascloud",
161 "AtlasCloud",
162 DEFAULT_ATLASCLOUD_BASE_URL,
163 DEFAULT_ATLASCLOUD_MODEL,
164 ["ATLASCLOUD_API_KEY"],
165 "atlascloud",
166 aliases: ["atlas-cloud", "atlas_cloud", "atlas"]
167);
168provider!(
169 WanjieArk,
170 WanjieArk,
171 "wanjie-ark",
172 "Wanjie Ark",
173 DEFAULT_WANJIE_ARK_BASE_URL,
174 DEFAULT_WANJIE_ARK_MODEL,
175 [
176 "WANJIE_ARK_API_KEY",
177 "WANJIE_API_KEY",
178 "WANJIE_MAAS_API_KEY"
179 ],
180 "wanjie_ark",
181 aliases: ["wanjie", "wanjie_ark", "ark-wanjie", "ark_wanjie", "wanjieark", "wanjie-maas", "wanjie_maas", "wanjiemaas"]
182);
183provider!(
184 Volcengine,
185 Volcengine,
186 "volcengine",
187 "Volcengine Ark",
188 DEFAULT_VOLCENGINE_BASE_URL,
189 DEFAULT_VOLCENGINE_MODEL,
190 [
191 "VOLCENGINE_API_KEY",
192 "VOLCENGINE_ARK_API_KEY",
193 "ARK_API_KEY"
194 ],
195 "volcengine",
196 aliases: ["volcengine-ark", "volcengine_ark", "ark", "volc-ark", "volcengineark"]
197);
198provider!(
199 Openrouter,
200 Openrouter,
201 "openrouter",
202 "OpenRouter",
203 DEFAULT_OPENROUTER_BASE_URL,
204 DEFAULT_OPENROUTER_MODEL,
205 ["OPENROUTER_API_KEY"],
206 "openrouter",
207 aliases: ["open_router"]
208);
209provider!(
210 XiaomiMimo,
211 XiaomiMimo,
212 "xiaomi-mimo",
213 "Xiaomi MiMo",
214 DEFAULT_XIAOMI_MIMO_BASE_URL,
215 DEFAULT_XIAOMI_MIMO_MODEL,
216 [
217 "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
218 "MIMO_TOKEN_PLAN_API_KEY",
219 "XIAOMI_MIMO_API_KEY",
220 "XIAOMI_API_KEY",
221 "MIMO_API_KEY",
222 ],
223 "xiaomi_mimo",
224 aliases: ["xiaomi_mimo", "xiaomimimo", "mimo", "xiaomi"]
225);
226provider!(
227 Novita,
228 Novita,
229 "novita",
230 "Novita AI",
231 DEFAULT_NOVITA_BASE_URL,
232 DEFAULT_NOVITA_MODEL,
233 ["NOVITA_API_KEY"],
234 "novita",
235 aliases: []
236);
237provider!(
238 Fireworks,
239 Fireworks,
240 "fireworks",
241 "Fireworks AI",
242 DEFAULT_FIREWORKS_BASE_URL,
243 DEFAULT_FIREWORKS_MODEL,
244 ["FIREWORKS_API_KEY"],
245 "fireworks",
246 aliases: ["fireworks-ai"]
247);
248provider!(
249 Siliconflow,
250 Siliconflow,
251 "siliconflow",
252 "SiliconFlow",
253 DEFAULT_SILICONFLOW_BASE_URL,
254 DEFAULT_SILICONFLOW_MODEL,
255 ["SILICONFLOW_API_KEY"],
256 "siliconflow",
257 aliases: ["silicon-flow", "silicon_flow"]
258);
259provider!(
260 SiliconflowCN,
261 SiliconflowCN,
262 "siliconflow-CN",
263 "SiliconFlow (China)",
264 DEFAULT_SILICONFLOW_CN_BASE_URL,
265 DEFAULT_SILICONFLOW_MODEL,
266 ["SILICONFLOW_API_KEY"],
267 "siliconflow_cn",
268 aliases: [
269 "silicon-flow-cn",
270 "silicon-flow-CN",
271 "silicon_flow_cn",
272 "silicon_flow_CN",
273 "siliconflow-china",
274 ]
275);
276provider!(
277 Arcee,
278 Arcee,
279 "arcee",
280 "Arcee AI",
281 DEFAULT_ARCEE_BASE_URL,
282 DEFAULT_ARCEE_MODEL,
283 ["ARCEE_API_KEY"],
284 "arcee",
285 aliases: ["arcee-ai", "arcee_ai"]
286);
287provider!(
288 Moonshot,
289 Moonshot,
290 "moonshot",
291 "Moonshot/Kimi",
292 DEFAULT_MOONSHOT_BASE_URL,
293 DEFAULT_MOONSHOT_MODEL,
294 ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
295 "moonshot",
296 aliases: ["moonshot-ai", "kimi", "kimi-k2"]
297);
298provider!(
299 Sglang,
300 Sglang,
301 "sglang",
302 "SGLang",
303 DEFAULT_SGLANG_BASE_URL,
304 DEFAULT_SGLANG_MODEL,
305 ["SGLANG_API_KEY"],
306 "sglang",
307 aliases: ["sg-lang"]
308);
309provider!(
310 Vllm,
311 Vllm,
312 "vllm",
313 "vLLM",
314 DEFAULT_VLLM_BASE_URL,
315 DEFAULT_VLLM_MODEL,
316 ["VLLM_API_KEY"],
317 "vllm",
318 aliases: ["v-llm"]
319);
320provider!(
321 Ollama,
322 Ollama,
323 "ollama",
324 "Ollama",
325 DEFAULT_OLLAMA_BASE_URL,
326 DEFAULT_OLLAMA_MODEL,
327 ["OLLAMA_API_KEY"],
328 "ollama",
329 aliases: ["ollama-local"]
330);
331provider!(
332 Huggingface,
333 Huggingface,
334 "huggingface",
335 "Hugging Face",
336 DEFAULT_HUGGINGFACE_BASE_URL,
337 DEFAULT_HUGGINGFACE_MODEL,
338 ["HUGGINGFACE_API_KEY", "HF_TOKEN"],
339 "huggingface",
340 aliases: ["hugging-face", "hugging_face", "hf"]
341);
342provider!(
343 Together,
344 Together,
345 "together",
346 "Together AI",
347 DEFAULT_TOGETHER_BASE_URL,
348 DEFAULT_TOGETHER_MODEL,
349 ["TOGETHER_API_KEY"],
350 "together",
351 aliases: ["together-ai", "together_ai"]
352);
353
354pub struct OpenaiCodex;
356
357impl Provider for OpenaiCodex {
358 fn id(&self) -> &'static str {
359 "openai-codex"
360 }
361
362 fn kind(&self) -> ProviderKind {
363 ProviderKind::OpenaiCodex
364 }
365
366 fn display_name(&self) -> &'static str {
367 "OpenAI Codex (ChatGPT)"
368 }
369
370 fn default_base_url(&self) -> &'static str {
371 DEFAULT_OPENAI_CODEX_BASE_URL
372 }
373
374 fn default_model(&self) -> &'static str {
375 DEFAULT_OPENAI_CODEX_MODEL
376 }
377
378 fn env_vars(&self) -> &'static [&'static str] {
379 &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"]
380 }
381
382 fn provider_config_key(&self) -> &'static str {
383 "openai_codex"
384 }
385
386 fn aliases(&self) -> &'static [&'static str] {
387 &[
388 "openai_codex",
389 "openaicodex",
390 "codex",
391 "chatgpt",
392 "chatgpt-codex",
393 "chatgpt_codex",
394 "chatgptcodex",
395 ]
396 }
397
398 fn wire(&self) -> WireFormat {
399 WireFormat::Responses
400 }
401}
402
403pub struct Anthropic;
405
406impl Provider for Anthropic {
407 fn id(&self) -> &'static str {
408 "anthropic"
409 }
410
411 fn kind(&self) -> ProviderKind {
412 ProviderKind::Anthropic
413 }
414
415 fn display_name(&self) -> &'static str {
416 "Anthropic"
417 }
418
419 fn default_base_url(&self) -> &'static str {
420 crate::DEFAULT_ANTHROPIC_BASE_URL
421 }
422
423 fn default_model(&self) -> &'static str {
424 crate::DEFAULT_ANTHROPIC_MODEL
425 }
426
427 fn env_vars(&self) -> &'static [&'static str] {
428 &["ANTHROPIC_API_KEY"]
429 }
430
431 fn provider_config_key(&self) -> &'static str {
432 "anthropic"
433 }
434
435 fn wire(&self) -> WireFormat {
436 WireFormat::AnthropicMessages
437 }
438}
439
440provider!(
441 Zai,
442 Zai,
443 "zai",
444 "Z.ai (GLM Coding)",
445 DEFAULT_ZAI_BASE_URL,
446 DEFAULT_ZAI_MODEL,
447 ["ZAI_API_KEY", "Z_AI_API_KEY"],
448 "zai",
449 aliases: ["z-ai", "z_ai", "z.ai"]
450);
451
452provider!(
453 Stepfun,
454 Stepfun,
455 "stepfun",
456 "StepFun / StepFlash",
457 DEFAULT_STEPFUN_BASE_URL,
458 DEFAULT_STEPFUN_MODEL,
459 ["STEPFUN_API_KEY", "STEP_API_KEY"],
460 "stepfun",
461 aliases: ["step-fun", "step_fun", "stepflash", "step-flash", "step_flash"]
462);
463
464provider!(
465 Minimax,
466 Minimax,
467 "minimax",
468 "MiniMax",
469 DEFAULT_MINIMAX_BASE_URL,
470 DEFAULT_MINIMAX_MODEL,
471 ["MINIMAX_API_KEY"],
472 "minimax",
473 aliases: ["mini-max", "mini_max"]
474);
475
476provider!(
477 Deepinfra,
478 Deepinfra,
479 "deepinfra",
480 "DeepInfra",
481 DEFAULT_DEEPINFRA_BASE_URL,
482 DEFAULT_DEEPINFRA_MODEL,
483 ["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
484 "deepinfra",
485 aliases: ["deep-infra", "deep_infra"]
486);
487
488static DEEPSEEK: Deepseek = Deepseek;
489static NVIDIA_NIM: NvidiaNim = NvidiaNim;
490static OPENAI: Openai = Openai;
491static ATLASCLOUD: Atlascloud = Atlascloud;
492static WANJIE_ARK: WanjieArk = WanjieArk;
493static VOLCENGINE: Volcengine = Volcengine;
494static OPENROUTER: Openrouter = Openrouter;
495static XIAOMI_MIMO: XiaomiMimo = XiaomiMimo;
496static NOVITA: Novita = Novita;
497static FIREWORKS: Fireworks = Fireworks;
498static SILICONFLOW: Siliconflow = Siliconflow;
499static SILICONFLOW_CN: SiliconflowCN = SiliconflowCN;
500static ARCEE: Arcee = Arcee;
501static MOONSHOT: Moonshot = Moonshot;
502static SGLANG: Sglang = Sglang;
503static VLLM: Vllm = Vllm;
504static OLLAMA: Ollama = Ollama;
505static HUGGINGFACE: Huggingface = Huggingface;
506static TOGETHER: Together = Together;
507static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
508static ANTHROPIC: Anthropic = Anthropic;
509static ZAI: Zai = Zai;
510static STEPFUN: Stepfun = Stepfun;
511static MINIMAX: Minimax = Minimax;
512static DEEPINFRA: Deepinfra = Deepinfra;
513
514static PROVIDER_REGISTRY: [&dyn Provider; 25] = [
515 &DEEPSEEK,
516 &NVIDIA_NIM,
517 &OPENAI,
518 &ATLASCLOUD,
519 &WANJIE_ARK,
520 &VOLCENGINE,
521 &OPENROUTER,
522 &XIAOMI_MIMO,
523 &NOVITA,
524 &FIREWORKS,
525 &SILICONFLOW,
526 &ARCEE,
527 &SILICONFLOW_CN,
528 &MOONSHOT,
529 &SGLANG,
530 &VLLM,
531 &OLLAMA,
532 &HUGGINGFACE,
533 &TOGETHER,
534 &OPENAI_CODEX,
535 &ANTHROPIC,
536 &ZAI,
537 &STEPFUN,
538 &MINIMAX,
539 &DEEPINFRA,
540];
541
542#[must_use]
548pub fn all_providers() -> &'static [&'static dyn Provider] {
549 &PROVIDER_REGISTRY
550}
551
552#[must_use]
568pub fn providers_sorted_for_display() -> Vec<&'static dyn Provider> {
569 let mut providers = all_providers().to_vec();
570 providers.sort_by(|a, b| {
571 a.display_name()
572 .to_ascii_lowercase()
573 .cmp(&b.display_name().to_ascii_lowercase())
574 });
575 providers
576}
577
578#[must_use]
580pub fn lookup_provider(id: &str) -> Option<&'static dyn Provider> {
581 let id = id.trim();
582 all_providers()
583 .iter()
584 .copied()
585 .find(|provider| provider.id() == id)
586}
587
588#[must_use]
590pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
591 ProviderKind::parse(id_or_alias).map(provider_for_kind)
592}
593
594#[must_use]
596pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
597 PROVIDER_REGISTRY
598 .iter()
599 .find(|p| p.kind() == kind)
600 .copied()
601 .expect("ProviderKind variant missing from PROVIDER_REGISTRY")
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn display_order_is_alphabetical_by_display_name() {
610 let display = providers_sorted_for_display();
611 let names: Vec<String> = display
612 .iter()
613 .map(|p| p.display_name().to_ascii_lowercase())
614 .collect();
615 let mut sorted = names.clone();
616 sorted.sort();
617 assert_eq!(
618 names, sorted,
619 "providers_sorted_for_display must be alphabetical (case-insensitive) by display name"
620 );
621 }
622
623 #[test]
624 fn display_order_differs_from_internal_all_order() {
625 let display_ids: Vec<&str> = providers_sorted_for_display()
628 .iter()
629 .map(|p| p.id())
630 .collect();
631 let internal_ids: Vec<&str> = all_providers().iter().map(|p| p.id()).collect();
632 assert_ne!(
633 display_ids, internal_ids,
634 "display order should not match internal ALL order"
635 );
636 }
637
638 #[test]
639 fn display_order_is_complete_and_unique() {
640 let display = providers_sorted_for_display();
642 assert_eq!(
643 display.len(),
644 all_providers().len(),
645 "display order must include every built-in provider"
646 );
647 let mut ids: Vec<&str> = display.iter().map(|p| p.id()).collect();
648 ids.sort_unstable();
649 let before = ids.len();
650 ids.dedup();
651 assert_eq!(
652 before,
653 ids.len(),
654 "display order must not contain duplicates"
655 );
656 }
657
658 #[test]
659 fn deepseek_is_present_but_not_first_in_display_order() {
660 let display = providers_sorted_for_display();
663 assert_eq!(
664 all_providers()[0].kind(),
665 ProviderKind::Deepseek,
666 "DeepSeek is expected to remain first in the stable internal order"
667 );
668 assert!(
669 display.iter().any(|p| p.kind() == ProviderKind::Deepseek),
670 "DeepSeek must remain present in display order"
671 );
672 assert_ne!(
673 display[0].kind(),
674 ProviderKind::Deepseek,
675 "DeepSeek must not be hard-coded first in display order"
676 );
677 assert_eq!(
680 display[0].display_name(),
681 "Anthropic",
682 "alphabetical display order should lead with Anthropic"
683 );
684 }
685}