Skip to main content

codewhale_config/
provider.rs

1//! Built-in provider metadata.
2//!
3//! This module is a metadata foundation for collapsing provider drift over
4//! time. It deliberately does not mutate request bodies or choose fallback
5//! providers; runtime routing remains in `ConfigToml::resolve_runtime_options`.
6
7use 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_META_BASE_URL, DEFAULT_META_MODEL,
14    DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
15    DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL,
16    DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL,
17    DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL,
18    DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL,
19    DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL,
20    DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL,
21    DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL,
22    DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL,
23    DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL,
24    DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
25    DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
26    DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
27    DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL, ProviderKind,
28};
29
30/// Wire protocol spoken by a provider.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum WireFormat {
34    /// OpenAI-compatible `/v1/chat/completions` style payloads.
35    ChatCompletions,
36    /// OpenAI Responses API (`/responses`).
37    Responses,
38    /// Native Anthropic Messages API (`/v1/messages`).
39    AnthropicMessages,
40}
41
42/// Static metadata for a built-in model provider.
43pub trait Provider: Send + Sync {
44    /// Provider enum variant represented by this entry.
45    fn kind(&self) -> ProviderKind;
46
47    /// Canonical provider identifier.
48    fn id(&self) -> &'static str {
49        self.kind().as_str()
50    }
51
52    /// Human-readable provider label for UIs and diagnostics.
53    fn display_name(&self) -> &'static str;
54
55    /// Default base URL used when no config/env/CLI override is present.
56    fn default_base_url(&self) -> &'static str;
57
58    /// Default model used when no config/env/CLI override is present.
59    fn default_model(&self) -> &'static str;
60
61    /// Environment variable candidates used for this provider's API key.
62    fn env_vars(&self) -> &'static [&'static str];
63
64    /// TOML table key under `[providers.<key>]`.
65    fn provider_config_key(&self) -> &'static str;
66
67    /// Alternate names accepted during provider resolution.
68    fn aliases(&self) -> &'static [&'static str] {
69        &[]
70    }
71
72    /// Wire format used by the provider.
73    fn wire(&self) -> WireFormat {
74        WireFormat::ChatCompletions
75    }
76}
77
78macro_rules! provider {
79    (
80        $struct_name:ident,
81        $kind:ident,
82        $id:literal,
83        $display_name:literal,
84        $base_url:ident,
85        $model:ident,
86        [$($env_var:literal),* $(,)?],
87        $config_key:literal,
88        aliases: [$($alias:literal),* $(,)?]
89    ) => {
90        /// Zero-sized metadata entry for this built-in provider.
91        pub struct $struct_name;
92
93        impl Provider for $struct_name {
94            fn id(&self) -> &'static str {
95                $id
96            }
97
98            fn kind(&self) -> ProviderKind {
99                ProviderKind::$kind
100            }
101
102            fn display_name(&self) -> &'static str {
103                $display_name
104            }
105
106            fn default_base_url(&self) -> &'static str {
107                $base_url
108            }
109
110            fn default_model(&self) -> &'static str {
111                $model
112            }
113
114            fn env_vars(&self) -> &'static [&'static str] {
115                &[$($env_var),*]
116            }
117
118            fn provider_config_key(&self) -> &'static str {
119                $config_key
120            }
121
122            fn aliases(&self) -> &'static [&'static str] {
123                &[$($alias),*]
124            }
125        }
126    };
127}
128
129provider!(
130    Deepseek,
131    Deepseek,
132    "deepseek",
133    "DeepSeek",
134    DEFAULT_DEEPSEEK_BASE_URL,
135    DEFAULT_DEEPSEEK_MODEL,
136    ["DEEPSEEK_API_KEY"],
137    "deepseek",
138    aliases: ["deep-seek", "deepseek-cn", "deepseek_china", "deepseekcn", "deepseek-china"]
139);
140
141/// Opt-in DeepSeek route that speaks the Anthropic Messages wire protocol.
142pub struct DeepseekAnthropic;
143
144impl Provider for DeepseekAnthropic {
145    fn id(&self) -> &'static str {
146        "deepseek-anthropic"
147    }
148
149    fn kind(&self) -> ProviderKind {
150        ProviderKind::DeepseekAnthropic
151    }
152
153    fn display_name(&self) -> &'static str {
154        "DeepSeek (Anthropic-compatible)"
155    }
156
157    fn default_base_url(&self) -> &'static str {
158        DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
159    }
160
161    fn default_model(&self) -> &'static str {
162        DEFAULT_DEEPSEEK_ANTHROPIC_MODEL
163    }
164
165    fn env_vars(&self) -> &'static [&'static str] {
166        &["DEEPSEEK_API_KEY"]
167    }
168
169    fn provider_config_key(&self) -> &'static str {
170        "deepseek_anthropic"
171    }
172
173    fn aliases(&self) -> &'static [&'static str] {
174        &["deepseek_anthropic", "deepseek-claude", "deepseek_claude"]
175    }
176
177    fn wire(&self) -> WireFormat {
178        WireFormat::AnthropicMessages
179    }
180}
181provider!(
182    NvidiaNim,
183    NvidiaNim,
184    "nvidia-nim",
185    "NVIDIA NIM",
186    DEFAULT_NVIDIA_NIM_BASE_URL,
187    DEFAULT_NVIDIA_NIM_MODEL,
188    ["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
189    "nvidia_nim",
190    aliases: ["nvidia", "nvidia_nim", "nim"]
191);
192provider!(
193    Openai,
194    Openai,
195    "openai",
196    "OpenAI-compatible",
197    DEFAULT_OPENAI_BASE_URL,
198    DEFAULT_OPENAI_MODEL,
199    ["OPENAI_API_KEY"],
200    "openai",
201    aliases: ["open-ai"]
202);
203provider!(
204    Atlascloud,
205    Atlascloud,
206    "atlascloud",
207    "AtlasCloud",
208    DEFAULT_ATLASCLOUD_BASE_URL,
209    DEFAULT_ATLASCLOUD_MODEL,
210    ["ATLASCLOUD_API_KEY"],
211    "atlascloud",
212    aliases: ["atlas-cloud", "atlas_cloud", "atlas"]
213);
214provider!(
215    WanjieArk,
216    WanjieArk,
217    "wanjie-ark",
218    "Wanjie Ark",
219    DEFAULT_WANJIE_ARK_BASE_URL,
220    DEFAULT_WANJIE_ARK_MODEL,
221    [
222        "WANJIE_ARK_API_KEY",
223        "WANJIE_API_KEY",
224        "WANJIE_MAAS_API_KEY"
225    ],
226    "wanjie_ark",
227    aliases: ["wanjie", "wanjie_ark", "ark-wanjie", "ark_wanjie", "wanjieark", "wanjie-maas", "wanjie_maas", "wanjiemaas"]
228);
229provider!(
230    Volcengine,
231    Volcengine,
232    "volcengine",
233    "Volcengine Ark",
234    DEFAULT_VOLCENGINE_BASE_URL,
235    DEFAULT_VOLCENGINE_MODEL,
236    [
237        "VOLCENGINE_API_KEY",
238        "VOLCENGINE_ARK_API_KEY",
239        "ARK_API_KEY"
240    ],
241    "volcengine",
242    aliases: ["volcengine-ark", "volcengine_ark", "ark", "volc-ark", "volcengineark"]
243);
244provider!(
245    Openrouter,
246    Openrouter,
247    "openrouter",
248    "OpenRouter",
249    DEFAULT_OPENROUTER_BASE_URL,
250    DEFAULT_OPENROUTER_MODEL,
251    ["OPENROUTER_API_KEY"],
252    "openrouter",
253    aliases: ["open_router"]
254);
255provider!(
256    XiaomiMimo,
257    XiaomiMimo,
258    "xiaomi-mimo",
259    "Xiaomi MiMo",
260    DEFAULT_XIAOMI_MIMO_BASE_URL,
261    DEFAULT_XIAOMI_MIMO_MODEL,
262    [
263        "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
264        "MIMO_TOKEN_PLAN_API_KEY",
265        "XIAOMI_MIMO_API_KEY",
266        "XIAOMI_API_KEY",
267        "MIMO_API_KEY",
268    ],
269    "xiaomi_mimo",
270    aliases: ["xiaomi_mimo", "xiaomimimo", "mimo", "xiaomi"]
271);
272provider!(
273    Novita,
274    Novita,
275    "novita",
276    "Novita AI",
277    DEFAULT_NOVITA_BASE_URL,
278    DEFAULT_NOVITA_MODEL,
279    ["NOVITA_API_KEY"],
280    "novita",
281    // `novita-ai` is the id Models.dev publishes for this provider; without it a
282    // live/full Models.dev catalog row keyed `novita-ai` would fail to normalize
283    // onto ProviderKind::Novita (Refs #4186).
284    aliases: ["novita-ai", "novita_ai"]
285);
286provider!(
287    Fireworks,
288    Fireworks,
289    "fireworks",
290    "Fireworks AI",
291    DEFAULT_FIREWORKS_BASE_URL,
292    DEFAULT_FIREWORKS_MODEL,
293    ["FIREWORKS_API_KEY"],
294    "fireworks",
295    aliases: ["fireworks-ai"]
296);
297provider!(
298    Siliconflow,
299    Siliconflow,
300    "siliconflow",
301    "SiliconFlow",
302    DEFAULT_SILICONFLOW_BASE_URL,
303    DEFAULT_SILICONFLOW_MODEL,
304    ["SILICONFLOW_API_KEY"],
305    "siliconflow",
306    aliases: ["silicon-flow", "silicon_flow"]
307);
308provider!(
309    SiliconflowCN,
310    SiliconflowCN,
311    "siliconflow-CN",
312    "SiliconFlow (China)",
313    DEFAULT_SILICONFLOW_CN_BASE_URL,
314    DEFAULT_SILICONFLOW_MODEL,
315    ["SILICONFLOW_API_KEY"],
316    "siliconflow_cn",
317    aliases: [
318        "silicon-flow-cn",
319        "silicon-flow-CN",
320        "silicon_flow_cn",
321        "silicon_flow_CN",
322        "siliconflow-china",
323    ]
324);
325provider!(
326    Arcee,
327    Arcee,
328    "arcee",
329    "Arcee AI",
330    DEFAULT_ARCEE_BASE_URL,
331    DEFAULT_ARCEE_MODEL,
332    ["ARCEE_API_KEY"],
333    "arcee",
334    aliases: ["arcee-ai", "arcee_ai"]
335);
336provider!(
337    Moonshot,
338    Moonshot,
339    "moonshot",
340    "Moonshot/Kimi",
341    DEFAULT_MOONSHOT_BASE_URL,
342    DEFAULT_MOONSHOT_MODEL,
343    ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
344    "moonshot",
345    // `moonshotai` is the id Models.dev publishes for Moonshot/Kimi; without
346    // it a live/full Models.dev catalog row keyed `moonshotai` would fail to
347    // normalize onto ProviderKind::Moonshot (Refs #4186).
348    aliases: ["moonshot-ai", "moonshotai", "moonshot_ai", "kimi", "kimi-k2"]
349);
350provider!(
351    Sglang,
352    Sglang,
353    "sglang",
354    "SGLang",
355    DEFAULT_SGLANG_BASE_URL,
356    DEFAULT_SGLANG_MODEL,
357    ["SGLANG_API_KEY"],
358    "sglang",
359    aliases: ["sg-lang"]
360);
361provider!(
362    Vllm,
363    Vllm,
364    "vllm",
365    "vLLM",
366    DEFAULT_VLLM_BASE_URL,
367    DEFAULT_VLLM_MODEL,
368    ["VLLM_API_KEY"],
369    "vllm",
370    aliases: ["v-llm"]
371);
372provider!(
373    Ollama,
374    Ollama,
375    "ollama",
376    "Ollama",
377    DEFAULT_OLLAMA_BASE_URL,
378    DEFAULT_OLLAMA_MODEL,
379    ["OLLAMA_API_KEY"],
380    "ollama",
381    aliases: ["ollama-local"]
382);
383provider!(
384    Huggingface,
385    Huggingface,
386    "huggingface",
387    "Hugging Face",
388    DEFAULT_HUGGINGFACE_BASE_URL,
389    DEFAULT_HUGGINGFACE_MODEL,
390    ["HUGGINGFACE_API_KEY", "HF_TOKEN"],
391    "huggingface",
392    aliases: ["hugging-face", "hugging_face", "hf"]
393);
394provider!(
395    Together,
396    Together,
397    "together",
398    "Together AI",
399    DEFAULT_TOGETHER_BASE_URL,
400    DEFAULT_TOGETHER_MODEL,
401    ["TOGETHER_API_KEY"],
402    "together",
403    // `togetherai` (no separator) is the id Models.dev publishes for Together;
404    // the hyphen/underscore spellings are legacy config aliases. All three must
405    // normalize onto ProviderKind::Together so live-catalog rows keyed
406    // `togetherai` resolve to the right kind (Refs #4186).
407    aliases: ["together-ai", "together_ai", "togetherai"]
408);
409provider!(
410    Qianfan,
411    Qianfan,
412    "qianfan",
413    "Baidu Qianfan",
414    DEFAULT_QIANFAN_BASE_URL,
415    DEFAULT_QIANFAN_MODEL,
416    ["QIANFAN_API_KEY", "BAIDU_QIANFAN_API_KEY"],
417    "qianfan",
418    aliases: ["baidu-qianfan", "baidu_qianfan", "baidu"]
419);
420
421/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
422pub struct OpenaiCodex;
423
424impl Provider for OpenaiCodex {
425    fn id(&self) -> &'static str {
426        "openai-codex"
427    }
428
429    fn kind(&self) -> ProviderKind {
430        ProviderKind::OpenaiCodex
431    }
432
433    fn display_name(&self) -> &'static str {
434        "OpenAI Codex (ChatGPT)"
435    }
436
437    fn default_base_url(&self) -> &'static str {
438        DEFAULT_OPENAI_CODEX_BASE_URL
439    }
440
441    fn default_model(&self) -> &'static str {
442        DEFAULT_OPENAI_CODEX_MODEL
443    }
444
445    fn env_vars(&self) -> &'static [&'static str] {
446        &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"]
447    }
448
449    fn provider_config_key(&self) -> &'static str {
450        "openai_codex"
451    }
452
453    fn aliases(&self) -> &'static [&'static str] {
454        &[
455            "openai_codex",
456            "openaicodex",
457            "codex",
458            "chatgpt",
459            "chatgpt-codex",
460            "chatgpt_codex",
461            "chatgptcodex",
462        ]
463    }
464
465    fn wire(&self) -> WireFormat {
466        WireFormat::Responses
467    }
468}
469
470/// Native Anthropic Messages API provider (#3014).
471pub struct Anthropic;
472
473impl Provider for Anthropic {
474    fn id(&self) -> &'static str {
475        "anthropic"
476    }
477
478    fn kind(&self) -> ProviderKind {
479        ProviderKind::Anthropic
480    }
481
482    fn display_name(&self) -> &'static str {
483        "Anthropic"
484    }
485
486    fn default_base_url(&self) -> &'static str {
487        crate::DEFAULT_ANTHROPIC_BASE_URL
488    }
489
490    fn default_model(&self) -> &'static str {
491        crate::DEFAULT_ANTHROPIC_MODEL
492    }
493
494    fn env_vars(&self) -> &'static [&'static str] {
495        &["ANTHROPIC_API_KEY"]
496    }
497
498    fn provider_config_key(&self) -> &'static str {
499        "anthropic"
500    }
501
502    fn wire(&self) -> WireFormat {
503        WireFormat::AnthropicMessages
504    }
505}
506
507/// OpenModel Anthropic-compatible Messages API provider.
508pub struct Openmodel;
509
510impl Provider for Openmodel {
511    fn id(&self) -> &'static str {
512        "openmodel"
513    }
514
515    fn kind(&self) -> ProviderKind {
516        ProviderKind::Openmodel
517    }
518
519    fn display_name(&self) -> &'static str {
520        "OpenModel"
521    }
522
523    fn default_base_url(&self) -> &'static str {
524        DEFAULT_OPENMODEL_BASE_URL
525    }
526
527    fn default_model(&self) -> &'static str {
528        DEFAULT_OPENMODEL_MODEL
529    }
530
531    fn env_vars(&self) -> &'static [&'static str] {
532        &["OPENMODEL_API_KEY"]
533    }
534
535    fn provider_config_key(&self) -> &'static str {
536        "openmodel"
537    }
538
539    fn aliases(&self) -> &'static [&'static str] {
540        &["open-model", "open_model"]
541    }
542
543    fn wire(&self) -> WireFormat {
544        WireFormat::AnthropicMessages
545    }
546}
547
548provider!(
549    Zai,
550    Zai,
551    "zai",
552    "Zhipu AI / Z.ai",
553    DEFAULT_ZAI_BASE_URL,
554    DEFAULT_ZAI_MODEL,
555    ["ZAI_API_KEY", "Z_AI_API_KEY", "ZHIPU_API_KEY", "GLM_API_KEY"],
556    "zai",
557    aliases: ["z-ai", "z_ai", "z.ai", "zhipu", "zhipuai", "bigmodel", "big-model"]
558);
559
560provider!(
561    Stepfun,
562    Stepfun,
563    "stepfun",
564    "StepFun / StepFlash",
565    DEFAULT_STEPFUN_BASE_URL,
566    DEFAULT_STEPFUN_MODEL,
567    ["STEPFUN_API_KEY", "STEP_API_KEY"],
568    "stepfun",
569    aliases: ["step-fun", "step_fun", "stepflash", "step-flash", "step_flash"]
570);
571
572provider!(
573    Minimax,
574    Minimax,
575    "minimax",
576    "MiniMax",
577    DEFAULT_MINIMAX_BASE_URL,
578    DEFAULT_MINIMAX_MODEL,
579    ["MINIMAX_API_KEY"],
580    "minimax",
581    aliases: ["mini-max", "mini_max"]
582);
583
584/// MiniMax route that speaks the Anthropic Messages wire protocol.
585pub struct MinimaxAnthropic;
586
587impl Provider for MinimaxAnthropic {
588    fn id(&self) -> &'static str {
589        "minimax-anthropic"
590    }
591
592    fn kind(&self) -> ProviderKind {
593        ProviderKind::MinimaxAnthropic
594    }
595
596    fn display_name(&self) -> &'static str {
597        "MiniMax (Anthropic-compatible)"
598    }
599
600    fn default_base_url(&self) -> &'static str {
601        DEFAULT_MINIMAX_ANTHROPIC_BASE_URL
602    }
603
604    fn default_model(&self) -> &'static str {
605        DEFAULT_MINIMAX_MODEL
606    }
607
608    fn env_vars(&self) -> &'static [&'static str] {
609        &["MINIMAX_API_KEY"]
610    }
611
612    fn provider_config_key(&self) -> &'static str {
613        "minimax_anthropic"
614    }
615
616    fn aliases(&self) -> &'static [&'static str] {
617        &[
618            "minimax_anthropic",
619            "mini-max-anthropic",
620            "mini_max_anthropic",
621        ]
622    }
623
624    fn wire(&self) -> WireFormat {
625        WireFormat::AnthropicMessages
626    }
627}
628
629provider!(
630    Deepinfra,
631    Deepinfra,
632    "deepinfra",
633    "DeepInfra",
634    DEFAULT_DEEPINFRA_BASE_URL,
635    DEFAULT_DEEPINFRA_MODEL,
636    ["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
637    "deepinfra",
638    aliases: ["deep-infra", "deep_infra"]
639);
640
641provider!(
642    Sakana,
643    Sakana,
644    "sakana",
645    "Sakana AI (Fugu)",
646    DEFAULT_SAKANA_BASE_URL,
647    DEFAULT_SAKANA_MODEL,
648    ["FUGU_API_KEY", "SAKANA_API_KEY"],
649    "sakana",
650    aliases: ["sakana-ai", "sakana_ai", "fugu"]
651);
652
653provider!(
654    LongCat,
655    LongCat,
656    "longcat",
657    "Meituan LongCat",
658    DEFAULT_LONGCAT_BASE_URL,
659    DEFAULT_LONGCAT_MODEL,
660    ["LONGCAT_API_KEY"],
661    "longcat",
662    aliases: ["long-cat", "meituan-longcat", "meituan"]
663);
664
665provider!(
666    Meta,
667    Meta,
668    "meta",
669    "Meta Model API",
670    DEFAULT_META_BASE_URL,
671    DEFAULT_META_MODEL,
672    ["META_MODEL_API_KEY", "MODEL_API_KEY"],
673    "meta",
674    aliases: [
675        "meta-ai",
676        "meta_ai",
677        "meta-model-api",
678        "meta_model_api",
679        "muse",
680        "muse-spark"
681    ]
682);
683
684provider!(
685    Xai,
686    Xai,
687    "xai",
688    "xAI",
689    DEFAULT_XAI_BASE_URL,
690    DEFAULT_XAI_MODEL,
691    ["XAI_API_KEY"],
692    "xai",
693    aliases: ["x-ai", "x_ai", "grok"]
694);
695
696/// User-defined OpenAI-compatible endpoint (#1519).
697///
698/// A single dynamic provider identity for arbitrary `[providers.<name>]
699/// kind="openai-compatible"` config entries. Unlike the built-in providers it
700/// carries no real default base URL/model/env var: the concrete endpoint, model
701/// id, and auth env var all arrive from the named `[providers.<name>]` config
702/// table at route time. The placeholder base URL/model here exist only so the
703/// descriptor stays well-formed (non-empty) for conformance; runtime routing
704/// always supplies a `base_url_override` and a wire model id, so these
705/// placeholders are never used to reach the network.
706pub struct Custom;
707
708impl Provider for Custom {
709    fn id(&self) -> &'static str {
710        "custom"
711    }
712
713    fn kind(&self) -> ProviderKind {
714        ProviderKind::Custom
715    }
716
717    fn display_name(&self) -> &'static str {
718        "Custom (OpenAI-compatible)"
719    }
720
721    fn default_base_url(&self) -> &'static str {
722        // Placeholder only; the real endpoint comes from the named config table
723        // via the route's base_url_override. Loopback so a misconfigured custom
724        // provider fails closed locally rather than reaching a public host.
725        "http://localhost/v1"
726    }
727
728    fn default_model(&self) -> &'static str {
729        // Placeholder only; the real model id comes from config and is preserved
730        // verbatim as the wire model id.
731        "custom-model"
732    }
733
734    fn env_vars(&self) -> &'static [&'static str] {
735        // No built-in env var: the auth env var is named per-entry via
736        // `[providers.<name>] api_key_env = "..."`.
737        &[]
738    }
739
740    fn provider_config_key(&self) -> &'static str {
741        "custom"
742    }
743
744    fn wire(&self) -> WireFormat {
745        WireFormat::ChatCompletions
746    }
747}
748
749static DEEPSEEK: Deepseek = Deepseek;
750static DEEPSEEK_ANTHROPIC: DeepseekAnthropic = DeepseekAnthropic;
751static NVIDIA_NIM: NvidiaNim = NvidiaNim;
752static OPENAI: Openai = Openai;
753static ATLASCLOUD: Atlascloud = Atlascloud;
754static WANJIE_ARK: WanjieArk = WanjieArk;
755static VOLCENGINE: Volcengine = Volcengine;
756static OPENROUTER: Openrouter = Openrouter;
757static XIAOMI_MIMO: XiaomiMimo = XiaomiMimo;
758static NOVITA: Novita = Novita;
759static FIREWORKS: Fireworks = Fireworks;
760static SILICONFLOW: Siliconflow = Siliconflow;
761static SILICONFLOW_CN: SiliconflowCN = SiliconflowCN;
762static ARCEE: Arcee = Arcee;
763static MOONSHOT: Moonshot = Moonshot;
764static SGLANG: Sglang = Sglang;
765static VLLM: Vllm = Vllm;
766static OLLAMA: Ollama = Ollama;
767static HUGGINGFACE: Huggingface = Huggingface;
768static TOGETHER: Together = Together;
769static QIANFAN: Qianfan = Qianfan;
770static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
771static ANTHROPIC: Anthropic = Anthropic;
772static OPENMODEL: Openmodel = Openmodel;
773static ZAI: Zai = Zai;
774static STEPFUN: Stepfun = Stepfun;
775static MINIMAX: Minimax = Minimax;
776static MINIMAX_ANTHROPIC: MinimaxAnthropic = MinimaxAnthropic;
777static DEEPINFRA: Deepinfra = Deepinfra;
778static SAKANA: Sakana = Sakana;
779static LONGCAT: LongCat = LongCat;
780static META: Meta = Meta;
781static XAI: Xai = Xai;
782static CUSTOM: Custom = Custom;
783
784static PROVIDER_REGISTRY: [&dyn Provider; 34] = [
785    &DEEPSEEK,
786    &DEEPSEEK_ANTHROPIC,
787    &NVIDIA_NIM,
788    &OPENAI,
789    &ATLASCLOUD,
790    &WANJIE_ARK,
791    &VOLCENGINE,
792    &OPENROUTER,
793    &XIAOMI_MIMO,
794    &NOVITA,
795    &FIREWORKS,
796    &SILICONFLOW,
797    &ARCEE,
798    &SILICONFLOW_CN,
799    &MOONSHOT,
800    &SGLANG,
801    &VLLM,
802    &OLLAMA,
803    &HUGGINGFACE,
804    &TOGETHER,
805    &QIANFAN,
806    &OPENAI_CODEX,
807    &ANTHROPIC,
808    &OPENMODEL,
809    &ZAI,
810    &STEPFUN,
811    &MINIMAX,
812    &MINIMAX_ANTHROPIC,
813    &DEEPINFRA,
814    &SAKANA,
815    &LONGCAT,
816    &META,
817    &XAI,
818    &CUSTOM,
819];
820
821/// Return all built-in provider metadata entries in `ProviderKind::ALL` order.
822///
823/// This insertion order is the stable order used for internal parsing and
824/// default selection. It is intentionally NOT the order user-facing UI should
825/// render; for browsing/picker surfaces use [`providers_sorted_for_display`].
826#[must_use]
827pub fn all_providers() -> &'static [&'static dyn Provider] {
828    &PROVIDER_REGISTRY
829}
830
831/// Return all built-in providers ordered for user-facing display.
832///
833/// Providers are sorted alphabetically (case-insensitively) by
834/// [`Provider::display_name`] so model/provider browsing surfaces present a
835/// neutral, predictable list rather than leading with whichever provider
836/// happens to sit first in [`ProviderKind::ALL`] (historically DeepSeek). The
837/// ordering policy intentionally differs from internal parsing/default order:
838///
839/// - [`all_providers`] / [`ProviderKind::ALL`] — stable order for internal
840///   matching, parsing, and default selection. Do not reorder.
841/// - [`providers_sorted_for_display`] — neutral alphabetical order for UI
842///   browsing. DeepSeek stays present and searchable but is not hard-coded
843///   first; a caller may still highlight/pin the active provider separately.
844///
845/// Returns an owned `Vec` because the sorted order is computed, not static.
846#[must_use]
847pub fn providers_sorted_for_display() -> Vec<&'static dyn Provider> {
848    let mut providers = all_providers().to_vec();
849    providers.sort_by(|a, b| {
850        a.display_name()
851            .to_ascii_lowercase()
852            .cmp(&b.display_name().to_ascii_lowercase())
853    });
854    providers
855}
856
857/// Find a provider by canonical id only.
858#[must_use]
859pub fn lookup_provider(id: &str) -> Option<&'static dyn Provider> {
860    let id = id.trim();
861    all_providers()
862        .iter()
863        .copied()
864        .find(|provider| provider.id() == id)
865}
866
867/// Resolve a provider by canonical id or supported legacy alias.
868#[must_use]
869pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
870    ProviderKind::parse(id_or_alias).map(provider_for_kind)
871}
872
873/// Return metadata for a known provider kind.
874#[must_use]
875pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
876    PROVIDER_REGISTRY
877        .iter()
878        .find(|p| p.kind() == kind)
879        .copied()
880        .expect("ProviderKind variant missing from PROVIDER_REGISTRY")
881}
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886
887    #[test]
888    fn display_order_is_alphabetical_by_display_name() {
889        let display = providers_sorted_for_display();
890        let names: Vec<String> = display
891            .iter()
892            .map(|p| p.display_name().to_ascii_lowercase())
893            .collect();
894        let mut sorted = names.clone();
895        sorted.sort();
896        assert_eq!(
897            names, sorted,
898            "providers_sorted_for_display must be alphabetical (case-insensitive) by display name"
899        );
900    }
901
902    #[test]
903    fn display_order_differs_from_internal_all_order() {
904        // The whole point of the helper is that UI ordering is NOT the
905        // internal ProviderKind::ALL / all_providers() insertion order.
906        let display_ids: Vec<&str> = providers_sorted_for_display()
907            .iter()
908            .map(|p| p.id())
909            .collect();
910        let internal_ids: Vec<&str> = all_providers().iter().map(|p| p.id()).collect();
911        assert_ne!(
912            display_ids, internal_ids,
913            "display order should not match internal ALL order"
914        );
915    }
916
917    #[test]
918    fn display_order_is_complete_and_unique() {
919        // No provider is dropped or duplicated by the sort.
920        let display = providers_sorted_for_display();
921        assert_eq!(
922            display.len(),
923            all_providers().len(),
924            "display order must include every built-in provider"
925        );
926        let mut ids: Vec<&str> = display.iter().map(|p| p.id()).collect();
927        ids.sort_unstable();
928        let before = ids.len();
929        ids.dedup();
930        assert_eq!(
931            before,
932            ids.len(),
933            "display order must not contain duplicates"
934        );
935    }
936
937    #[test]
938    fn deepseek_is_present_but_not_first_in_display_order() {
939        // Acceptance: DeepSeek stays searchable but is no longer hard-coded
940        // first in provider browsing UI. (It is first in internal ALL order.)
941        let display = providers_sorted_for_display();
942        assert_eq!(
943            all_providers()[0].kind(),
944            ProviderKind::Deepseek,
945            "DeepSeek is expected to remain first in the stable internal order"
946        );
947        assert!(
948            display.iter().any(|p| p.kind() == ProviderKind::Deepseek),
949            "DeepSeek must remain present in display order"
950        );
951        assert_ne!(
952            display[0].kind(),
953            ProviderKind::Deepseek,
954            "DeepSeek must not be hard-coded first in display order"
955        );
956        // Anthropic ('Anthropic') sorts before 'DeepSeek' alphabetically, so it
957        // is a stable check that the neutral ordering actually took effect.
958        assert_eq!(
959            display[0].display_name(),
960            "Anthropic",
961            "alphabetical display order should lead with Anthropic"
962        );
963    }
964}