返回 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 // `rename_all = "kebab-case"` would spell this variant `siliconflow-c-n`,
58 // which is not the canonical id (`siliconflow-CN`) and which the config
59 // loader rejects. Serialize the canonical id; keep reading the old
60 // spelling so files written by earlier releases still load.
61 #[serde(
62 rename = "siliconflow-CN",
63 alias = "siliconflow-cn",
64 alias = "siliconflow-c-n"
65 )]
66 SiliconflowCN,
67 #[serde(alias = "moonshot-ai", alias = "moonshotai", alias = "moonshot_ai")]
68 Moonshot,
69 Sglang,
70 Vllm,
71 Ollama,
72 #[serde(alias = "ollama_cloud")]
73 OllamaCloud,
74 #[serde(alias = "hugging-face", alias = "hugging_face", alias = "hf")]
75 Huggingface,
76 #[serde(alias = "together-ai", alias = "together_ai", alias = "togetherai")]
77 Together,
78 #[serde(alias = "baidu-qianfan", alias = "baidu_qianfan", alias = "baidu")]
79 Qianfan,
80 #[serde(
81 alias = "openai-codex",
82 alias = "openai_codex",
83 alias = "codex",
84 alias = "chatgpt",
85 alias = "chatgpt-codex",
86 alias = "chatgpt_codex"
87 )]
88 OpenaiCodex,
89 #[serde(alias = "claude")]
90 Anthropic,
91 #[serde(alias = "open-model", alias = "open_model")]
92 Openmodel,
93 #[serde(
94 alias = "z-ai",
95 alias = "z_ai",
96 alias = "z.ai",
97 alias = "zhipu",
98 alias = "zhipuai",
99 alias = "bigmodel",
100 alias = "big-model"
101 )]
102 Zai,
103 #[serde(
104 alias = "step-fun",
105 alias = "step_fun",
106 alias = "stepfun",
107 alias = "stepflash",
108 alias = "step-flash",
109 alias = "step_flash"
110 )]
111 Stepfun,
112 #[serde(alias = "mini-max", alias = "mini_max", alias = "minimax")]
113 Minimax,
114 #[serde(
115 alias = "minimax_anthropic",
116 alias = "mini-max-anthropic",
117 alias = "mini_max_anthropic"
118 )]
119 MinimaxAnthropic,
120 #[serde(alias = "deep-infra", alias = "deep_infra")]
121 Deepinfra,
122 #[serde(alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")]
123 Sakana,
124 #[serde(
125 rename = "longcat",
126 alias = "long-cat",
127 alias = "meituan-longcat",
128 alias = "meituan"
129 )]
130 LongCat,
131 #[serde(alias = "opencode_go", alias = "opencodego")]
132 OpencodeGo,
133 #[serde(
134 alias = "opencode_zen",
135 alias = "opencodezen",
136 alias = "zen",
137 alias = "opencode"
138 )]
139 OpencodeZen,
140 #[serde(
141 alias = "meta-ai",
142 alias = "meta_ai",
143 alias = "meta-model-api",
144 alias = "meta_model_api",
145 alias = "muse",
146 alias = "muse-spark"
147 )]
148 Meta,
149 #[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
150 Xai,
151 /// Mistral AI — la Plateforme (OpenAI-compatible Chat Completions).
152 #[serde(
153 alias = "mistral-ai",
154 alias = "mistral_ai",
155 alias = "mistralai",
156 alias = "la-plateforme",
157 alias = "la_plateforme"
158 )]
159 Mistral,
160 /// Jiangsu Telecom TokenHub (OpenAI-compatible).
161 ///
162 /// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
163 /// Completions wire protocol and serves a broad model catalog; each API key
164 /// may access a different subset of models.
165 #[serde(
166 alias = "telecom-js",
167 alias = "telecom_js",
168 alias = "telecomjs-cn",
169 alias = "tokenhub"
170 )]
171 Telecomjs,
172 /// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
173 ///
174 /// Token Plan Personal and Team share the same endpoint. Both the OpenAI
175 /// and Anthropic dialects are available; select the Anthropic dialect via
176 /// `modelstudio-token-plan-anthropic`. Pay-as-you-go workspace-id templating
177 /// is out of scope for v1; use a custom provider for that plan.
178 #[serde(
179 alias = "modelstudio-token-plan",
180 alias = "modelstudio_token_plan",
181 alias = "alibaba-token-plan",
182 alias = "dashscope-token-plan"
183 )]
184 ModelstudioTokenPlan,
185 /// Alibaba Cloud Model Studio — Token Plan Anthropic-compatible endpoint.
186 #[serde(
187 alias = "modelstudio-token-plan-anthropic",
188 alias = "modelstudio_token_plan_anthropic",
189 alias = "alibaba-token-plan-anthropic"
190 )]
191 ModelstudioTokenPlanAnthropic,
192 /// Alibaba Cloud Model Studio — Coding Plan (OpenAI-compatible Chat Completions).
193 #[serde(
194 alias = "modelstudio-coding-plan",
195 alias = "modelstudio_coding_plan",
196 alias = "alibaba-coding-plan",
197 alias = "dashscope-coding-plan"
198 )]
199 ModelstudioCodingPlan,
200 /// Alibaba Cloud Model Studio — Coding Plan Anthropic-compatible endpoint.
201 #[serde(
202 alias = "modelstudio-coding-plan-anthropic",
203 alias = "modelstudio_coding_plan_anthropic",
204 alias = "alibaba-coding-plan-anthropic"
205 )]
206 ModelstudioCodingPlanAnthropic,
207 /// Legacy Antigravity configuration identity.
208 ///
209 /// Kept only so existing configuration can be read and cleared. It is not
210 /// a selectable or runnable provider; Gemini users should use [`Google`].
211 ///
212 /// [`Google`]: Self::Google
213 #[serde(alias = "agy")]
214 Antigravity,
215 /// ModelScope — Alibaba's ModelScope inference API (OpenAI-compatible).
216 #[serde(
217 alias = "model-scope",
218 alias = "model_scope",
219 alias = "modelscope-cn",
220 alias = "modelscope_cn"
221 )]
222 Modelscope,
223 /// Google — Gemini OpenAI-compatible endpoint. Its own backend, not an
224 /// OpenAI alias: thought signatures on tool calls are captured and
225 /// replayed per Google's contract.
226 #[serde(
227 alias = "google-gemini",
228 alias = "google_gemini",
229 alias = "gemini",
230 alias = "google-ai",
231 alias = "google_ai",
232 alias = "ai-studio",
233 alias = "aistudio"
234 )]
235 Google,
236 /// Eden AI — OpenAI-compatible AI gateway (aggregator).
237 ///
238 /// Serves a broad catalog of upstream models under `provider/model`
239 /// namespaced wire ids over the OpenAI Chat Completions protocol.
240 #[serde(alias = "eden-ai", alias = "eden_ai", alias = "edenai")]
241 Edenai,
242 /// ZenMux — OpenAI-compatible AI gateway (aggregator).
243 ///
244 /// Serves ~200 upstream models under `provider/model` namespaced wire
245 /// ids over the OpenAI Chat Completions protocol at
246 /// `https://zenmux.ai/api/v1`. The `/models` catalog is keyless-readable.
247 #[serde(alias = "zen-mux", alias = "zen_mux")]
248 Zenmux,
249 /// CSDN 星图 (Starmap) — CSDN's hosted OpenAI-compatible model platform
250 /// and Coding Plan subscription.
251 ///
252 /// The plan model id `glm_for_coding` (GLM-5.2 underneath, 200k context
253 /// cap) bills against plan quota and requires the dedicated Coding Plan
254 /// key type; other marketplace model ids and general keys bill metered
255 /// through the same endpoint.
256 #[serde(
257 alias = "csdn-ai",
258 alias = "csdn_ai",
259 alias = "csdn-coding-plan",
260 alias = "csdn_coding_plan",
261 alias = "starmap"
262 )]
263 Csdn,
264 /// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
265 ///
266 /// Serves a broad catalog of upstream models over the OpenAI Responses
267 /// protocol at `/v1/responses` with a bearer Universal API key. Model ids
268 /// pass through verbatim: a plain catalog id (`gpt-5.6-sol`) lets the
269 /// gateway choose the upstream provider, `provider/model` pins one, and
270 /// `concentrate/auto` reaches the gateway's own `auto` router. Opt-in and
271 /// BYOK only: the key lives in the local secret store and Codewhale adds
272 /// no fee, no managed default, and no resale lane (see docs/PROVIDERS.md).
273 #[serde(
274 alias = "concentrate-ai",
275 alias = "concentrate_ai",
276 alias = "concentrateai"
277 )]
278 Concentrate,
279 /// Codewhale API — account-backed model access over the customer's own
280 /// connected provider keys.
281 ///
282 /// One base URL, one `cwc_key_…` account API key with the `models:infer`
283 /// scope, and a per-model wire chosen from the account's live catalog:
284 /// `GET /v1/models` returns `provider/model` rows carrying
285 /// `codewhale.protocol` (`chat-completions`, `anthropic-messages`, or
286 /// `responses`). Every protocol authenticates with `Authorization: Bearer`
287 /// — the Anthropic passthrough does **not** take `x-api-key`.
288 #[serde(
289 alias = "codewhale-api",
290 alias = "codewhale_api",
291 alias = "cw-api",
292 alias = "codewhale-cloud"
293 )]
294 Codewhale,
295 /// User-defined OpenAI-compatible endpoint (#1519).
296 ///
297 /// A single dynamic identity for arbitrary `[providers.<name>]
298 /// kind="openai-compatible"` entries. It speaks the OpenAI Chat Completions
299 /// wire protocol and carries no built-in base URL/model — the concrete
300 /// endpoint and model arrive via config (`base_url` / `model`) and the
301 /// route's `base_url_override`, never from this static descriptor.
302 Custom,
303 }
304
305 impl ProviderKind {
306 /// Catalog / picker surface: one identity per vendor.
307 ///
308 /// Dual-wire dialect kinds (`*Anthropic`) and Model Studio plan variants
309 /// stay on the enum for serde and `provider_for_kind`, but they are not
310 /// first-class catalog rows. Plan is `mode` / base_url; dialect is
311 /// `wire = openai|anthropic` on the primary provider config.
312 pub const ALL: [Self; 46] = crate::descriptors::SELECTABLE_PROVIDER_KINDS;
313
314 #[must_use]
315 pub fn all() -> &'static [Self] {
316 &Self::ALL
317 }
318
319 #[must_use]
320 pub fn names_hint() -> String {
321 Self::all()
322 .iter()
323 .map(|provider| provider.as_str())
324 .collect::<Vec<_>>()
325 .join(", ")
326 }
327
328 #[must_use]
329 pub fn as_str(self) -> &'static str {
330 self.provider().id()
331 }
332
333 #[must_use]
334 pub fn parse(value: &str) -> Option<Self> {
335 let trimmed = value.trim();
336 // Gated through `ALL`, not the full registry: the registry still holds
337 // retired tombstones so old config deserializes, and a selection
338 // surface must never be able to resolve one back into a route.
339 Self::all().iter().copied().find(|kind| {
340 let p = kind.provider();
341 crate::descriptors::builtin_provider_descriptor(*kind).selectable
342 && (trimmed.eq_ignore_ascii_case(p.id())
343 || p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a)))
344 })
345 }
346
347 /// Parse a provider identifier for **config-table identity** — the kind
348 /// used to look up credentials, model, and base URL in the user's
349 /// `[providers.*]` tables.
350 ///
351 /// [`parse`](Self::parse) is *catalog* identity: legacy dual-wire
352 /// spellings (`deepseek-anthropic`, `minimax-anthropic`, the Model Studio
353 /// plan/dialect kinds) are aliases of the vendor primary and collapse
354 /// onto it so pickers show one row per vendor. That collapse must not
355 /// decide which config table holds the user's credentials: TOML serde
356 /// keeps the legacy kind for `provider = "deepseek-anthropic"`, so env
357 /// (`CODEWHALE_PROVIDER`) and `config set provider` must resolve the same
358 /// way or the user's own named table is orphaned with the key present.
359 ///
360 /// An exact canonical-id or `provider_config_key` match across the full
361 /// registry (including legacy dialect/plan kinds) therefore wins over
362 /// alias collapse; everything else falls back to [`parse`](Self::parse).
363 /// Wire-endpoint selection is unaffected: it keys off the resolved kind's
364 /// `wire` config, not this parse.
365 #[must_use]
366 pub fn parse_config_identity(value: &str) -> Option<Self> {
367 let trimmed = value.trim();
368 provider::all_providers()
369 .iter()
370 .find(|p| {
371 trimmed.eq_ignore_ascii_case(p.id())
372 || trimmed.eq_ignore_ascii_case(p.provider_config_key())
373 })
374 .map(|p| p.kind())
375 .or_else(|| Self::parse(trimmed))
376 .or_else(|| Self::parse_retired_alias(trimmed))
377 }
378
379 /// Alias lookup restricted to registry entries that are *not* in the
380 /// selectable catalog. Catalog aliases are handled by [`parse`](Self::parse)
381 /// and always take precedence.
382 ///
383 /// Retired kinds still need to answer to their aliases here so a selection
384 /// surface can *name* the tombstone and refuse it, instead of failing to
385 /// recognize `agy` and minting a fresh `[providers.agy]` table that serde
386 /// would fold straight back onto the legacy one.
387 fn parse_retired_alias(trimmed: &str) -> Option<Self> {
388 provider::all_providers()
389 .iter()
390 .filter(|p| !Self::all().contains(&p.kind()))
391 .find(|p| p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a)))
392 .map(|p| p.kind())
393 }
394
395 /// Canonical durable-credential slot in the local secret store.
396 ///
397 /// Most providers own a slot named after their id. Variants authenticated
398 /// by the SAME account share one slot so a single saved key (or logout)
399 /// applies to the whole family:
400 ///
401 /// - `SiliconflowCN` shares `siliconflow` (historical China-endpoint slot,
402 /// already the TUI/CLI convention).
403 /// - The four Alibaba Cloud Model Studio variants share
404 /// `modelstudio-token-plan`: one Model Studio account/key authenticates
405 /// the Token Plan and Coding Plan endpoints in both wire dialects, so
406 /// per-variant slots produced three bogus "missing key" rows whenever
407 /// one variant held the key.
408 #[must_use]
409 pub fn secret_store_slot(self) -> &'static str {
410 crate::descriptors::builtin_provider_descriptor(self).secret_store_slot
411 }
412
413 /// Return the built-in metadata entry for this provider.
414 ///
415 /// This is a metadata foundation only; runtime routing still resolves
416 /// through [`crate::ConfigToml::resolve_runtime_options`].
417 #[must_use]
418 pub fn provider(self) -> &'static dyn provider::Provider {
419 provider::provider_for_kind(self)
420 }
421 }
422
422 lines RUST