返回 CodeWhale
provider_kind.rs
根目录 / crates / config / src / 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
7 use serde::{Deserialize, Serialize};
8
9 use crate::provider;
10
11 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
12 #[serde(rename_all = "kebab-case")]
13 pub 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 /// Legacy Antigravity configuration identity.
195 ///
196 /// Kept only so existing configuration can be read and cleared. It is not
197 /// a selectable or runnable provider; Gemini users should use [`Google`].
198 ///
199 /// [`Google`]: Self::Google
200 #[serde(alias = "agy")]
201 Antigravity,
202 /// ModelScope — Alibaba's ModelScope inference API (OpenAI-compatible).
203 #[serde(
204 alias = "model-scope",
205 alias = "model_scope",
206 alias = "modelscope-cn",
207 alias = "modelscope_cn"
208 )]
209 Modelscope,
210 /// Google — Gemini OpenAI-compatible endpoint. Its own backend, not an
211 /// OpenAI alias: thought signatures on tool calls are captured and
212 /// replayed per Google's contract.
213 #[serde(
214 alias = "google-gemini",
215 alias = "google_gemini",
216 alias = "gemini",
217 alias = "google-ai",
218 alias = "google_ai",
219 alias = "ai-studio",
220 alias = "aistudio"
221 )]
222 Google,
223 /// Eden AI — OpenAI-compatible AI gateway (aggregator).
224 ///
225 /// Serves a broad catalog of upstream models under `provider/model`
226 /// namespaced wire ids over the OpenAI Chat Completions protocol.
227 #[serde(alias = "eden-ai", alias = "eden_ai", alias = "edenai")]
228 Edenai,
229 /// ZenMux — OpenAI-compatible AI gateway (aggregator).
230 ///
231 /// Serves ~200 upstream models under `provider/model` namespaced wire
232 /// ids over the OpenAI Chat Completions protocol at
233 /// `https://zenmux.ai/api/v1`. The `/models` catalog is keyless-readable.
234 #[serde(alias = "zen-mux", alias = "zen_mux")]
235 Zenmux,
236 /// CSDN 星图 (Starmap) — CSDN's hosted OpenAI-compatible model platform
237 /// and Coding Plan subscription.
238 ///
239 /// The plan model id `glm_for_coding` (GLM-5.2 underneath, 200k context
240 /// cap) bills against plan quota and requires the dedicated Coding Plan
241 /// key type; other marketplace model ids and general keys bill metered
242 /// through the same endpoint.
243 #[serde(
244 alias = "csdn-ai",
245 alias = "csdn_ai",
246 alias = "csdn-coding-plan",
247 alias = "csdn_coding_plan",
248 alias = "starmap"
249 )]
250 Csdn,
251 /// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
252 ///
253 /// Serves a broad catalog of upstream models over the OpenAI Responses
254 /// protocol at `/v1/responses` with a bearer Universal API key. Model ids
255 /// pass through verbatim: a plain catalog id (`gpt-5.6-sol`) lets the
256 /// gateway choose the upstream provider, `provider/model` pins one, and
257 /// `concentrate/auto` reaches the gateway's own `auto` router. Opt-in and
258 /// BYOK only: the key lives in the local secret store and Codewhale adds
259 /// no fee, no managed default, and no resale lane (see docs/PROVIDERS.md).
260 #[serde(
261 alias = "concentrate-ai",
262 alias = "concentrate_ai",
263 alias = "concentrateai"
264 )]
265 Concentrate,
266 /// Codewhale API — account-backed model access over the customer's own
267 /// connected provider keys.
268 ///
269 /// One base URL, one `cwc_key_…` account API key with the `models:infer`
270 /// scope, and a per-model wire chosen from the account's live catalog:
271 /// `GET /v1/models` returns `provider/model` rows carrying
272 /// `codewhale.protocol` (`chat-completions`, `anthropic-messages`, or
273 /// `responses`). Every protocol authenticates with `Authorization: Bearer`
274 /// — the Anthropic passthrough does **not** take `x-api-key`.
275 #[serde(
276 alias = "codewhale-api",
277 alias = "codewhale_api",
278 alias = "cw-api",
279 alias = "codewhale-cloud"
280 )]
281 Codewhale,
282 /// User-defined OpenAI-compatible endpoint (#1519).
283 ///
284 /// A single dynamic identity for arbitrary `[providers.<name>]
285 /// kind="openai-compatible"` entries. It speaks the OpenAI Chat Completions
286 /// wire protocol and carries no built-in base URL/model — the concrete
287 /// endpoint and model arrive via config (`base_url` / `model`) and the
288 /// route's `base_url_override`, never from this static descriptor.
289 Custom,
290 }
291
292 impl ProviderKind {
293 /// Catalog / picker surface: one identity per vendor.
294 ///
295 /// Dual-wire dialect kinds (`*Anthropic`) and Model Studio plan variants
296 /// stay on the enum for serde and `provider_for_kind`, but they are not
297 /// first-class catalog rows. Plan is `mode` / base_url; dialect is
298 /// `wire = openai|anthropic` on the primary provider config.
299 pub const ALL: [Self; 46] = [
300 Self::Deepseek,
301 Self::NvidiaNim,
302 Self::Openai,
303 Self::Atlascloud,
304 Self::WanjieArk,
305 Self::Volcengine,
306 Self::Openrouter,
307 Self::Orcarouter,
308 Self::XiaomiMimo,
309 Self::Novita,
310 Self::Fireworks,
311 Self::Siliconflow,
312 Self::Arcee,
313 Self::SiliconflowCN,
314 Self::Moonshot,
315 Self::Sglang,
316 Self::Vllm,
317 Self::Ollama,
318 Self::OllamaCloud,
319 Self::Huggingface,
320 Self::Together,
321 Self::Qianfan,
322 Self::OpenaiCodex,
323 Self::Anthropic,
324 Self::Openmodel,
325 Self::Zai,
326 Self::Stepfun,
327 Self::Minimax,
328 Self::Deepinfra,
329 Self::Sakana,
330 Self::LongCat,
331 Self::OpencodeGo,
332 Self::OpencodeZen,
333 Self::Meta,
334 Self::Xai,
335 Self::Mistral,
336 Self::Telecomjs,
337 Self::ModelstudioTokenPlan,
338 Self::Modelscope,
339 Self::Google,
340 Self::Edenai,
341 Self::Zenmux,
342 Self::Csdn,
343 Self::Concentrate,
344 Self::Codewhale,
345 Self::Custom,
346 ];
347
348 #[must_use]
349 pub fn all() -> &'static [Self] {
350 &Self::ALL
351 }
352
353 #[must_use]
354 pub fn names_hint() -> String {
355 Self::all()
356 .iter()
357 .map(|provider| provider.as_str())
358 .collect::<Vec<_>>()
359 .join(", ")
360 }
361
362 #[must_use]
363 pub fn as_str(self) -> &'static str {
364 self.provider().id()
365 }
366
367 #[must_use]
368 pub fn parse(value: &str) -> Option<Self> {
369 let trimmed = value.trim();
370 // Gated through `ALL`, not the full registry: the registry still holds
371 // retired tombstones so old config deserializes, and a selection
372 // surface must never be able to resolve one back into a route.
373 Self::all().iter().copied().find(|kind| {
374 let p = kind.provider();
375 trimmed.eq_ignore_ascii_case(p.id())
376 || p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a))
377 })
378 }
379
380 /// Parse a provider identifier for **config-table identity** — the kind
381 /// used to look up credentials, model, and base URL in the user's
382 /// `[providers.*]` tables.
383 ///
384 /// [`parse`](Self::parse) is *catalog* identity: legacy dual-wire
385 /// spellings (`deepseek-anthropic`, `minimax-anthropic`, the Model Studio
386 /// plan/dialect kinds) are aliases of the vendor primary and collapse
387 /// onto it so pickers show one row per vendor. That collapse must not
388 /// decide which config table holds the user's credentials: TOML serde
389 /// keeps the legacy kind for `provider = "deepseek-anthropic"`, so env
390 /// (`CODEWHALE_PROVIDER`) and `config set provider` must resolve the same
391 /// way or the user's own named table is orphaned with the key present.
392 ///
393 /// An exact canonical-id or `provider_config_key` match across the full
394 /// registry (including legacy dialect/plan kinds) therefore wins over
395 /// alias collapse; everything else falls back to [`parse`](Self::parse).
396 /// Wire-endpoint selection is unaffected: it keys off the resolved kind's
397 /// `wire` config, not this parse.
398 #[must_use]
399 pub fn parse_config_identity(value: &str) -> Option<Self> {
400 let trimmed = value.trim();
401 provider::all_providers()
402 .iter()
403 .find(|p| {
404 trimmed.eq_ignore_ascii_case(p.id())
405 || trimmed.eq_ignore_ascii_case(p.provider_config_key())
406 })
407 .map(|p| p.kind())
408 .or_else(|| Self::parse(trimmed))
409 .or_else(|| Self::parse_retired_alias(trimmed))
410 }
411
412 /// Alias lookup restricted to registry entries that are *not* in the
413 /// selectable catalog. Catalog aliases are handled by [`parse`](Self::parse)
414 /// and always take precedence.
415 ///
416 /// Retired kinds still need to answer to their aliases here so a selection
417 /// surface can *name* the tombstone and refuse it, instead of failing to
418 /// recognize `agy` and minting a fresh `[providers.agy]` table that serde
419 /// would fold straight back onto the legacy one.
420 fn parse_retired_alias(trimmed: &str) -> Option<Self> {
421 provider::all_providers()
422 .iter()
423 .filter(|p| !Self::all().contains(&p.kind()))
424 .find(|p| p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a)))
425 .map(|p| p.kind())
426 }
427
428 /// Canonical durable-credential slot in the local secret store.
429 ///
430 /// Most providers own a slot named after their id. Variants authenticated
431 /// by the SAME account share one slot so a single saved key (or logout)
432 /// applies to the whole family:
433 ///
434 /// - `SiliconflowCN` shares `siliconflow` (historical China-endpoint slot,
435 /// already the TUI/CLI convention).
436 /// - The four Alibaba Cloud Model Studio variants share
437 /// `modelstudio-token-plan`: one Model Studio account/key authenticates
438 /// the Token Plan and Coding Plan endpoints in both wire dialects, so
439 /// per-variant slots produced three bogus "missing key" rows whenever
440 /// one variant held the key.
441 #[must_use]
442 pub fn secret_store_slot(self) -> &'static str {
443 match self {
444 Self::SiliconflowCN => "siliconflow",
445 Self::ModelstudioTokenPlan
446 | Self::ModelstudioTokenPlanAnthropic
447 | Self::ModelstudioCodingPlan
448 | Self::ModelstudioCodingPlanAnthropic => "modelstudio-token-plan",
449 _ => self.as_str(),
450 }
451 }
452
453 /// Return the built-in metadata entry for this provider.
454 ///
455 /// This is a metadata foundation only; runtime routing still resolves
456 /// through [`crate::ConfigToml::resolve_runtime_options`].
457 #[must_use]
458 pub fn provider(self) -> &'static dyn provider::Provider {
459 provider::provider_for_kind(self)
460 }
461 }
462
462 lines RUST