返回 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 = "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 /// Jiangsu Telecom TokenHub (OpenAI-compatible).
135 ///
136 /// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
137 /// Completions wire protocol and serves a broad model catalog; each API key
138 /// may access a different subset of models.
139 #[serde(
140 alias = "telecom-js",
141 alias = "telecom_js",
142 alias = "telecomjs-cn",
143 alias = "tokenhub"
144 )]
145 Telecomjs,
146 /// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
147 ///
148 /// Token Plan Personal and Team share the same endpoint. Both the OpenAI
149 /// and Anthropic dialects are available; select the Anthropic dialect via
150 /// `modelstudio-token-plan-anthropic`. Pay-as-you-go workspace-id templating
151 /// is out of scope for v1; use a custom provider for that plan.
152 #[serde(
153 alias = "modelstudio-token-plan",
154 alias = "modelstudio_token_plan",
155 alias = "alibaba-token-plan",
156 alias = "dashscope-token-plan"
157 )]
158 ModelstudioTokenPlan,
159 /// Alibaba Cloud Model Studio — Token Plan Anthropic-compatible endpoint.
160 #[serde(
161 alias = "modelstudio-token-plan-anthropic",
162 alias = "modelstudio_token_plan_anthropic",
163 alias = "alibaba-token-plan-anthropic"
164 )]
165 ModelstudioTokenPlanAnthropic,
166 /// Alibaba Cloud Model Studio — Coding Plan (OpenAI-compatible Chat Completions).
167 #[serde(
168 alias = "modelstudio-coding-plan",
169 alias = "modelstudio_coding_plan",
170 alias = "alibaba-coding-plan",
171 alias = "dashscope-coding-plan"
172 )]
173 ModelstudioCodingPlan,
174 /// Alibaba Cloud Model Studio — Coding Plan Anthropic-compatible endpoint.
175 #[serde(
176 alias = "modelstudio-coding-plan-anthropic",
177 alias = "modelstudio_coding_plan_anthropic",
178 alias = "alibaba-coding-plan-anthropic"
179 )]
180 ModelstudioCodingPlanAnthropic,
181 /// User-defined OpenAI-compatible endpoint (#1519).
182 ///
183 /// A single dynamic identity for arbitrary `[providers.<name>]
184 /// kind="openai-compatible"` entries. It speaks the OpenAI Chat Completions
185 /// wire protocol and carries no built-in base URL/model — the concrete
186 /// endpoint and model arrive via config (`base_url` / `model`) and the
187 /// route's `base_url_override`, never from this static descriptor.
188 Custom,
189 }
190
191 impl ProviderKind {
192 /// Catalog / picker surface: one identity per vendor.
193 ///
194 /// Dual-wire dialect kinds (`*Anthropic`) and Model Studio plan variants
195 /// stay on the enum for serde and `provider_for_kind`, but they are not
196 /// first-class catalog rows. Plan is `mode` / base_url; dialect is
197 /// `wire = openai|anthropic` on the primary provider config.
198 pub const ALL: [Self; 36] = [
199 Self::Deepseek,
200 Self::NvidiaNim,
201 Self::Openai,
202 Self::Atlascloud,
203 Self::WanjieArk,
204 Self::Volcengine,
205 Self::Openrouter,
206 Self::XiaomiMimo,
207 Self::Novita,
208 Self::Fireworks,
209 Self::Siliconflow,
210 Self::Arcee,
211 Self::SiliconflowCN,
212 Self::Moonshot,
213 Self::Sglang,
214 Self::Vllm,
215 Self::Ollama,
216 Self::Huggingface,
217 Self::Together,
218 Self::Qianfan,
219 Self::OpenaiCodex,
220 Self::Anthropic,
221 Self::Openmodel,
222 Self::Zai,
223 Self::Stepfun,
224 Self::Minimax,
225 Self::Deepinfra,
226 Self::Sakana,
227 Self::LongCat,
228 Self::OpencodeGo,
229 Self::OpencodeZen,
230 Self::Meta,
231 Self::Xai,
232 Self::Telecomjs,
233 Self::ModelstudioTokenPlan,
234 Self::Custom,
235 ];
236
237 #[must_use]
238 pub fn all() -> &'static [Self] {
239 &Self::ALL
240 }
241
242 #[must_use]
243 pub fn names_hint() -> String {
244 Self::all()
245 .iter()
246 .map(|provider| provider.as_str())
247 .collect::<Vec<_>>()
248 .join(", ")
249 }
250
251 #[must_use]
252 pub fn as_str(self) -> &'static str {
253 self.provider().id()
254 }
255
256 #[must_use]
257 pub fn parse(value: &str) -> Option<Self> {
258 let trimmed = value.trim();
259 provider::all_providers()
260 .iter()
261 .find(|p| {
262 trimmed.eq_ignore_ascii_case(p.id())
263 || p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a))
264 })
265 .map(|p| p.kind())
266 }
267
268 /// Parse a provider identifier for **config-table identity** — the kind
269 /// used to look up credentials, model, and base URL in the user's
270 /// `[providers.*]` tables.
271 ///
272 /// [`parse`](Self::parse) is *catalog* identity: legacy dual-wire
273 /// spellings (`deepseek-anthropic`, `minimax-anthropic`, the Model Studio
274 /// plan/dialect kinds) are aliases of the vendor primary and collapse
275 /// onto it so pickers show one row per vendor. That collapse must not
276 /// decide which config table holds the user's credentials: TOML serde
277 /// keeps the legacy kind for `provider = "deepseek-anthropic"`, so env
278 /// (`CODEWHALE_PROVIDER`) and `config set provider` must resolve the same
279 /// way or the user's own named table is orphaned with the key present.
280 ///
281 /// An exact canonical-id or `provider_config_key` match across the full
282 /// registry (including legacy dialect/plan kinds) therefore wins over
283 /// alias collapse; everything else falls back to [`parse`](Self::parse).
284 /// Wire-endpoint selection is unaffected: it keys off the resolved kind's
285 /// `wire` config, not this parse.
286 #[must_use]
287 pub fn parse_config_identity(value: &str) -> Option<Self> {
288 let trimmed = value.trim();
289 provider::all_providers()
290 .iter()
291 .find(|p| {
292 trimmed.eq_ignore_ascii_case(p.id())
293 || trimmed.eq_ignore_ascii_case(p.provider_config_key())
294 })
295 .map(|p| p.kind())
296 .or_else(|| Self::parse(trimmed))
297 }
298
299 #[must_use]
300 pub fn is_siliconflow(self) -> bool {
301 matches!(self, Self::Siliconflow | Self::SiliconflowCN)
302 }
303
304 /// Canonical durable-credential slot in the local secret store.
305 ///
306 /// Most providers own a slot named after their id. Variants authenticated
307 /// by the SAME account share one slot so a single saved key (or logout)
308 /// applies to the whole family:
309 ///
310 /// - `SiliconflowCN` shares `siliconflow` (historical China-endpoint slot,
311 /// already the TUI/CLI convention).
312 /// - The four Alibaba Cloud Model Studio variants share
313 /// `modelstudio-token-plan`: one Model Studio account/key authenticates
314 /// the Token Plan and Coding Plan endpoints in both wire dialects, so
315 /// per-variant slots produced three bogus "missing key" rows whenever
316 /// one variant held the key.
317 #[must_use]
318 pub fn secret_store_slot(self) -> &'static str {
319 match self {
320 Self::SiliconflowCN => "siliconflow",
321 Self::ModelstudioTokenPlan
322 | Self::ModelstudioTokenPlanAnthropic
323 | Self::ModelstudioCodingPlan
324 | Self::ModelstudioCodingPlanAnthropic => "modelstudio-token-plan",
325 _ => self.as_str(),
326 }
327 }
328
329 /// Return the built-in metadata entry for this provider.
330 ///
331 /// This is a metadata foundation only; runtime routing still resolves
332 /// through [`crate::ConfigToml::resolve_runtime_options`].
333 #[must_use]
334 pub fn provider(self) -> &'static dyn provider::Provider {
335 provider::provider_for_kind(self)
336 }
337 }
338
338 lines RUST