| 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 |