1use super::{
8 DEFAULT_ARCEE_BASE_URL, DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL,
9 DEFAULT_ATLASCLOUD_MODEL, DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL,
10 DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
11 DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL,
12 DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
13 DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
14 DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
15 DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
16 DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
17 DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL,
18 DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL,
19 DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL,
20 DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL,
21 DEFAULT_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL,
22 DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL,
23 DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL,
24 DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL,
25 DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL,
26 DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL,
27 DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL, DEFAULT_TOGETHER_BASE_URL,
28 DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
29 DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
30 DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
31 DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL,
32 MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL, MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
33 ProviderKind,
34};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum WireFormat {
40 ChatCompletions,
42 Responses,
44 AnthropicMessages,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum CredentialAcquisition {
55 ApiKey,
57 ApiKeyOrOAuth,
59 LocalOptional,
61 OAuth,
63 Configuration,
65}
66
67impl CredentialAcquisition {
68 #[must_use]
70 pub const fn as_str(self) -> &'static str {
71 match self {
72 Self::ApiKey => "api_key",
73 Self::ApiKeyOrOAuth => "api_key_or_oauth",
74 Self::LocalOptional => "local_optional",
75 Self::OAuth => "oauth",
76 Self::Configuration => "configuration",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum WirePolicy {
84 Fixed(WireFormat),
86 ModelAware,
88}
89
90impl WirePolicy {
91 #[must_use]
93 pub const fn fixed(self) -> Option<WireFormat> {
94 match self {
95 Self::Fixed(format) => Some(format),
96 Self::ModelAware => None,
97 }
98 }
99
100 #[must_use]
102 pub fn resolve(self, endpoint_key: &str) -> Option<WireFormat> {
103 if let Self::Fixed(format) = self {
104 return Some(format);
105 }
106
107 match endpoint_key.trim().to_ascii_lowercase().as_str() {
108 "chat" | "chat_completions" | "chat-completions" => Some(WireFormat::ChatCompletions),
109 "responses" => Some(WireFormat::Responses),
110 "messages" | "anthropic_messages" | "anthropic-messages" => {
111 Some(WireFormat::AnthropicMessages)
112 }
113 _ => None,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct CredentialHelp {
121 pub acquisition: CredentialAcquisition,
122 pub credential_url: Option<&'static str>,
127 pub docs_url: Option<&'static str>,
129 pub guidance: &'static str,
131}
132
133pub const KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL: &str = "https://www.kimi.com/code/console";
139
140pub trait Provider: Send + Sync {
142 fn kind(&self) -> ProviderKind;
144
145 fn id(&self) -> &'static str {
147 self.kind().as_str()
148 }
149
150 fn display_name(&self) -> &'static str;
152
153 fn default_base_url(&self) -> &'static str;
155
156 fn default_model(&self) -> &'static str;
158
159 fn env_vars(&self) -> &'static [&'static str];
161
162 fn provider_config_key(&self) -> &'static str;
164
165 fn aliases(&self) -> &'static [&'static str] {
167 &[]
168 }
169
170 fn wire_policy(&self) -> WirePolicy {
172 WirePolicy::Fixed(WireFormat::ChatCompletions)
173 }
174
175 fn credential_help(&self) -> CredentialHelp {
178 credential_help(self.kind())
179 }
180}
181
182#[must_use]
191pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
192 use CredentialAcquisition::{ApiKey, ApiKeyOrOAuth, Configuration, LocalOptional, OAuth};
193
194 match kind {
195 ProviderKind::Deepseek | ProviderKind::DeepseekAnthropic => CredentialHelp {
196 acquisition: ApiKey,
197 credential_url: Some("https://platform.deepseek.com/api_keys"),
198 docs_url: Some("https://api-docs.deepseek.com/"),
199 guidance: "Create an API key in the DeepSeek platform console.",
200 },
201 ProviderKind::NvidiaNim => CredentialHelp {
202 acquisition: ApiKey,
203 credential_url: Some("https://build.nvidia.com/settings/api-keys"),
204 docs_url: Some("https://build.nvidia.com/explore/discover"),
205 guidance: "Create an NVIDIA NIM key in the NVIDIA build console.",
206 },
207 ProviderKind::Openai => CredentialHelp {
208 acquisition: ApiKey,
209 credential_url: Some("https://platform.openai.com/api-keys"),
210 docs_url: Some("https://platform.openai.com/docs/api-reference"),
211 guidance: "Create an OpenAI API key, or configure the credential for your compatible endpoint.",
212 },
213 ProviderKind::Atlascloud => CredentialHelp {
214 acquisition: ApiKey,
215 credential_url: Some("https://atlascloud.ai/docs/en/api-keys"),
216 docs_url: Some("https://atlascloud.ai/docs/en/api-keys"),
217 guidance: "Follow Atlas Cloud's API Keys guide to create a credential.",
218 },
219 ProviderKind::WanjieArk => CredentialHelp {
220 acquisition: ApiKey,
221 credential_url: Some("https://docs.wanjiedata.com/maas/maas-openapi-v1.html"),
222 docs_url: Some("https://docs.wanjiedata.com/maas/maas-openapi-v1.html"),
223 guidance: "Follow Wanjie MaaS's APIKEY guide to create a credential.",
224 },
225 ProviderKind::Volcengine => CredentialHelp {
226 acquisition: ApiKey,
227 credential_url: Some("https://console.volcengine.com/ark/apiKey"),
228 docs_url: Some("https://www.volcengine.com/docs/82379/1541594"),
229 guidance: "Create a Volcengine Ark API key in the Ark console.",
230 },
231 ProviderKind::Openrouter => CredentialHelp {
232 acquisition: ApiKey,
233 credential_url: Some("https://openrouter.ai/settings/keys"),
234 docs_url: Some("https://openrouter.ai/docs/api/reference/authentication"),
235 guidance: "Create an OpenRouter key from account settings.",
236 },
237 ProviderKind::XiaomiMimo => CredentialHelp {
238 acquisition: ApiKey,
239 credential_url: Some("https://platform.xiaomimimo.com/token-plan"),
240 docs_url: Some("https://mimo.mi.com/docs/en-US/tokenplan/Token%20Plan/subscription"),
241 guidance: "Create a Xiaomi MiMo Token Plan or pay-as-you-go key and keep its matching base URL.",
242 },
243 ProviderKind::Novita => CredentialHelp {
244 acquisition: ApiKey,
245 credential_url: Some("https://novita.ai/en/settings/key-management"),
246 docs_url: Some("https://novita.ai/docs/guides/quickstart"),
247 guidance: "Create a Novita key in account Key Management.",
248 },
249 ProviderKind::Fireworks => CredentialHelp {
250 acquisition: ApiKey,
251 credential_url: Some("https://fireworks.ai/api-keys"),
252 docs_url: Some("https://docs.fireworks.ai/getting-started/quickstart"),
253 guidance: "Create a Fireworks API key before configuring the provider.",
254 },
255 ProviderKind::Siliconflow => CredentialHelp {
256 acquisition: ApiKey,
257 credential_url: Some("https://cloud.siliconflow.com/account/ak"),
258 docs_url: Some("https://docs.siliconflow.com/en/userguide/quickstart"),
259 guidance: "Use the global SiliconFlow console for the global endpoint.",
260 },
261 ProviderKind::SiliconflowCN => CredentialHelp {
262 acquisition: ApiKey,
263 credential_url: Some("https://cloud.siliconflow.cn/account/ak"),
264 docs_url: Some("https://docs.siliconflow.cn/en/userguide/quickstart"),
265 guidance: "Use the China SiliconFlow console for the China endpoint.",
266 },
267 ProviderKind::Arcee => CredentialHelp {
268 acquisition: ApiKey,
269 credential_url: Some("https://docs.arcee.ai/other/create-your-first-api-key"),
270 docs_url: Some("https://docs.arcee.ai/other/create-your-first-api-key"),
271 guidance: "Follow Arcee's API key guide to create a credential.",
272 },
273 ProviderKind::Moonshot => CredentialHelp {
274 acquisition: ApiKey,
275 credential_url: Some("https://platform.kimi.ai/console/api-keys"),
276 docs_url: Some("https://platform.kimi.ai/docs/overview"),
277 guidance: "For Moonshot's default direct API route, sign in to Kimi API Platform and create and copy an API key. A configured Kimi Code route uses a separate membership-plan console and never imports Kimi CLI credentials; first-class Kimi OAuth is not available.",
278 },
279 ProviderKind::Sglang => CredentialHelp {
280 acquisition: LocalOptional,
281 credential_url: None,
282 docs_url: Some("https://docs.sglang.ai/"),
283 guidance: "Self-hosted SGLang is keyless by default; configure a key only if your server requires one.",
284 },
285 ProviderKind::Vllm => CredentialHelp {
286 acquisition: LocalOptional,
287 credential_url: None,
288 docs_url: Some("https://docs.vllm.ai/en/stable/serving/openai_compatible_server/"),
289 guidance: "Self-hosted vLLM is keyless by default; configure a key only if your server requires one.",
290 },
291 ProviderKind::Ollama => CredentialHelp {
292 acquisition: LocalOptional,
293 credential_url: None,
294 docs_url: Some("https://docs.ollama.com/api"),
295 guidance: "Local Ollama is keyless by default; configure a key only if your server requires one.",
296 },
297 ProviderKind::Huggingface => CredentialHelp {
298 acquisition: ApiKey,
299 credential_url: Some("https://huggingface.co/settings/tokens"),
300 docs_url: Some("https://huggingface.co/docs/hub/en/security-tokens"),
301 guidance: "Create a scoped Hugging Face access token.",
302 },
303 ProviderKind::Together => CredentialHelp {
304 acquisition: ApiKey,
305 credential_url: Some("https://api.together.ai/settings/api-keys"),
306 docs_url: Some("https://docs.together.ai/docs/api-keys-authentication"),
307 guidance: "Create a Together API key from account settings.",
308 },
309 ProviderKind::Qianfan => CredentialHelp {
310 acquisition: ApiKey,
311 credential_url: Some("https://console.bce.baidu.com/iam/#/iam/accesslist"),
312 docs_url: Some("https://cloud.baidu.com/doc/qianfan/index.html"),
313 guidance: "Create Baidu Qianfan credentials in the Baidu Cloud console.",
314 },
315 ProviderKind::OpenaiCodex => CredentialHelp {
316 acquisition: OAuth,
317 credential_url: None,
318 docs_url: Some("https://developers.openai.com/codex/"),
319 guidance: "Run `codex login`, then explicitly grant Codewhale read-only access to that exact Codex credential file; or use a process-scoped token environment variable.",
320 },
321 ProviderKind::Anthropic => CredentialHelp {
322 acquisition: ApiKey,
323 credential_url: Some("https://console.anthropic.com/settings/keys"),
324 docs_url: Some("https://docs.anthropic.com/en/api/overview"),
325 guidance: "Create an Anthropic API key in the Anthropic Console.",
326 },
327 ProviderKind::Openmodel => CredentialHelp {
328 acquisition: ApiKey,
329 credential_url: Some("https://console.openmodel.ai/"),
330 docs_url: Some("https://docs.openmodel.ai/en/docs/getting-started/authentication"),
331 guidance: "Create an API key in the OpenModel console, then follow the authentication guide.",
332 },
333 ProviderKind::Zai => CredentialHelp {
334 acquisition: ApiKey,
335 credential_url: Some("https://z.ai/model-api"),
336 docs_url: Some("https://docs.z.ai/api-reference/introduction"),
337 guidance: "Create or manage a Z.ai API key from the Model API page.",
338 },
339 ProviderKind::Stepfun => CredentialHelp {
340 acquisition: ApiKey,
341 credential_url: Some("https://platform.stepfun.ai/"),
342 docs_url: Some("https://platform.stepfun.ai/docs/en/quickstart/overview"),
343 guidance: "Open Account Management, then Interface Keys, in the StepFun console.",
344 },
345 ProviderKind::Minimax | ProviderKind::MinimaxAnthropic => CredentialHelp {
346 acquisition: ApiKey,
347 credential_url: Some(
348 "https://platform.minimax.io/user-center/basic-information/interface-key",
349 ),
350 docs_url: Some("https://platform.minimax.io/docs/api-reference/api-overview"),
351 guidance: "Create a MiniMax API key or subscription-plan key in the user center.",
352 },
353 ProviderKind::Deepinfra => CredentialHelp {
354 acquisition: ApiKey,
355 credential_url: Some("https://deepinfra.com/dash/api_keys"),
356 docs_url: Some("https://docs.deepinfra.com/quickstart"),
357 guidance: "Create a DeepInfra API key from the dashboard.",
358 },
359 ProviderKind::Sakana => CredentialHelp {
360 acquisition: ApiKey,
361 credential_url: Some("https://console.sakana.ai/api-keys"),
362 docs_url: Some("https://console.sakana.ai/get-started"),
363 guidance: "Create a Sakana AI key in the console and copy it when shown.",
364 },
365 ProviderKind::LongCat => CredentialHelp {
366 acquisition: ApiKey,
367 credential_url: Some("https://longcat.chat/platform"),
368 docs_url: Some("https://longcat.chat/platform"),
369 guidance: "Sign up on the LongCat platform and create an API key.",
370 },
371 ProviderKind::OpencodeGo => CredentialHelp {
372 acquisition: ApiKey,
373 credential_url: Some("https://opencode.ai/zen/"),
374 docs_url: Some("https://opencode.ai/docs/go/"),
375 guidance: "Create or copy an OpenCode Go subscription key from OpenCode Zen.",
376 },
377 ProviderKind::OpencodeZen => CredentialHelp {
378 acquisition: ApiKey,
379 credential_url: Some("https://opencode.ai/zen/"),
380 docs_url: Some("https://opencode.ai/docs/zen/"),
381 guidance: "Create or copy an OpenCode Zen API key from OpenCode Zen.",
382 },
383 ProviderKind::Meta => CredentialHelp {
384 acquisition: ApiKey,
385 credential_url: Some("https://developer.meta.com/ai/"),
386 docs_url: Some("https://developer.meta.com/ai/resources/blog/build-with-muse-spark/"),
387 guidance: "Use the Meta developer portal to obtain Model API access and a key.",
388 },
389 ProviderKind::Xai => CredentialHelp {
390 acquisition: ApiKeyOrOAuth,
391 credential_url: Some("https://console.x.ai/"),
392 docs_url: None,
393 guidance: "Use an xAI Console API key or Codewhale's native device login. Reading an existing Grok CLI file requires explicit provider-scoped read-only consent.",
394 },
395 ProviderKind::Mistral => CredentialHelp {
396 acquisition: ApiKey,
397 credential_url: Some("https://console.mistral.ai/api-keys"),
398 docs_url: Some("https://docs.mistral.ai/"),
399 guidance: "Create a Mistral API key in the Mistral Console (la Plateforme).",
400 },
401 ProviderKind::Telecomjs => CredentialHelp {
402 acquisition: ApiKey,
403 credential_url: Some("https://aigw.telecomjs.com/"),
404 docs_url: None,
405 guidance: "Create a TelecomJS TokenHub API key, then use the provider's live model catalog to discover the models available to that key.",
406 },
407 ProviderKind::ModelstudioTokenPlan
408 | ProviderKind::ModelstudioTokenPlanAnthropic
409 | ProviderKind::ModelstudioCodingPlan
410 | ProviderKind::ModelstudioCodingPlanAnthropic => CredentialHelp {
411 acquisition: ApiKey,
412 credential_url: Some("https://bailian.console.aliyun.com/"),
413 docs_url: Some("https://www.alibabacloud.com/help/en/model-studio/"),
414 guidance: "Sign in to Alibaba Cloud Model Studio (Bailian console), create or copy an API key, and select the plan endpoint matching your subscription (Token Plan or Coding Plan).",
415 },
416 ProviderKind::Custom => CredentialHelp {
417 acquisition: Configuration,
418 credential_url: None,
419 docs_url: None,
420 guidance: "Set this custom provider's base_url and api_key_env or api_key in configuration; no canonical vendor credential page exists.",
421 },
422 }
423}
424
425fn is_exact_https_route(base_url: &str, expected_authority: &str, expected_path: &str) -> bool {
426 let trimmed = base_url.trim();
432 let normalized = trimmed.strip_suffix('/').unwrap_or(trimmed);
433 let Some((scheme, authority_and_path)) = normalized.split_once("://") else {
434 return false;
435 };
436 let Some((authority, path)) = authority_and_path.split_once('/') else {
437 return false;
438 };
439
440 scheme.eq_ignore_ascii_case("https")
441 && authority.eq_ignore_ascii_case(expected_authority)
442 && path == expected_path
443}
444
445#[must_use]
450pub fn is_exact_kimi_code_route(kind: ProviderKind, base_url: &str) -> bool {
451 if kind != ProviderKind::Moonshot {
452 return false;
453 }
454
455 is_exact_https_route(base_url, "api.kimi.com", "coding/v1")
456}
457
458#[must_use]
464pub fn is_exact_moonshot_platform_route(kind: ProviderKind, base_url: &str) -> bool {
465 kind == ProviderKind::Moonshot && is_exact_https_route(base_url, "api.moonshot.ai", "v1")
466}
467
468#[must_use]
475pub fn is_exact_zai_chat_route(kind: ProviderKind, base_url: &str) -> bool {
476 kind == ProviderKind::Zai
477 && (is_exact_https_route(base_url, "api.z.ai", "api/coding/paas/v4")
478 || is_exact_https_route(base_url, "api.z.ai", "api/paas/v4"))
479}
480
481#[must_use]
487pub fn is_exact_minimax_chat_route(kind: ProviderKind, base_url: &str) -> bool {
488 kind == ProviderKind::Minimax
489 && (is_exact_https_route(base_url, "api.minimax.io", "v1")
490 || is_exact_https_route(base_url, "api.minimaxi.com", "v1"))
491}
492
493#[must_use]
500pub fn is_exact_minimax_anthropic_route(kind: ProviderKind, base_url: &str) -> bool {
501 kind == ProviderKind::MinimaxAnthropic
502 && (is_exact_https_route(base_url, "api.minimax.io", "anthropic")
503 || is_exact_https_route(base_url, "api.minimaxi.com", "anthropic"))
504}
505
506#[must_use]
512pub fn credential_help_for_route(kind: ProviderKind, base_url: &str) -> CredentialHelp {
513 if is_exact_kimi_code_route(kind, base_url) {
514 return CredentialHelp {
515 acquisition: CredentialAcquisition::ApiKey,
516 credential_url: Some(KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL),
517 docs_url: None,
518 guidance: "Create a Kimi Code membership-plan API key in the Kimi Code console. This route uses api.kimi.com/coding/v1; Codewhale does not import Kimi CLI credentials.",
519 };
520 }
521
522 credential_help(kind)
523}
524
525macro_rules! provider {
526 (
527 $struct_name:ident,
528 $kind:ident,
529 $id:literal,
530 $display_name:literal,
531 $base_url:ident,
532 $model:ident,
533 [$($env_var:literal),* $(,)?],
534 $config_key:literal,
535 aliases: [$($alias:literal),* $(,)?]
536 ) => {
537 pub struct $struct_name;
539
540 impl Provider for $struct_name {
541 fn id(&self) -> &'static str {
542 $id
543 }
544
545 fn kind(&self) -> ProviderKind {
546 ProviderKind::$kind
547 }
548
549 fn display_name(&self) -> &'static str {
550 $display_name
551 }
552
553 fn default_base_url(&self) -> &'static str {
554 $base_url
555 }
556
557 fn default_model(&self) -> &'static str {
558 $model
559 }
560
561 fn env_vars(&self) -> &'static [&'static str] {
562 &[$($env_var),*]
563 }
564
565 fn provider_config_key(&self) -> &'static str {
566 $config_key
567 }
568
569 fn aliases(&self) -> &'static [&'static str] {
570 &[$($alias),*]
571 }
572 }
573 };
574}
575
576pub struct Deepseek;
583
584impl Provider for Deepseek {
585 fn id(&self) -> &'static str {
586 "deepseek"
587 }
588
589 fn kind(&self) -> ProviderKind {
590 ProviderKind::Deepseek
591 }
592
593 fn display_name(&self) -> &'static str {
594 "DeepSeek"
595 }
596
597 fn default_base_url(&self) -> &'static str {
598 DEFAULT_DEEPSEEK_BASE_URL
599 }
600
601 fn default_model(&self) -> &'static str {
602 DEFAULT_DEEPSEEK_MODEL
603 }
604
605 fn env_vars(&self) -> &'static [&'static str] {
606 &["DEEPSEEK_API_KEY"]
607 }
608
609 fn provider_config_key(&self) -> &'static str {
610 "deepseek"
611 }
612
613 fn aliases(&self) -> &'static [&'static str] {
614 &[
615 "deep-seek",
616 "deepseek-cn",
617 "deepseek_china",
618 "deepseekcn",
619 "deepseek-china",
620 "deepseek-anthropic",
622 "deepseek_anthropic",
623 "deepseek-claude",
624 "deepseek_claude",
625 ]
626 }
627
628 fn wire_policy(&self) -> WirePolicy {
629 WirePolicy::ModelAware
630 }
631}
632
633pub struct DeepseekAnthropic;
637
638impl Provider for DeepseekAnthropic {
639 fn id(&self) -> &'static str {
640 "deepseek-anthropic"
641 }
642
643 fn kind(&self) -> ProviderKind {
644 ProviderKind::DeepseekAnthropic
645 }
646
647 fn display_name(&self) -> &'static str {
648 "DeepSeek"
650 }
651
652 fn default_base_url(&self) -> &'static str {
653 DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
654 }
655
656 fn default_model(&self) -> &'static str {
657 DEFAULT_DEEPSEEK_ANTHROPIC_MODEL
658 }
659
660 fn env_vars(&self) -> &'static [&'static str] {
661 &["DEEPSEEK_API_KEY"]
662 }
663
664 fn provider_config_key(&self) -> &'static str {
665 "deepseek_anthropic"
666 }
667
668 fn aliases(&self) -> &'static [&'static str] {
669 &[]
670 }
671
672 fn wire_policy(&self) -> WirePolicy {
673 WirePolicy::Fixed(WireFormat::AnthropicMessages)
674 }
675}
676provider!(
677 NvidiaNim,
678 NvidiaNim,
679 "nvidia-nim",
680 "NVIDIA NIM",
681 DEFAULT_NVIDIA_NIM_BASE_URL,
682 DEFAULT_NVIDIA_NIM_MODEL,
683 ["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
684 "nvidia_nim",
685 aliases: ["nvidia", "nvidia_nim", "nim"]
686);
687provider!(
688 Openai,
689 Openai,
690 "openai",
691 "OpenAI-compatible",
692 DEFAULT_OPENAI_BASE_URL,
693 DEFAULT_OPENAI_MODEL,
694 ["OPENAI_API_KEY"],
695 "openai",
696 aliases: ["open-ai"]
697);
698provider!(
699 Atlascloud,
700 Atlascloud,
701 "atlascloud",
702 "AtlasCloud",
703 DEFAULT_ATLASCLOUD_BASE_URL,
704 DEFAULT_ATLASCLOUD_MODEL,
705 ["ATLASCLOUD_API_KEY"],
706 "atlascloud",
707 aliases: ["atlas-cloud", "atlas_cloud", "atlas"]
708);
709provider!(
710 WanjieArk,
711 WanjieArk,
712 "wanjie-ark",
713 "Wanjie Ark",
714 DEFAULT_WANJIE_ARK_BASE_URL,
715 DEFAULT_WANJIE_ARK_MODEL,
716 [
717 "WANJIE_ARK_API_KEY",
718 "WANJIE_API_KEY",
719 "WANJIE_MAAS_API_KEY"
720 ],
721 "wanjie_ark",
722 aliases: ["wanjie", "wanjie_ark", "ark-wanjie", "ark_wanjie", "wanjieark", "wanjie-maas", "wanjie_maas", "wanjiemaas"]
723);
724provider!(
725 Volcengine,
726 Volcengine,
727 "volcengine",
728 "Volcengine Ark",
729 DEFAULT_VOLCENGINE_BASE_URL,
730 DEFAULT_VOLCENGINE_MODEL,
731 [
732 "VOLCENGINE_API_KEY",
733 "VOLCENGINE_ARK_API_KEY",
734 "ARK_API_KEY"
735 ],
736 "volcengine",
737 aliases: ["volcengine-ark", "volcengine_ark", "ark", "volc-ark", "volcengineark"]
738);
739provider!(
740 Openrouter,
741 Openrouter,
742 "openrouter",
743 "OpenRouter",
744 DEFAULT_OPENROUTER_BASE_URL,
745 DEFAULT_OPENROUTER_MODEL,
746 ["OPENROUTER_API_KEY"],
747 "openrouter",
748 aliases: ["open_router"]
749);
750provider!(
751 XiaomiMimo,
752 XiaomiMimo,
753 "xiaomi-mimo",
754 "Xiaomi MiMo",
755 DEFAULT_XIAOMI_MIMO_BASE_URL,
756 DEFAULT_XIAOMI_MIMO_MODEL,
757 [
758 "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
759 "MIMO_TOKEN_PLAN_API_KEY",
760 "XIAOMI_MIMO_API_KEY",
761 "XIAOMI_API_KEY",
762 "MIMO_API_KEY",
763 ],
764 "xiaomi_mimo",
765 aliases: ["xiaomi_mimo", "xiaomimimo", "mimo", "xiaomi"]
766);
767provider!(
768 Novita,
769 Novita,
770 "novita",
771 "Novita AI",
772 DEFAULT_NOVITA_BASE_URL,
773 DEFAULT_NOVITA_MODEL,
774 ["NOVITA_API_KEY"],
775 "novita",
776 aliases: ["novita-ai", "novita_ai"]
780);
781provider!(
782 Fireworks,
783 Fireworks,
784 "fireworks",
785 "Fireworks AI",
786 DEFAULT_FIREWORKS_BASE_URL,
787 DEFAULT_FIREWORKS_MODEL,
788 ["FIREWORKS_API_KEY"],
789 "fireworks",
790 aliases: ["fireworks-ai"]
791);
792provider!(
793 Siliconflow,
794 Siliconflow,
795 "siliconflow",
796 "SiliconFlow",
797 DEFAULT_SILICONFLOW_BASE_URL,
798 DEFAULT_SILICONFLOW_MODEL,
799 ["SILICONFLOW_API_KEY"],
800 "siliconflow",
801 aliases: ["silicon-flow", "silicon_flow"]
802);
803provider!(
804 SiliconflowCN,
805 SiliconflowCN,
806 "siliconflow-CN",
807 "SiliconFlow (China)",
808 DEFAULT_SILICONFLOW_CN_BASE_URL,
809 DEFAULT_SILICONFLOW_MODEL,
810 ["SILICONFLOW_API_KEY"],
811 "siliconflow_cn",
812 aliases: [
813 "silicon-flow-cn",
814 "silicon-flow-CN",
815 "silicon_flow_cn",
816 "silicon_flow_CN",
817 "siliconflow-china",
818 ]
819);
820provider!(
821 Arcee,
822 Arcee,
823 "arcee",
824 "Arcee AI",
825 DEFAULT_ARCEE_BASE_URL,
826 DEFAULT_ARCEE_MODEL,
827 ["ARCEE_API_KEY"],
828 "arcee",
829 aliases: ["arcee-ai", "arcee_ai"]
830);
831provider!(
832 Moonshot,
833 Moonshot,
834 "moonshot",
835 "Moonshot/Kimi",
836 DEFAULT_MOONSHOT_BASE_URL,
837 DEFAULT_MOONSHOT_MODEL,
838 ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
839 "moonshot",
840 aliases: ["moonshot-ai", "moonshotai", "moonshot_ai", "kimi", "kimi-k2"]
844);
845provider!(
846 Sglang,
847 Sglang,
848 "sglang",
849 "SGLang",
850 DEFAULT_SGLANG_BASE_URL,
851 DEFAULT_SGLANG_MODEL,
852 ["SGLANG_API_KEY"],
853 "sglang",
854 aliases: ["sg-lang"]
855);
856provider!(
857 Vllm,
858 Vllm,
859 "vllm",
860 "vLLM",
861 DEFAULT_VLLM_BASE_URL,
862 DEFAULT_VLLM_MODEL,
863 ["VLLM_API_KEY"],
864 "vllm",
865 aliases: ["v-llm"]
866);
867provider!(
868 Ollama,
869 Ollama,
870 "ollama",
871 "Ollama",
872 DEFAULT_OLLAMA_BASE_URL,
873 DEFAULT_OLLAMA_MODEL,
874 ["OLLAMA_API_KEY"],
875 "ollama",
876 aliases: ["ollama-local"]
877);
878provider!(
879 Huggingface,
880 Huggingface,
881 "huggingface",
882 "Hugging Face",
883 DEFAULT_HUGGINGFACE_BASE_URL,
884 DEFAULT_HUGGINGFACE_MODEL,
885 ["HUGGINGFACE_API_KEY", "HF_TOKEN"],
886 "huggingface",
887 aliases: ["hugging-face", "hugging_face", "hf"]
888);
889provider!(
890 Together,
891 Together,
892 "together",
893 "Together AI",
894 DEFAULT_TOGETHER_BASE_URL,
895 DEFAULT_TOGETHER_MODEL,
896 ["TOGETHER_API_KEY"],
897 "together",
898 aliases: ["together-ai", "together_ai", "togetherai"]
903);
904provider!(
905 Qianfan,
906 Qianfan,
907 "qianfan",
908 "Baidu Qianfan",
909 DEFAULT_QIANFAN_BASE_URL,
910 DEFAULT_QIANFAN_MODEL,
911 ["QIANFAN_API_KEY", "BAIDU_QIANFAN_API_KEY"],
912 "qianfan",
913 aliases: ["baidu-qianfan", "baidu_qianfan", "baidu"]
914);
915provider!(
916 Mistral,
917 Mistral,
918 "mistral",
919 "Mistral AI",
920 DEFAULT_MISTRAL_BASE_URL,
921 DEFAULT_MISTRAL_MODEL,
922 ["MISTRAL_API_KEY"],
923 "mistral",
924 aliases: ["mistral-ai", "mistral_ai", "mistralai", "la-plateforme", "la_plateforme"]
925);
926
927pub struct OpenaiCodex;
929
930impl Provider for OpenaiCodex {
931 fn id(&self) -> &'static str {
932 "openai-codex"
933 }
934
935 fn kind(&self) -> ProviderKind {
936 ProviderKind::OpenaiCodex
937 }
938
939 fn display_name(&self) -> &'static str {
940 "OpenAI Codex (ChatGPT)"
941 }
942
943 fn default_base_url(&self) -> &'static str {
944 DEFAULT_OPENAI_CODEX_BASE_URL
945 }
946
947 fn default_model(&self) -> &'static str {
948 DEFAULT_OPENAI_CODEX_MODEL
949 }
950
951 fn env_vars(&self) -> &'static [&'static str] {
952 &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"]
953 }
954
955 fn provider_config_key(&self) -> &'static str {
956 "openai_codex"
957 }
958
959 fn aliases(&self) -> &'static [&'static str] {
960 &[
961 "openai_codex",
962 "openaicodex",
963 "codex",
964 "chatgpt",
965 "chatgpt-codex",
966 "chatgpt_codex",
967 "chatgptcodex",
968 ]
969 }
970
971 fn wire_policy(&self) -> WirePolicy {
972 WirePolicy::Fixed(WireFormat::Responses)
973 }
974}
975
976pub struct Anthropic;
978
979impl Provider for Anthropic {
980 fn id(&self) -> &'static str {
981 "anthropic"
982 }
983
984 fn kind(&self) -> ProviderKind {
985 ProviderKind::Anthropic
986 }
987
988 fn display_name(&self) -> &'static str {
989 "Anthropic"
990 }
991
992 fn default_base_url(&self) -> &'static str {
993 crate::DEFAULT_ANTHROPIC_BASE_URL
994 }
995
996 fn default_model(&self) -> &'static str {
997 crate::DEFAULT_ANTHROPIC_MODEL
998 }
999
1000 fn env_vars(&self) -> &'static [&'static str] {
1001 &["ANTHROPIC_API_KEY"]
1002 }
1003
1004 fn provider_config_key(&self) -> &'static str {
1005 "anthropic"
1006 }
1007
1008 fn wire_policy(&self) -> WirePolicy {
1009 WirePolicy::Fixed(WireFormat::AnthropicMessages)
1010 }
1011}
1012
1013pub struct Openmodel;
1015
1016impl Provider for Openmodel {
1017 fn id(&self) -> &'static str {
1018 "openmodel"
1019 }
1020
1021 fn kind(&self) -> ProviderKind {
1022 ProviderKind::Openmodel
1023 }
1024
1025 fn display_name(&self) -> &'static str {
1026 "OpenModel"
1027 }
1028
1029 fn default_base_url(&self) -> &'static str {
1030 DEFAULT_OPENMODEL_BASE_URL
1031 }
1032
1033 fn default_model(&self) -> &'static str {
1034 DEFAULT_OPENMODEL_MODEL
1035 }
1036
1037 fn env_vars(&self) -> &'static [&'static str] {
1038 &["OPENMODEL_API_KEY"]
1039 }
1040
1041 fn provider_config_key(&self) -> &'static str {
1042 "openmodel"
1043 }
1044
1045 fn aliases(&self) -> &'static [&'static str] {
1046 &["open-model", "open_model"]
1047 }
1048
1049 fn wire_policy(&self) -> WirePolicy {
1050 WirePolicy::Fixed(WireFormat::AnthropicMessages)
1051 }
1052}
1053
1054provider!(
1055 Zai,
1056 Zai,
1057 "zai",
1058 "Zhipu AI / Z.ai",
1059 DEFAULT_ZAI_BASE_URL,
1060 DEFAULT_ZAI_MODEL,
1061 ["ZAI_API_KEY", "Z_AI_API_KEY", "ZHIPU_API_KEY", "GLM_API_KEY"],
1062 "zai",
1063 aliases: ["z-ai", "z_ai", "z.ai", "zhipu", "zhipuai", "bigmodel", "big-model"]
1064);
1065
1066provider!(
1067 Stepfun,
1068 Stepfun,
1069 "stepfun",
1070 "StepFun / StepFlash",
1071 DEFAULT_STEPFUN_BASE_URL,
1072 DEFAULT_STEPFUN_MODEL,
1073 ["STEPFUN_API_KEY", "STEP_API_KEY"],
1074 "stepfun",
1075 aliases: ["step-fun", "step_fun", "stepflash", "step-flash", "step_flash"]
1076);
1077
1078provider!(
1079 Minimax,
1080 Minimax,
1081 "minimax",
1082 "MiniMax",
1083 DEFAULT_MINIMAX_BASE_URL,
1084 DEFAULT_MINIMAX_MODEL,
1085 ["MINIMAX_API_KEY"],
1086 "minimax",
1087 aliases: ["mini-max", "mini_max", "minimax-anthropic", "minimax_anthropic", "mini-max-anthropic", "mini_max_anthropic"]
1089);
1090
1091pub struct MinimaxAnthropic;
1093
1094impl Provider for MinimaxAnthropic {
1095 fn id(&self) -> &'static str {
1096 "minimax-anthropic"
1097 }
1098
1099 fn kind(&self) -> ProviderKind {
1100 ProviderKind::MinimaxAnthropic
1101 }
1102
1103 fn display_name(&self) -> &'static str {
1104 "MiniMax"
1106 }
1107
1108 fn default_base_url(&self) -> &'static str {
1109 DEFAULT_MINIMAX_ANTHROPIC_BASE_URL
1110 }
1111
1112 fn default_model(&self) -> &'static str {
1113 DEFAULT_MINIMAX_MODEL
1114 }
1115
1116 fn env_vars(&self) -> &'static [&'static str] {
1117 &["MINIMAX_API_KEY"]
1118 }
1119
1120 fn provider_config_key(&self) -> &'static str {
1121 "minimax_anthropic"
1122 }
1123
1124 fn aliases(&self) -> &'static [&'static str] {
1125 &[]
1126 }
1127
1128 fn wire_policy(&self) -> WirePolicy {
1129 WirePolicy::Fixed(WireFormat::AnthropicMessages)
1130 }
1131}
1132
1133provider!(
1134 Deepinfra,
1135 Deepinfra,
1136 "deepinfra",
1137 "DeepInfra",
1138 DEFAULT_DEEPINFRA_BASE_URL,
1139 DEFAULT_DEEPINFRA_MODEL,
1140 ["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
1141 "deepinfra",
1142 aliases: ["deep-infra", "deep_infra"]
1143);
1144
1145provider!(
1146 Sakana,
1147 Sakana,
1148 "sakana",
1149 "Sakana AI (Fugu)",
1150 DEFAULT_SAKANA_BASE_URL,
1151 DEFAULT_SAKANA_MODEL,
1152 ["FUGU_API_KEY", "SAKANA_API_KEY"],
1153 "sakana",
1154 aliases: ["sakana-ai", "sakana_ai", "fugu"]
1155);
1156
1157provider!(
1158 LongCat,
1159 LongCat,
1160 "longcat",
1161 "Meituan LongCat",
1162 DEFAULT_LONGCAT_BASE_URL,
1163 DEFAULT_LONGCAT_MODEL,
1164 ["LONGCAT_API_KEY"],
1165 "longcat",
1166 aliases: ["long-cat", "meituan-longcat", "meituan"]
1167);
1168
1169provider!(
1170 OpencodeGo,
1171 OpencodeGo,
1172 "opencode-go",
1173 "OpenCode Go",
1174 DEFAULT_OPENCODE_GO_BASE_URL,
1175 DEFAULT_OPENCODE_GO_MODEL,
1176 ["OPENCODE_GO_API_KEY"],
1177 "opencode_go",
1178 aliases: ["opencode_go", "opencodego"]
1179);
1180
1181pub struct OpencodeZen;
1183
1184impl Provider for OpencodeZen {
1185 fn id(&self) -> &'static str {
1186 "opencode-zen"
1187 }
1188
1189 fn kind(&self) -> ProviderKind {
1190 ProviderKind::OpencodeZen
1191 }
1192
1193 fn display_name(&self) -> &'static str {
1194 "OpenCode Zen"
1195 }
1196
1197 fn default_base_url(&self) -> &'static str {
1198 DEFAULT_OPENCODE_ZEN_BASE_URL
1199 }
1200
1201 fn default_model(&self) -> &'static str {
1202 DEFAULT_OPENCODE_ZEN_MODEL
1203 }
1204
1205 fn env_vars(&self) -> &'static [&'static str] {
1206 &["OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"]
1207 }
1208
1209 fn provider_config_key(&self) -> &'static str {
1210 "opencode_zen"
1211 }
1212
1213 fn aliases(&self) -> &'static [&'static str] {
1214 &["opencode_zen", "opencodezen", "zen", "opencode"]
1215 }
1216
1217 fn wire_policy(&self) -> WirePolicy {
1218 WirePolicy::ModelAware
1219 }
1220}
1221
1222provider!(
1223 Meta,
1224 Meta,
1225 "meta",
1226 "Meta Model API",
1227 DEFAULT_META_BASE_URL,
1228 DEFAULT_META_MODEL,
1229 ["META_MODEL_API_KEY", "MODEL_API_KEY"],
1230 "meta",
1231 aliases: [
1232 "meta-ai",
1233 "meta_ai",
1234 "meta-model-api",
1235 "meta_model_api",
1236 "muse",
1237 "muse-spark"
1238 ]
1239);
1240
1241provider!(
1242 Xai,
1243 Xai,
1244 "xai",
1245 "xAI",
1246 DEFAULT_XAI_BASE_URL,
1247 DEFAULT_XAI_MODEL,
1248 ["XAI_API_KEY"],
1249 "xai",
1250 aliases: ["x-ai", "x_ai", "grok"]
1251);
1252
1253provider!(
1254 Telecomjs,
1255 Telecomjs,
1256 "telecomjs",
1257 "TelecomJS TokenHub",
1258 DEFAULT_TELECOMJS_BASE_URL,
1259 DEFAULT_TELECOMJS_MODEL,
1260 ["TELECOMJS_API_KEY"],
1261 "telecomjs",
1262 aliases: ["telecom-js", "telecom_js", "telecomjs-cn", "tokenhub"]
1263);
1264
1265pub struct ModelstudioTokenPlan;
1271
1272impl Provider for ModelstudioTokenPlan {
1273 fn id(&self) -> &'static str {
1274 "modelstudio-token-plan"
1275 }
1276
1277 fn kind(&self) -> ProviderKind {
1278 ProviderKind::ModelstudioTokenPlan
1279 }
1280
1281 fn display_name(&self) -> &'static str {
1282 "Alibaba Cloud Model Studio"
1286 }
1287
1288 fn default_base_url(&self) -> &'static str {
1289 DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL
1290 }
1291
1292 fn default_model(&self) -> &'static str {
1293 DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL
1294 }
1295
1296 fn env_vars(&self) -> &'static [&'static str] {
1297 &["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"]
1298 }
1299
1300 fn provider_config_key(&self) -> &'static str {
1301 "modelstudio_token_plan"
1302 }
1303
1304 fn aliases(&self) -> &'static [&'static str] {
1305 &[
1308 "modelstudio-token-plan",
1309 "modelstudio_token_plan",
1310 "modelstudio",
1311 "alibaba-token-plan",
1312 "dashscope-token-plan",
1313 "alibaba",
1314 "dashscope",
1315 "modelstudio-coding-plan",
1318 "modelstudio_coding_plan",
1319 "alibaba-coding-plan",
1320 "dashscope-coding-plan",
1321 "modelstudio-token-plan-anthropic",
1322 "modelstudio_token_plan_anthropic",
1323 "alibaba-token-plan-anthropic",
1324 "modelstudio-coding-plan-anthropic",
1325 "modelstudio_coding_plan_anthropic",
1326 "alibaba-coding-plan-anthropic",
1327 ]
1328 }
1329}
1330
1331pub struct ModelstudioTokenPlanAnthropic;
1336
1337impl Provider for ModelstudioTokenPlanAnthropic {
1338 fn id(&self) -> &'static str {
1339 "modelstudio-token-plan-anthropic"
1340 }
1341
1342 fn kind(&self) -> ProviderKind {
1343 ProviderKind::ModelstudioTokenPlanAnthropic
1344 }
1345
1346 fn display_name(&self) -> &'static str {
1347 "Alibaba Cloud Model Studio"
1348 }
1349
1350 fn default_base_url(&self) -> &'static str {
1351 MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL
1352 }
1353
1354 fn default_model(&self) -> &'static str {
1355 DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL
1356 }
1357
1358 fn env_vars(&self) -> &'static [&'static str] {
1359 &["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"]
1360 }
1361
1362 fn provider_config_key(&self) -> &'static str {
1363 "modelstudio_token_plan_anthropic"
1364 }
1365
1366 fn aliases(&self) -> &'static [&'static str] {
1367 &[]
1369 }
1370
1371 fn wire_policy(&self) -> WirePolicy {
1372 WirePolicy::Fixed(WireFormat::AnthropicMessages)
1373 }
1374}
1375
1376pub struct ModelstudioCodingPlan;
1380
1381impl Provider for ModelstudioCodingPlan {
1382 fn id(&self) -> &'static str {
1383 "modelstudio-coding-plan"
1384 }
1385
1386 fn kind(&self) -> ProviderKind {
1387 ProviderKind::ModelstudioCodingPlan
1388 }
1389
1390 fn display_name(&self) -> &'static str {
1391 "Alibaba Cloud Model Studio"
1392 }
1393
1394 fn default_base_url(&self) -> &'static str {
1395 DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL
1396 }
1397
1398 fn default_model(&self) -> &'static str {
1399 DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL
1400 }
1401
1402 fn env_vars(&self) -> &'static [&'static str] {
1403 &["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"]
1404 }
1405
1406 fn provider_config_key(&self) -> &'static str {
1407 "modelstudio_coding_plan"
1408 }
1409
1410 fn aliases(&self) -> &'static [&'static str] {
1411 &[]
1412 }
1413}
1414
1415pub struct ModelstudioCodingPlanAnthropic;
1417
1418impl Provider for ModelstudioCodingPlanAnthropic {
1419 fn id(&self) -> &'static str {
1420 "modelstudio-coding-plan-anthropic"
1421 }
1422
1423 fn kind(&self) -> ProviderKind {
1424 ProviderKind::ModelstudioCodingPlanAnthropic
1425 }
1426
1427 fn display_name(&self) -> &'static str {
1428 "Alibaba Cloud Model Studio"
1429 }
1430
1431 fn default_base_url(&self) -> &'static str {
1432 MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL
1433 }
1434
1435 fn default_model(&self) -> &'static str {
1436 DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL
1437 }
1438
1439 fn env_vars(&self) -> &'static [&'static str] {
1440 &["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"]
1441 }
1442
1443 fn provider_config_key(&self) -> &'static str {
1444 "modelstudio_coding_plan_anthropic"
1445 }
1446
1447 fn aliases(&self) -> &'static [&'static str] {
1448 &[]
1449 }
1450
1451 fn wire_policy(&self) -> WirePolicy {
1452 WirePolicy::Fixed(WireFormat::AnthropicMessages)
1453 }
1454}
1455
1456pub struct Custom;
1467
1468impl Provider for Custom {
1469 fn id(&self) -> &'static str {
1470 "custom"
1471 }
1472
1473 fn kind(&self) -> ProviderKind {
1474 ProviderKind::Custom
1475 }
1476
1477 fn display_name(&self) -> &'static str {
1478 "Custom (OpenAI-compatible)"
1479 }
1480
1481 fn default_base_url(&self) -> &'static str {
1482 "http://localhost/v1"
1486 }
1487
1488 fn default_model(&self) -> &'static str {
1489 "custom-model"
1492 }
1493
1494 fn env_vars(&self) -> &'static [&'static str] {
1495 &[]
1498 }
1499
1500 fn provider_config_key(&self) -> &'static str {
1501 "custom"
1502 }
1503
1504 fn wire_policy(&self) -> WirePolicy {
1505 WirePolicy::Fixed(WireFormat::ChatCompletions)
1506 }
1507}
1508
1509static DEEPSEEK: Deepseek = Deepseek;
1510static DEEPSEEK_ANTHROPIC: DeepseekAnthropic = DeepseekAnthropic;
1511static NVIDIA_NIM: NvidiaNim = NvidiaNim;
1512static OPENAI: Openai = Openai;
1513static ATLASCLOUD: Atlascloud = Atlascloud;
1514static WANJIE_ARK: WanjieArk = WanjieArk;
1515static VOLCENGINE: Volcengine = Volcengine;
1516static OPENROUTER: Openrouter = Openrouter;
1517static XIAOMI_MIMO: XiaomiMimo = XiaomiMimo;
1518static NOVITA: Novita = Novita;
1519static FIREWORKS: Fireworks = Fireworks;
1520static SILICONFLOW: Siliconflow = Siliconflow;
1521static SILICONFLOW_CN: SiliconflowCN = SiliconflowCN;
1522static ARCEE: Arcee = Arcee;
1523static MOONSHOT: Moonshot = Moonshot;
1524static SGLANG: Sglang = Sglang;
1525static VLLM: Vllm = Vllm;
1526static OLLAMA: Ollama = Ollama;
1527static HUGGINGFACE: Huggingface = Huggingface;
1528static TOGETHER: Together = Together;
1529static QIANFAN: Qianfan = Qianfan;
1530static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
1531static ANTHROPIC: Anthropic = Anthropic;
1532static OPENMODEL: Openmodel = Openmodel;
1533static ZAI: Zai = Zai;
1534static STEPFUN: Stepfun = Stepfun;
1535static MINIMAX: Minimax = Minimax;
1536static MINIMAX_ANTHROPIC: MinimaxAnthropic = MinimaxAnthropic;
1537static DEEPINFRA: Deepinfra = Deepinfra;
1538static SAKANA: Sakana = Sakana;
1539static LONGCAT: LongCat = LongCat;
1540static OPENCODE_GO: OpencodeGo = OpencodeGo;
1541static OPENCODE_ZEN: OpencodeZen = OpencodeZen;
1542static META: Meta = Meta;
1543static XAI: Xai = Xai;
1544static MISTRAL: Mistral = Mistral;
1545static TELECOMJS: Telecomjs = Telecomjs;
1546static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
1547static MODELSTUDIO_TOKEN_PLAN_ANTHROPIC: ModelstudioTokenPlanAnthropic =
1548 ModelstudioTokenPlanAnthropic;
1549static MODELSTUDIO_CODING_PLAN: ModelstudioCodingPlan = ModelstudioCodingPlan;
1550static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
1551 ModelstudioCodingPlanAnthropic;
1552static CUSTOM: Custom = Custom;
1553
1554static PROVIDER_REGISTRY: [&dyn Provider; 42] = [
1555 &DEEPSEEK,
1556 &DEEPSEEK_ANTHROPIC,
1557 &NVIDIA_NIM,
1558 &OPENAI,
1559 &ATLASCLOUD,
1560 &WANJIE_ARK,
1561 &VOLCENGINE,
1562 &OPENROUTER,
1563 &XIAOMI_MIMO,
1564 &NOVITA,
1565 &FIREWORKS,
1566 &SILICONFLOW,
1567 &ARCEE,
1568 &SILICONFLOW_CN,
1569 &MOONSHOT,
1570 &SGLANG,
1571 &VLLM,
1572 &OLLAMA,
1573 &HUGGINGFACE,
1574 &TOGETHER,
1575 &QIANFAN,
1576 &OPENAI_CODEX,
1577 &ANTHROPIC,
1578 &OPENMODEL,
1579 &ZAI,
1580 &STEPFUN,
1581 &MINIMAX,
1582 &MINIMAX_ANTHROPIC,
1583 &DEEPINFRA,
1584 &SAKANA,
1585 &LONGCAT,
1586 &OPENCODE_GO,
1587 &OPENCODE_ZEN,
1588 &META,
1589 &XAI,
1590 &MISTRAL,
1591 &TELECOMJS,
1592 &MODELSTUDIO_TOKEN_PLAN,
1593 &MODELSTUDIO_TOKEN_PLAN_ANTHROPIC,
1594 &MODELSTUDIO_CODING_PLAN,
1595 &MODELSTUDIO_CODING_PLAN_ANTHROPIC,
1596 &CUSTOM,
1597];
1598
1599#[must_use]
1605pub fn all_providers() -> &'static [&'static dyn Provider] {
1606 &PROVIDER_REGISTRY
1607}
1608
1609#[must_use]
1625pub fn providers_sorted_for_display() -> Vec<&'static dyn Provider> {
1626 let mut providers = all_providers().to_vec();
1627 providers.sort_by(|a, b| {
1628 a.display_name()
1629 .to_ascii_lowercase()
1630 .cmp(&b.display_name().to_ascii_lowercase())
1631 });
1632 providers
1633}
1634
1635#[must_use]
1637pub fn lookup_provider(id: &str) -> Option<&'static dyn Provider> {
1638 let id = id.trim();
1639 all_providers()
1640 .iter()
1641 .copied()
1642 .find(|provider| provider.id() == id)
1643}
1644
1645#[must_use]
1647pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
1648 ProviderKind::parse(id_or_alias).map(provider_for_kind)
1649}
1650
1651#[must_use]
1653pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
1654 PROVIDER_REGISTRY
1655 .iter()
1656 .find(|p| p.kind() == kind)
1657 .copied()
1658 .expect("ProviderKind variant missing from PROVIDER_REGISTRY")
1659}
1660
1661#[cfg(test)]
1662mod tests {
1663 use super::*;
1664
1665 #[test]
1666 fn credential_help_covers_every_provider_without_guessing_non_key_urls() {
1667 for provider in all_providers() {
1668 let help = provider.credential_help();
1669 assert!(
1670 !help.guidance.trim().is_empty(),
1671 "{} credential guidance must not be empty",
1672 provider.id()
1673 );
1674
1675 match help.acquisition {
1676 CredentialAcquisition::ApiKey | CredentialAcquisition::ApiKeyOrOAuth => {
1677 assert!(
1678 help.credential_url.is_some(),
1679 "{} needs a stable provider-owned credential link",
1680 provider.id()
1681 );
1682 }
1683 CredentialAcquisition::LocalOptional
1684 | CredentialAcquisition::OAuth
1685 | CredentialAcquisition::Configuration => assert!(
1686 help.credential_url.is_none(),
1687 "{} must explain its non-key route instead of inventing a credential link",
1688 provider.id()
1689 ),
1690 }
1691 }
1692 }
1693
1694 #[test]
1695 fn kimi_credential_help_uses_the_durable_api_key_console_only() {
1696 let help = provider_for_kind(ProviderKind::Moonshot).credential_help();
1697
1698 assert_eq!(help.acquisition, CredentialAcquisition::ApiKey);
1699 assert_eq!(
1700 help.credential_url,
1701 Some("https://platform.kimi.ai/console/api-keys")
1702 );
1703 assert_eq!(
1704 help.docs_url,
1705 Some("https://platform.kimi.ai/docs/overview")
1706 );
1707 assert!(help.guidance.contains("create and copy an API key"));
1708 assert!(help.guidance.contains("OAuth is not available"));
1709 }
1710
1711 #[test]
1712 fn kimi_code_route_credential_help_is_distinct_from_direct_moonshot() {
1713 let direct = credential_help_for_route(ProviderKind::Moonshot, DEFAULT_MOONSHOT_BASE_URL);
1714 let kimi_code =
1715 credential_help_for_route(ProviderKind::Moonshot, "https://api.kimi.com/coding/v1/");
1716
1717 assert_eq!(
1718 direct.credential_url,
1719 Some("https://platform.kimi.ai/console/api-keys")
1720 );
1721 assert_eq!(
1722 kimi_code.credential_url,
1723 Some(KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL)
1724 );
1725 assert_eq!(kimi_code.docs_url, None);
1726 assert!(kimi_code.guidance.contains("membership-plan API key"));
1727 assert!(
1728 kimi_code
1729 .guidance
1730 .contains("does not import Kimi CLI credentials")
1731 );
1732 assert!(!is_exact_kimi_code_route(
1733 ProviderKind::Moonshot,
1734 "https://api.kimi.com/coding/v1/preview"
1735 ));
1736
1737 assert!(is_exact_kimi_code_route(
1740 ProviderKind::Moonshot,
1741 "HTTPS://API.KIMI.COM/coding/v1/"
1742 ));
1743 for neighboring_route in [
1744 "https://api.kimi.com/CODING/v1",
1745 "https://api.kimi.com/coding/V1",
1746 "http://api.kimi.com/coding/v1",
1747 "https://api.kimi.com:443/coding/v1",
1748 "https://api.kimi.com/coding/v1?preview=1",
1749 "https://api.kimi.com/coding/v1#fragment",
1750 "https://api.kimi.com/coding/v1//",
1751 ] {
1752 assert!(
1753 !is_exact_kimi_code_route(ProviderKind::Moonshot, neighboring_route),
1754 "{neighboring_route} must not inherit Kimi Code membership semantics"
1755 );
1756 }
1757 }
1758
1759 #[test]
1760 fn direct_moonshot_route_matching_is_exact() {
1761 assert!(is_exact_moonshot_platform_route(
1762 ProviderKind::Moonshot,
1763 "HTTPS://API.MOONSHOT.AI/v1/"
1764 ));
1765 for neighboring_route in [
1766 "https://api.moonshot.ai/V1",
1767 "http://api.moonshot.ai/v1",
1768 "https://api.moonshot.ai:443/v1",
1769 "https://api.moonshot.ai/v1?preview=1",
1770 "https://api.moonshot.ai/v1#fragment",
1771 "https://api.moonshot.ai/v1//",
1772 "https://api.moonshot.ai/v1/chat/completions",
1773 "https://api.kimi.com/coding/v1",
1774 ] {
1775 assert!(
1776 !is_exact_moonshot_platform_route(ProviderKind::Moonshot, neighboring_route),
1777 "{neighboring_route} must not inherit direct Moonshot semantics"
1778 );
1779 }
1780 assert!(!is_exact_moonshot_platform_route(
1781 ProviderKind::Openai,
1782 DEFAULT_MOONSHOT_BASE_URL
1783 ));
1784 }
1785
1786 #[test]
1787 fn zai_chat_route_matching_is_exact() {
1788 for route in [
1789 "https://api.z.ai/api/coding/paas/v4",
1790 "https://api.z.ai/api/paas/v4/",
1791 "HTTPS://API.Z.AI/api/paas/v4",
1792 ] {
1793 assert!(is_exact_zai_chat_route(ProviderKind::Zai, route), "{route}");
1794 }
1795 for neighboring_route in [
1796 "http://api.z.ai/api/paas/v4",
1797 "https://api.z.ai:443/api/paas/v4",
1798 "https://api.z.ai/API/paas/v4",
1799 "https://api.z.ai/api/paas/v4?preview=1",
1800 "https://api.z.ai/api/paas/v4#fragment",
1801 "https://api.z.ai/api/paas/v4//",
1802 "https://api.z.ai/api/paas/v4/chat/completions",
1803 "https://gateway.example/v1",
1804 ] {
1805 assert!(
1806 !is_exact_zai_chat_route(ProviderKind::Zai, neighboring_route),
1807 "{neighboring_route} must not inherit Z.ai-only request fields"
1808 );
1809 }
1810 assert!(!is_exact_zai_chat_route(
1811 ProviderKind::Openai,
1812 DEFAULT_ZAI_BASE_URL
1813 ));
1814 }
1815
1816 #[test]
1817 fn minimax_chat_route_matching_is_exact_and_excludes_messages() {
1818 for route in [
1819 "https://api.minimax.io/v1",
1820 "https://api.minimaxi.com/v1/",
1821 "HTTPS://API.MINIMAX.IO/v1",
1822 ] {
1823 assert!(
1824 is_exact_minimax_chat_route(ProviderKind::Minimax, route),
1825 "{route}"
1826 );
1827 }
1828 for neighboring_route in [
1829 "http://api.minimax.io/v1",
1830 "https://api.minimax.io:443/v1",
1831 "https://api.minimax.io/V1",
1832 "https://api.minimax.io/v1?preview=1",
1833 "https://api.minimax.io/v1#fragment",
1834 "https://api.minimax.io/v1//",
1835 "https://api.minimax.io/v1/chat/completions",
1836 "https://api.minimax.io/anthropic",
1837 "https://api.minimaxi.com/anthropic",
1838 "https://gateway.example/v1",
1839 ] {
1840 assert!(
1841 !is_exact_minimax_chat_route(ProviderKind::Minimax, neighboring_route),
1842 "{neighboring_route} must not inherit MiniMax Chat request fields"
1843 );
1844 }
1845 assert!(!is_exact_minimax_chat_route(
1846 ProviderKind::MinimaxAnthropic,
1847 DEFAULT_MINIMAX_BASE_URL
1848 ));
1849 }
1850
1851 #[test]
1852 fn minimax_anthropic_route_matching_is_exact_and_excludes_chat() {
1853 for route in [
1854 "https://api.minimax.io/anthropic",
1855 "https://api.minimaxi.com/anthropic/",
1856 "HTTPS://API.MINIMAX.IO/anthropic",
1857 ] {
1858 assert!(
1859 is_exact_minimax_anthropic_route(ProviderKind::MinimaxAnthropic, route),
1860 "{route}"
1861 );
1862 }
1863 for neighboring_route in [
1864 "http://api.minimax.io/anthropic",
1865 "https://api.minimax.io:443/anthropic",
1866 "https://api.minimax.io/Anthropic",
1867 "https://api.minimax.io/anthropic?preview=1",
1868 "https://api.minimax.io/anthropic#fragment",
1869 "https://api.minimax.io/anthropic//",
1870 "https://api.minimax.io/anthropic/v1/messages",
1871 "https://api.minimax.io/v1",
1872 "https://gateway.example/anthropic",
1873 ] {
1874 assert!(
1875 !is_exact_minimax_anthropic_route(
1876 ProviderKind::MinimaxAnthropic,
1877 neighboring_route
1878 ),
1879 "{neighboring_route} must not inherit MiniMax Messages semantics"
1880 );
1881 }
1882 assert!(!is_exact_minimax_anthropic_route(
1883 ProviderKind::Minimax,
1884 DEFAULT_MINIMAX_ANTHROPIC_BASE_URL
1885 ));
1886 }
1887
1888 #[test]
1889 fn non_key_and_mixed_routes_are_typed_explicitly() {
1890 for kind in [
1891 ProviderKind::Sglang,
1892 ProviderKind::Vllm,
1893 ProviderKind::Ollama,
1894 ] {
1895 assert_eq!(
1896 provider_for_kind(kind).credential_help().acquisition,
1897 CredentialAcquisition::LocalOptional
1898 );
1899 }
1900 assert_eq!(
1901 provider_for_kind(ProviderKind::OpenaiCodex)
1902 .credential_help()
1903 .acquisition,
1904 CredentialAcquisition::OAuth
1905 );
1906 assert_eq!(
1907 provider_for_kind(ProviderKind::Xai)
1908 .credential_help()
1909 .acquisition,
1910 CredentialAcquisition::ApiKeyOrOAuth
1911 );
1912 assert_eq!(
1913 provider_for_kind(ProviderKind::Custom)
1914 .credential_help()
1915 .acquisition,
1916 CredentialAcquisition::Configuration
1917 );
1918 }
1919
1920 #[test]
1921 fn live_verified_console_replacements_do_not_regress_to_404_links() {
1922 let openmodel = provider_for_kind(ProviderKind::Openmodel).credential_help();
1923 assert_eq!(
1924 openmodel.credential_url,
1925 Some("https://console.openmodel.ai/")
1926 );
1927 assert_eq!(
1928 openmodel.docs_url,
1929 Some("https://docs.openmodel.ai/en/docs/getting-started/authentication")
1930 );
1931
1932 let sakana = provider_for_kind(ProviderKind::Sakana).credential_help();
1933 assert_eq!(
1934 sakana.credential_url,
1935 Some("https://console.sakana.ai/api-keys")
1936 );
1937 assert_eq!(
1938 sakana.docs_url,
1939 Some("https://console.sakana.ai/get-started")
1940 );
1941 }
1942
1943 #[test]
1944 fn model_aware_wire_policy_resolves_only_supported_endpoint_keys() {
1945 let policy = WirePolicy::ModelAware;
1946 assert_eq!(policy.resolve("chat"), Some(WireFormat::ChatCompletions));
1947 assert_eq!(policy.resolve("responses"), Some(WireFormat::Responses));
1948 assert_eq!(
1949 policy.resolve("messages"),
1950 Some(WireFormat::AnthropicMessages)
1951 );
1952 assert_eq!(policy.resolve("models/gemini-3.1-pro"), None);
1953 assert_eq!(policy.resolve(""), None);
1954 }
1955
1956 #[test]
1957 fn fixed_wire_policy_ignores_catalog_endpoint_keys() {
1958 let policy = WirePolicy::Fixed(WireFormat::Responses);
1959 assert_eq!(policy.resolve("chat"), Some(WireFormat::Responses));
1960 assert_eq!(policy.resolve("unknown"), Some(WireFormat::Responses));
1961 }
1962
1963 #[test]
1964 fn display_order_is_alphabetical_by_display_name() {
1965 let display = providers_sorted_for_display();
1966 let names: Vec<String> = display
1967 .iter()
1968 .map(|p| p.display_name().to_ascii_lowercase())
1969 .collect();
1970 let mut sorted = names.clone();
1971 sorted.sort();
1972 assert_eq!(
1973 names, sorted,
1974 "providers_sorted_for_display must be alphabetical (case-insensitive) by display name"
1975 );
1976 }
1977
1978 #[test]
1979 fn display_order_differs_from_internal_all_order() {
1980 let display_ids: Vec<&str> = providers_sorted_for_display()
1983 .iter()
1984 .map(|p| p.id())
1985 .collect();
1986 let internal_ids: Vec<&str> = all_providers().iter().map(|p| p.id()).collect();
1987 assert_ne!(
1988 display_ids, internal_ids,
1989 "display order should not match internal ALL order"
1990 );
1991 }
1992
1993 #[test]
1994 fn display_order_is_complete_and_unique() {
1995 let display = providers_sorted_for_display();
1997 assert_eq!(
1998 display.len(),
1999 all_providers().len(),
2000 "display order must include every built-in provider"
2001 );
2002 let mut ids: Vec<&str> = display.iter().map(|p| p.id()).collect();
2003 ids.sort_unstable();
2004 let before = ids.len();
2005 ids.dedup();
2006 assert_eq!(
2007 before,
2008 ids.len(),
2009 "display order must not contain duplicates"
2010 );
2011 }
2012
2013 #[test]
2014 fn deepseek_is_present_but_not_first_in_display_order() {
2015 let display = providers_sorted_for_display();
2018 assert_eq!(
2019 all_providers()[0].kind(),
2020 ProviderKind::Deepseek,
2021 "DeepSeek is expected to remain first in the stable internal order"
2022 );
2023 assert!(
2024 display.iter().any(|p| p.kind() == ProviderKind::Deepseek),
2025 "DeepSeek must remain present in display order"
2026 );
2027 assert_ne!(
2028 display[0].kind(),
2029 ProviderKind::Deepseek,
2030 "DeepSeek must not be hard-coded first in display order"
2031 );
2032 assert_eq!(
2036 display[0].display_name(),
2037 "Alibaba Cloud Model Studio",
2038 "alphabetical display order should lead with Alibaba Cloud Model Studio"
2039 );
2040 }
2041}