Skip to main content

codewhale_config/
provider_kind.rs

1//! The canonical [`ProviderKind`] enum (#3311): the set of built-in provider
2//! kinds, their serde aliases, and identity helpers (`all`, `as_str`, `parse`,
3//! `provider`). Extracted verbatim from `lib.rs` to separate provider identity
4//! from config schema/loading; re-exported at the crate root so
5//! `codewhale_config::ProviderKind` is unchanged. Behavior is identical.
6
7use serde::{Deserialize, Serialize};
8
9use crate::provider;
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
12#[serde(rename_all = "kebab-case")]
13pub enum ProviderKind {
14    #[default]
15    #[serde(
16        alias = "deepseek-cn",
17        alias = "deepseek_china",
18        alias = "deepseekcn",
19        alias = "deepseek-china"
20    )]
21    Deepseek,
22    #[serde(
23        alias = "deepseek-anthropic",
24        alias = "deepseek_anthropic",
25        alias = "deepseek-claude",
26        alias = "deepseek_claude"
27    )]
28    DeepseekAnthropic,
29    NvidiaNim,
30    #[serde(alias = "open-ai")]
31    Openai,
32    Atlascloud,
33    #[serde(
34        alias = "wanjie",
35        alias = "wanjie_ark",
36        alias = "ark-wanjie",
37        alias = "ark_wanjie",
38        alias = "wanjie-maas",
39        alias = "wanjie_maas"
40    )]
41    WanjieArk,
42    #[serde(alias = "volcengine-ark", alias = "volcengine_ark", alias = "ark")]
43    Volcengine,
44    Openrouter,
45    #[serde(alias = "orca_router", alias = "orca")]
46    Orcarouter,
47    #[serde(alias = "mimo", alias = "xiaomi", alias = "xiaomi_mimo")]
48    XiaomiMimo,
49    #[serde(alias = "novita-ai", alias = "novita_ai")]
50    Novita,
51    #[serde(alias = "fireworks-ai", alias = "fireworks_ai")]
52    Fireworks,
53    #[serde(alias = "silicon-flow", alias = "silicon_flow")]
54    Siliconflow,
55    #[serde(alias = "arcee-ai", alias = "arcee_ai")]
56    Arcee,
57    #[serde(alias = "siliconflow-cn", alias = "siliconflow-CN")]
58    SiliconflowCN,
59    #[serde(alias = "moonshot-ai", alias = "moonshotai", alias = "moonshot_ai")]
60    Moonshot,
61    Sglang,
62    Vllm,
63    Ollama,
64    #[serde(alias = "ollama_cloud")]
65    OllamaCloud,
66    #[serde(alias = "hugging-face", alias = "hugging_face", alias = "hf")]
67    Huggingface,
68    #[serde(alias = "together-ai", alias = "together_ai", alias = "togetherai")]
69    Together,
70    #[serde(alias = "baidu-qianfan", alias = "baidu_qianfan", alias = "baidu")]
71    Qianfan,
72    #[serde(
73        alias = "openai-codex",
74        alias = "openai_codex",
75        alias = "codex",
76        alias = "chatgpt",
77        alias = "chatgpt-codex",
78        alias = "chatgpt_codex"
79    )]
80    OpenaiCodex,
81    #[serde(alias = "claude")]
82    Anthropic,
83    #[serde(alias = "open-model", alias = "open_model")]
84    Openmodel,
85    #[serde(
86        alias = "z-ai",
87        alias = "z_ai",
88        alias = "z.ai",
89        alias = "zhipu",
90        alias = "zhipuai",
91        alias = "bigmodel",
92        alias = "big-model"
93    )]
94    Zai,
95    #[serde(
96        alias = "step-fun",
97        alias = "step_fun",
98        alias = "stepfun",
99        alias = "stepflash",
100        alias = "step-flash",
101        alias = "step_flash"
102    )]
103    Stepfun,
104    #[serde(alias = "mini-max", alias = "mini_max", alias = "minimax")]
105    Minimax,
106    #[serde(
107        alias = "minimax_anthropic",
108        alias = "mini-max-anthropic",
109        alias = "mini_max_anthropic"
110    )]
111    MinimaxAnthropic,
112    #[serde(alias = "deep-infra", alias = "deep_infra")]
113    Deepinfra,
114    #[serde(alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")]
115    Sakana,
116    #[serde(alias = "long-cat", alias = "meituan-longcat", alias = "meituan")]
117    LongCat,
118    #[serde(alias = "opencode_go", alias = "opencodego")]
119    OpencodeGo,
120    #[serde(
121        alias = "opencode_zen",
122        alias = "opencodezen",
123        alias = "zen",
124        alias = "opencode"
125    )]
126    OpencodeZen,
127    #[serde(
128        alias = "meta-ai",
129        alias = "meta_ai",
130        alias = "meta-model-api",
131        alias = "meta_model_api",
132        alias = "muse",
133        alias = "muse-spark"
134    )]
135    Meta,
136    #[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
137    Xai,
138    /// Mistral AI — la Plateforme (OpenAI-compatible Chat Completions).
139    #[serde(
140        alias = "mistral-ai",
141        alias = "mistral_ai",
142        alias = "mistralai",
143        alias = "la-plateforme",
144        alias = "la_plateforme"
145    )]
146    Mistral,
147    /// Jiangsu Telecom TokenHub (OpenAI-compatible).
148    ///
149    /// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
150    /// Completions wire protocol and serves a broad model catalog; each API key
151    /// may access a different subset of models.
152    #[serde(
153        alias = "telecom-js",
154        alias = "telecom_js",
155        alias = "telecomjs-cn",
156        alias = "tokenhub"
157    )]
158    Telecomjs,
159    /// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
160    ///
161    /// Token Plan Personal and Team share the same endpoint. Both the OpenAI
162    /// and Anthropic dialects are available; select the Anthropic dialect via
163    /// `modelstudio-token-plan-anthropic`. Pay-as-you-go workspace-id templating
164    /// is out of scope for v1; use a custom provider for that plan.
165    #[serde(
166        alias = "modelstudio-token-plan",
167        alias = "modelstudio_token_plan",
168        alias = "alibaba-token-plan",
169        alias = "dashscope-token-plan"
170    )]
171    ModelstudioTokenPlan,
172    /// Alibaba Cloud Model Studio — Token Plan Anthropic-compatible endpoint.
173    #[serde(
174        alias = "modelstudio-token-plan-anthropic",
175        alias = "modelstudio_token_plan_anthropic",
176        alias = "alibaba-token-plan-anthropic"
177    )]
178    ModelstudioTokenPlanAnthropic,
179    /// Alibaba Cloud Model Studio — Coding Plan (OpenAI-compatible Chat Completions).
180    #[serde(
181        alias = "modelstudio-coding-plan",
182        alias = "modelstudio_coding_plan",
183        alias = "alibaba-coding-plan",
184        alias = "dashscope-coding-plan"
185    )]
186    ModelstudioCodingPlan,
187    /// Alibaba Cloud Model Studio — Coding Plan Anthropic-compatible endpoint.
188    #[serde(
189        alias = "modelstudio-coding-plan-anthropic",
190        alias = "modelstudio_coding_plan_anthropic",
191        alias = "alibaba-coding-plan-anthropic"
192    )]
193    ModelstudioCodingPlanAnthropic,
194    /// Google Antigravity (`agy` CLI) — consent-gated read-only credential
195    /// import only; the cloud-code wire protocol is not implemented and
196    /// requests fail closed with an actionable message.
197    #[serde(alias = "agy")]
198    Antigravity,
199    /// Google — Gemini OpenAI-compatible endpoint. Its own backend, not an
200    /// OpenAI alias: thought signatures on tool calls are captured and
201    /// replayed per Google's contract.
202    #[serde(
203        alias = "google-gemini",
204        alias = "google_gemini",
205        alias = "gemini",
206        alias = "google-ai",
207        alias = "google_ai",
208        alias = "ai-studio",
209        alias = "aistudio"
210    )]
211    Google,
212    /// Eden AI — OpenAI-compatible AI gateway (aggregator).
213    ///
214    /// Serves a broad catalog of upstream models under `provider/model`
215    /// namespaced wire ids over the OpenAI Chat Completions protocol.
216    #[serde(alias = "eden-ai", alias = "eden_ai", alias = "edenai")]
217    Edenai,
218    /// User-defined OpenAI-compatible endpoint (#1519).
219    ///
220    /// A single dynamic identity for arbitrary `[providers.<name>]
221    /// kind="openai-compatible"` entries. It speaks the OpenAI Chat Completions
222    /// wire protocol and carries no built-in base URL/model — the concrete
223    /// endpoint and model arrive via config (`base_url` / `model`) and the
224    /// route's `base_url_override`, never from this static descriptor.
225    Custom,
226}
227
228impl ProviderKind {
229    /// Catalog / picker surface: one identity per vendor.
230    ///
231    /// Dual-wire dialect kinds (`*Anthropic`) and Model Studio plan variants
232    /// stay on the enum for serde and `provider_for_kind`, but they are not
233    /// first-class catalog rows. Plan is `mode` / base_url; dialect is
234    /// `wire = openai|anthropic` on the primary provider config.
235    pub const ALL: [Self; 42] = [
236        Self::Deepseek,
237        Self::NvidiaNim,
238        Self::Openai,
239        Self::Atlascloud,
240        Self::WanjieArk,
241        Self::Volcengine,
242        Self::Openrouter,
243        Self::Orcarouter,
244        Self::XiaomiMimo,
245        Self::Novita,
246        Self::Fireworks,
247        Self::Siliconflow,
248        Self::Arcee,
249        Self::SiliconflowCN,
250        Self::Moonshot,
251        Self::Sglang,
252        Self::Vllm,
253        Self::Ollama,
254        Self::OllamaCloud,
255        Self::Huggingface,
256        Self::Together,
257        Self::Qianfan,
258        Self::OpenaiCodex,
259        Self::Anthropic,
260        Self::Openmodel,
261        Self::Zai,
262        Self::Stepfun,
263        Self::Minimax,
264        Self::Deepinfra,
265        Self::Sakana,
266        Self::LongCat,
267        Self::OpencodeGo,
268        Self::OpencodeZen,
269        Self::Meta,
270        Self::Xai,
271        Self::Mistral,
272        Self::Telecomjs,
273        Self::ModelstudioTokenPlan,
274        Self::Google,
275        Self::Antigravity,
276        Self::Edenai,
277        Self::Custom,
278    ];
279
280    #[must_use]
281    pub fn all() -> &'static [Self] {
282        &Self::ALL
283    }
284
285    #[must_use]
286    pub fn names_hint() -> String {
287        Self::all()
288            .iter()
289            .map(|provider| provider.as_str())
290            .collect::<Vec<_>>()
291            .join(", ")
292    }
293
294    #[must_use]
295    pub fn as_str(self) -> &'static str {
296        self.provider().id()
297    }
298
299    #[must_use]
300    pub fn parse(value: &str) -> Option<Self> {
301        let trimmed = value.trim();
302        provider::all_providers()
303            .iter()
304            .find(|p| {
305                trimmed.eq_ignore_ascii_case(p.id())
306                    || p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a))
307            })
308            .map(|p| p.kind())
309    }
310
311    /// Parse a provider identifier for **config-table identity** — the kind
312    /// used to look up credentials, model, and base URL in the user's
313    /// `[providers.*]` tables.
314    ///
315    /// [`parse`](Self::parse) is *catalog* identity: legacy dual-wire
316    /// spellings (`deepseek-anthropic`, `minimax-anthropic`, the Model Studio
317    /// plan/dialect kinds) are aliases of the vendor primary and collapse
318    /// onto it so pickers show one row per vendor. That collapse must not
319    /// decide which config table holds the user's credentials: TOML serde
320    /// keeps the legacy kind for `provider = "deepseek-anthropic"`, so env
321    /// (`CODEWHALE_PROVIDER`) and `config set provider` must resolve the same
322    /// way or the user's own named table is orphaned with the key present.
323    ///
324    /// An exact canonical-id or `provider_config_key` match across the full
325    /// registry (including legacy dialect/plan kinds) therefore wins over
326    /// alias collapse; everything else falls back to [`parse`](Self::parse).
327    /// Wire-endpoint selection is unaffected: it keys off the resolved kind's
328    /// `wire` config, not this parse.
329    #[must_use]
330    pub fn parse_config_identity(value: &str) -> Option<Self> {
331        let trimmed = value.trim();
332        provider::all_providers()
333            .iter()
334            .find(|p| {
335                trimmed.eq_ignore_ascii_case(p.id())
336                    || trimmed.eq_ignore_ascii_case(p.provider_config_key())
337            })
338            .map(|p| p.kind())
339            .or_else(|| Self::parse(trimmed))
340    }
341
342    #[must_use]
343    pub fn is_siliconflow(self) -> bool {
344        matches!(self, Self::Siliconflow | Self::SiliconflowCN)
345    }
346
347    /// Canonical durable-credential slot in the local secret store.
348    ///
349    /// Most providers own a slot named after their id. Variants authenticated
350    /// by the SAME account share one slot so a single saved key (or logout)
351    /// applies to the whole family:
352    ///
353    /// - `SiliconflowCN` shares `siliconflow` (historical China-endpoint slot,
354    ///   already the TUI/CLI convention).
355    /// - The four Alibaba Cloud Model Studio variants share
356    ///   `modelstudio-token-plan`: one Model Studio account/key authenticates
357    ///   the Token Plan and Coding Plan endpoints in both wire dialects, so
358    ///   per-variant slots produced three bogus "missing key" rows whenever
359    ///   one variant held the key.
360    #[must_use]
361    pub fn secret_store_slot(self) -> &'static str {
362        match self {
363            Self::SiliconflowCN => "siliconflow",
364            Self::ModelstudioTokenPlan
365            | Self::ModelstudioTokenPlanAnthropic
366            | Self::ModelstudioCodingPlan
367            | Self::ModelstudioCodingPlanAnthropic => "modelstudio-token-plan",
368            _ => self.as_str(),
369        }
370    }
371
372    /// Return the built-in metadata entry for this provider.
373    ///
374    /// This is a metadata foundation only; runtime routing still resolves
375    /// through [`crate::ConfigToml::resolve_runtime_options`].
376    #[must_use]
377    pub fn provider(self) -> &'static dyn provider::Provider {
378        provider::provider_for_kind(self)
379    }
380}