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