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_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/// Wire protocol spoken by a provider.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum WireFormat {
33    /// OpenAI-compatible `/v1/chat/completions` style payloads.
34    ChatCompletions,
35    /// OpenAI Responses API (`/responses`).
36    Responses,
37    /// Native Anthropic Messages API (`/v1/messages`).
38    AnthropicMessages,
39}
40
41/// Static metadata for a built-in model provider.
42pub trait Provider: Send + Sync {
43    /// Provider enum variant represented by this entry.
44    fn kind(&self) -> ProviderKind;
45
46    /// Canonical provider identifier.
47    fn id(&self) -> &'static str {
48        self.kind().as_str()
49    }
50
51    /// Human-readable provider label for UIs and diagnostics.
52    fn display_name(&self) -> &'static str;
53
54    /// Default base URL used when no config/env/CLI override is present.
55    fn default_base_url(&self) -> &'static str;
56
57    /// Default model used when no config/env/CLI override is present.
58    fn default_model(&self) -> &'static str;
59
60    /// Environment variable candidates used for this provider's API key.
61    fn env_vars(&self) -> &'static [&'static str];
62
63    /// TOML table key under `[providers.<key>]`.
64    fn provider_config_key(&self) -> &'static str;
65
66    /// Alternate names accepted during provider resolution.
67    fn aliases(&self) -> &'static [&'static str] {
68        &[]
69    }
70
71    /// Wire format used by the provider.
72    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        /// Zero-sized metadata entry for this built-in provider.
90        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
140/// Opt-in DeepSeek route that speaks the Anthropic Messages wire protocol.
141pub 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
410/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
411pub 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
459/// Native Anthropic Messages API provider (#3014).
460pub 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
496/// OpenModel Anthropic-compatible Messages API provider.
497pub 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
597/// User-defined OpenAI-compatible endpoint (#1519).
598///
599/// A single dynamic provider identity for arbitrary `[providers.<name>]
600/// kind="openai-compatible"` config entries. Unlike the built-in providers it
601/// carries no real default base URL/model/env var: the concrete endpoint, model
602/// id, and auth env var all arrive from the named `[providers.<name>]` config
603/// table at route time. The placeholder base URL/model here exist only so the
604/// descriptor stays well-formed (non-empty) for conformance; runtime routing
605/// always supplies a `base_url_override` and a wire model id, so these
606/// placeholders are never used to reach the network.
607pub 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        // Placeholder only; the real endpoint comes from the named config table
624        // via the route's base_url_override. Loopback so a misconfigured custom
625        // provider fails closed locally rather than reaching a public host.
626        "http://localhost/v1"
627    }
628
629    fn default_model(&self) -> &'static str {
630        // Placeholder only; the real model id comes from config and is preserved
631        // verbatim as the wire model id.
632        "custom-model"
633    }
634
635    fn env_vars(&self) -> &'static [&'static str] {
636        // No built-in env var: the auth env var is named per-entry via
637        // `[providers.<name>] api_key_env = "..."`.
638        &[]
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/// Return all built-in provider metadata entries in `ProviderKind::ALL` order.
715///
716/// This insertion order is the stable order used for internal parsing and
717/// default selection. It is intentionally NOT the order user-facing UI should
718/// render; for browsing/picker surfaces use [`providers_sorted_for_display`].
719#[must_use]
720pub fn all_providers() -> &'static [&'static dyn Provider] {
721    &PROVIDER_REGISTRY
722}
723
724/// Return all built-in providers ordered for user-facing display.
725///
726/// Providers are sorted alphabetically (case-insensitively) by
727/// [`Provider::display_name`] so model/provider browsing surfaces present a
728/// neutral, predictable list rather than leading with whichever provider
729/// happens to sit first in [`ProviderKind::ALL`] (historically DeepSeek). The
730/// ordering policy intentionally differs from internal parsing/default order:
731///
732/// - [`all_providers`] / [`ProviderKind::ALL`] — stable order for internal
733///   matching, parsing, and default selection. Do not reorder.
734/// - [`providers_sorted_for_display`] — neutral alphabetical order for UI
735///   browsing. DeepSeek stays present and searchable but is not hard-coded
736///   first; a caller may still highlight/pin the active provider separately.
737///
738/// Returns an owned `Vec` because the sorted order is computed, not static.
739#[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/// Find a provider by canonical id only.
751#[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/// Resolve a provider by canonical id or supported legacy alias.
761#[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/// Return metadata for a known provider kind.
767#[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        // The whole point of the helper is that UI ordering is NOT the
798        // internal ProviderKind::ALL / all_providers() insertion order.
799        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        // No provider is dropped or duplicated by the sort.
813        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        // Acceptance: DeepSeek stays searchable but is no longer hard-coded
833        // first in provider browsing UI. (It is first in internal ALL order.)
834        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        // Anthropic ('Anthropic') sorts before 'DeepSeek' alphabetically, so it
850        // is a stable check that the neutral ordering actually took effect.
851        assert_eq!(
852            display[0].display_name(),
853            "Anthropic",
854            "alphabetical display order should lead with Anthropic"
855        );
856    }
857}