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_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/// Wire protocol spoken by a provider.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WireFormat {
29    /// OpenAI-compatible `/v1/chat/completions` style payloads.
30    ChatCompletions,
31    /// OpenAI Responses API (`/responses`).
32    Responses,
33    /// Native Anthropic Messages API (`/v1/messages`).
34    AnthropicMessages,
35}
36
37/// Static metadata for a built-in model provider.
38pub trait Provider: Send + Sync {
39    /// Provider enum variant represented by this entry.
40    fn kind(&self) -> ProviderKind;
41
42    /// Canonical provider identifier.
43    fn id(&self) -> &'static str {
44        self.kind().as_str()
45    }
46
47    /// Human-readable provider label for UIs and diagnostics.
48    fn display_name(&self) -> &'static str;
49
50    /// Default base URL used when no config/env/CLI override is present.
51    fn default_base_url(&self) -> &'static str;
52
53    /// Default model used when no config/env/CLI override is present.
54    fn default_model(&self) -> &'static str;
55
56    /// Environment variable candidates used for this provider's API key.
57    fn env_vars(&self) -> &'static [&'static str];
58
59    /// TOML table key under `[providers.<key>]`.
60    fn provider_config_key(&self) -> &'static str;
61
62    /// Alternate names accepted during provider resolution.
63    fn aliases(&self) -> &'static [&'static str] {
64        &[]
65    }
66
67    /// Wire format used by the provider.
68    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        /// Zero-sized metadata entry for this built-in provider.
86        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
354/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
355pub 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
403/// Native Anthropic Messages API provider (#3014).
404pub 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/// Return all built-in provider metadata entries in `ProviderKind::ALL` order.
543///
544/// This insertion order is the stable order used for internal parsing and
545/// default selection. It is intentionally NOT the order user-facing UI should
546/// render; for browsing/picker surfaces use [`providers_sorted_for_display`].
547#[must_use]
548pub fn all_providers() -> &'static [&'static dyn Provider] {
549    &PROVIDER_REGISTRY
550}
551
552/// Return all built-in providers ordered for user-facing display.
553///
554/// Providers are sorted alphabetically (case-insensitively) by
555/// [`Provider::display_name`] so model/provider browsing surfaces present a
556/// neutral, predictable list rather than leading with whichever provider
557/// happens to sit first in [`ProviderKind::ALL`] (historically DeepSeek). The
558/// ordering policy intentionally differs from internal parsing/default order:
559///
560/// - [`all_providers`] / [`ProviderKind::ALL`] — stable order for internal
561///   matching, parsing, and default selection. Do not reorder.
562/// - [`providers_sorted_for_display`] — neutral alphabetical order for UI
563///   browsing. DeepSeek stays present and searchable but is not hard-coded
564///   first; a caller may still highlight/pin the active provider separately.
565///
566/// Returns an owned `Vec` because the sorted order is computed, not static.
567#[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/// Find a provider by canonical id only.
579#[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/// Resolve a provider by canonical id or supported legacy alias.
589#[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/// Return metadata for a known provider kind.
595#[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        // The whole point of the helper is that UI ordering is NOT the
626        // internal ProviderKind::ALL / all_providers() insertion order.
627        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        // No provider is dropped or duplicated by the sort.
641        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        // Acceptance: DeepSeek stays searchable but is no longer hard-coded
661        // first in provider browsing UI. (It is first in internal ALL order.)
662        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        // Anthropic ('Anthropic') sorts before 'DeepSeek' alphabetically, so it
678        // is a stable check that the neutral ordering actually took effect.
679        assert_eq!(
680            display[0].display_name(),
681            "Anthropic",
682            "alphabetical display order should lead with Anthropic"
683        );
684    }
685}