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