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