diff --git a/src/channels/lark.rs b/src/channels/lark.rs index 5f929f8..4be8f20 100644 --- a/src/channels/lark.rs +++ b/src/channels/lark.rs @@ -1085,7 +1085,7 @@ mod tests { "sender": { "sender_id": { "open_id": "ou_user" } }, "message": { "message_type": "text", - "content": "{\"text\":\"δ½ ε₯½δΈ–η•Œ 🌍\"}", + "content": "{\"text\":\"Hello world 🌍\"}", "chat_id": "oc_chat", "create_time": "1000" } @@ -1094,7 +1094,7 @@ mod tests { let msgs = ch.parse_event_payload(&payload); assert_eq!(msgs.len(), 1); - assert_eq!(msgs[0].content, "δ½ ε₯½δΈ–η•Œ 🌍"); + assert_eq!(msgs[0].content, "Hello world 🌍"); } #[test] diff --git a/src/config/schema.rs b/src/config/schema.rs index 54619dd..c90573c 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -1620,7 +1620,7 @@ impl Default for AuditConfig { } } -/// DingTalk (ι’‰ι’‰) configuration for Stream Mode messaging +/// DingTalk configuration for Stream Mode messaging #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DingTalkConfig { /// Client ID (AppKey) from DingTalk developer console @@ -1827,10 +1827,19 @@ impl Config { self.api_key = Some(key); } } - // API Key: GLM_API_KEY overrides when provider is glm (provider-specific) - if self.default_provider.as_deref() == Some("glm") - || self.default_provider.as_deref() == Some("zhipu") - { + // API Key: GLM_API_KEY overrides when provider is a GLM/Zhipu variant. + if matches!( + self.default_provider.as_deref(), + Some( + "glm" + | "zhipu" + | "glm-global" + | "zhipu-global" + | "glm-cn" + | "zhipu-cn" + | "bigmodel" + ) + ) { if let Ok(key) = std::env::var("GLM_API_KEY") { if !key.is_empty() { self.api_key = Some(key); @@ -3086,6 +3095,21 @@ default_temperature = 0.7 std::env::remove_var("PROVIDER"); } + #[test] + fn env_override_glm_api_key_for_regional_aliases() { + let _env_guard = env_override_test_guard(); + let mut config = Config { + default_provider: Some("glm-cn".to_string()), + ..Config::default() + }; + + std::env::set_var("GLM_API_KEY", "glm-regional-key"); + config.apply_env_overrides(); + assert_eq!(config.api_key.as_deref(), Some("glm-regional-key")); + + std::env::remove_var("GLM_API_KEY"); + } + #[test] fn env_override_model() { let _env_guard = env_override_test_guard(); diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 7c618ed..b59f6cf 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1318,7 +1318,7 @@ mod tests { #[test] fn whatsapp_signature_unicode_body() { let app_secret = "test_secret_key_12345"; - let body = "Hello πŸ¦€ δΈ–η•Œ".as_bytes(); + let body = "Hello πŸ¦€ World".as_bytes(); let signature_header = compute_whatsapp_signature_header(app_secret, body); diff --git a/src/integrations/registry.rs b/src/integrations/registry.rs index d725e3b..3933950 100644 --- a/src/integrations/registry.rs +++ b/src/integrations/registry.rs @@ -133,7 +133,7 @@ pub fn all_integrations() -> Vec { }, IntegrationEntry { name: "DingTalk", - description: "DingTalk Stream Mode (ι’‰ι’‰)", + description: "DingTalk Stream Mode", category: IntegrationCategory::Chat, status_fn: |c| { if c.channels_config.dingtalk.is_some() { @@ -317,7 +317,19 @@ pub fn all_integrations() -> Vec { description: "Kimi & Kimi Coding", category: IntegrationCategory::AiModel, status_fn: |c| { - if c.default_provider.as_deref() == Some("moonshot") { + if matches!( + c.default_provider.as_deref(), + Some( + "moonshot" + | "kimi" + | "moonshot-intl" + | "moonshot-global" + | "moonshot-cn" + | "kimi-intl" + | "kimi-global" + | "kimi-cn" + ) + ) { IntegrationStatus::Active } else { IntegrationStatus::Available @@ -365,7 +377,18 @@ pub fn all_integrations() -> Vec { description: "ChatGLM / Zhipu models", category: IntegrationCategory::AiModel, status_fn: |c| { - if c.default_provider.as_deref() == Some("glm") { + if matches!( + c.default_provider.as_deref(), + Some( + "glm" + | "zhipu" + | "glm-global" + | "zhipu-global" + | "glm-cn" + | "zhipu-cn" + | "bigmodel" + ) + ) { IntegrationStatus::Active } else { IntegrationStatus::Available @@ -377,7 +400,43 @@ pub fn all_integrations() -> Vec { description: "MiniMax AI models", category: IntegrationCategory::AiModel, status_fn: |c| { - if c.default_provider.as_deref() == Some("minimax") { + if matches!( + c.default_provider.as_deref(), + Some( + "minimax" + | "minimax-intl" + | "minimax-io" + | "minimax-global" + | "minimax-cn" + | "minimaxi" + ) + ) { + IntegrationStatus::Active + } else { + IntegrationStatus::Available + } + }, + }, + IntegrationEntry { + name: "Qwen", + description: "Alibaba DashScope Qwen models", + category: IntegrationCategory::AiModel, + status_fn: |c| { + if matches!( + c.default_provider.as_deref(), + Some( + "qwen" + | "dashscope" + | "qwen-cn" + | "dashscope-cn" + | "qwen-intl" + | "dashscope-intl" + | "qwen-international" + | "dashscope-international" + | "qwen-us" + | "dashscope-us" + ) + ) { IntegrationStatus::Active } else { IntegrationStatus::Available @@ -905,4 +964,40 @@ mod tests { "Expected 5+ AI model integrations, got {ai_count}" ); } + + #[test] + fn regional_provider_aliases_activate_expected_ai_integrations() { + let entries = all_integrations(); + let mut config = Config { + default_provider: Some("minimax-cn".to_string()), + ..Config::default() + }; + + let minimax = entries.iter().find(|e| e.name == "MiniMax").unwrap(); + assert!(matches!( + (minimax.status_fn)(&config), + IntegrationStatus::Active + )); + + config.default_provider = Some("glm-cn".to_string()); + let glm = entries.iter().find(|e| e.name == "GLM").unwrap(); + assert!(matches!( + (glm.status_fn)(&config), + IntegrationStatus::Active + )); + + config.default_provider = Some("moonshot-intl".to_string()); + let moonshot = entries.iter().find(|e| e.name == "Moonshot").unwrap(); + assert!(matches!( + (moonshot.status_fn)(&config), + IntegrationStatus::Active + )); + + config.default_provider = Some("qwen-intl".to_string()); + let qwen = entries.iter().find(|e| e.name == "Qwen").unwrap(); + assert!(matches!( + (qwen.status_fn)(&config), + IntegrationStatus::Active + )); + } } diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index 9e05f68..4aa339d 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -448,6 +448,20 @@ fn canonical_provider_name(provider_name: &str) -> &str { "grok" => "xai", "together" => "together-ai", "google" | "google-gemini" => "gemini", + "dashscope" + | "qwen-cn" + | "dashscope-cn" + | "qwen-intl" + | "dashscope-intl" + | "qwen-international" + | "dashscope-international" + | "qwen-us" + | "dashscope-us" => "qwen", + "zhipu" | "glm-global" | "zhipu-global" | "glm-cn" | "zhipu-cn" | "bigmodel" => "glm", + "kimi" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi-intl" + | "kimi-global" | "kimi-cn" => "moonshot", + "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" | "minimaxi" => "minimax", + "baidu" => "qianfan", _ => provider_name, } } @@ -467,6 +481,7 @@ fn default_model_for_provider(provider: &str) -> String { "openai" => "gpt-5.2".into(), "glm" | "zhipu" | "zai" | "z.ai" => "glm-5".into(), "minimax" => "MiniMax-M2.5".into(), + "qwen" => "qwen-plus".into(), "ollama" => "llama3.2".into(), "groq" => "llama-3.3-70b-versatile".into(), "deepseek" => "deepseek-chat".into(), @@ -702,6 +717,20 @@ fn curated_models_for_provider(provider_name: &str) -> Vec<(String, String)> { "MiniMax M2.1 Lightning (fast)".to_string(), ), ], + "qwen" => vec![ + ( + "qwen-max".to_string(), + "Qwen Max (highest quality)".to_string(), + ), + ( + "qwen-plus".to_string(), + "Qwen Plus (balanced default)".to_string(), + ), + ( + "qwen-turbo".to_string(), + "Qwen Turbo (fast and cost-efficient)".to_string(), + ), + ], "ollama" => vec![ ( "llama3.2".to_string(), @@ -1306,7 +1335,7 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { "⭐ Recommended (OpenRouter, Venice, Anthropic, OpenAI, Gemini)", "⚑ Fast inference (Groq, Fireworks, Together AI, NVIDIA NIM)", "🌐 Gateway / proxy (Vercel AI, Cloudflare AI, Amazon Bedrock)", - "πŸ”¬ Specialized (Moonshot/Kimi, GLM/Zhipu, MiniMax, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)", + "πŸ”¬ Specialized (Moonshot/Kimi, GLM/Zhipu, MiniMax, Qwen/DashScope, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)", "🏠 Local / private (Ollama β€” no API key needed)", "πŸ”§ Custom β€” bring your own OpenAI-compatible API", ]; @@ -1347,9 +1376,21 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { ("bedrock", "Amazon Bedrock β€” AWS managed models"), ], 3 => vec![ - ("moonshot", "Moonshot β€” Kimi & Kimi Coding"), - ("glm", "GLM β€” ChatGLM / Zhipu models"), - ("minimax", "MiniMax β€” MiniMax AI models"), + ("moonshot", "Moonshot β€” Kimi API (China endpoint)"), + ( + "moonshot-intl", + "Moonshot β€” Kimi API (international endpoint)", + ), + ("glm", "GLM β€” ChatGLM / Zhipu (international endpoint)"), + ("glm-cn", "GLM β€” ChatGLM / Zhipu (China endpoint)"), + ( + "minimax", + "MiniMax β€” international endpoint (api.minimax.io)", + ), + ("minimax-cn", "MiniMax β€” China endpoint (api.minimaxi.com)"), + ("qwen", "Qwen β€” DashScope China endpoint"), + ("qwen-intl", "Qwen β€” DashScope international endpoint"), + ("qwen-us", "Qwen β€” DashScope US endpoint"), ("qianfan", "Qianfan β€” Baidu AI models"), ("zai", "Z.AI β€” Z.AI inference"), ("synthetic", "Synthetic β€” Synthetic AI models"), @@ -1512,10 +1553,30 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { "perplexity" => "https://www.perplexity.ai/settings/api", "xai" => "https://console.x.ai", "cohere" => "https://dashboard.cohere.com/api-keys", - "moonshot" => "https://platform.moonshot.cn/console/api-keys", - "glm" | "zhipu" => "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys", - "zai" | "z.ai" => "https://platform.z.ai/", - "minimax" => "https://www.minimaxi.com/user-center/basic-information", + "moonshot" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi" + | "kimi-intl" | "kimi-global" | "kimi-cn" => { + "https://platform.moonshot.cn/console/api-keys" + } + "glm" | "zhipu" | "glm-global" | "zhipu-global" | "zai" | "z.ai" => { + "https://platform.z.ai/" + } + "glm-cn" | "zhipu-cn" | "bigmodel" => { + "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys" + } + "minimax" | "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" + | "minimaxi" => "https://www.minimaxi.com/user-center/basic-information", + "qwen" + | "dashscope" + | "qwen-cn" + | "dashscope-cn" + | "qwen-intl" + | "dashscope-intl" + | "qwen-international" + | "dashscope-international" + | "qwen-us" + | "dashscope-us" => { + "https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key" + } "vercel" => "https://vercel.com/account/tokens", "cloudflare" => "https://dash.cloudflare.com/profile/api-tokens", "nvidia" | "nvidia-nim" | "build.nvidia.com" => "https://build.nvidia.com/", @@ -1551,7 +1612,8 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { }; // ── Model selection ── - let models: Vec<(&str, &str)> = match provider_name { + let canonical_provider = canonical_provider_name(provider_name); + let models: Vec<(&str, &str)> = match canonical_provider { "openrouter" => vec![ ( "anthropic/claude-sonnet-4", @@ -1629,7 +1691,7 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { "Mixtral 8x22B", ), ], - "together" => vec![ + "together-ai" => vec![ ( "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "Llama 3.1 70B Turbo", @@ -1660,6 +1722,11 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String)> { ("glm-4-flash", "GLM-4 Flash (fast)"), ], "minimax" => MINIMAX_ONBOARD_MODELS.to_vec(), + "qwen" => vec![ + ("qwen-plus", "Qwen Plus (balanced default)"), + ("qwen-max", "Qwen Max (highest quality)"), + ("qwen-turbo", "Qwen Turbo (fast and cost-efficient)"), + ], "ollama" => vec![ ("llama3.2", "Llama 3.2 (recommended local)"), ("mistral", "Mistral 7B"), @@ -1861,6 +1928,7 @@ fn provider_env_var(name: &str) -> &'static str { "moonshot" | "kimi" => "MOONSHOT_API_KEY", "glm" | "zhipu" => "GLM_API_KEY", "minimax" => "MINIMAX_API_KEY", + "qwen" | "dashscope" => "DASHSCOPE_API_KEY", "qianfan" | "baidu" => "QIANFAN_API_KEY", "zai" | "z.ai" => "ZAI_API_KEY", "synthetic" => "SYNTHETIC_API_KEY", @@ -2384,7 +2452,7 @@ fn setup_channels() -> Result { if config.dingtalk.is_some() { "βœ… connected" } else { - "β€” ι’‰ι’‰ Stream Mode" + "β€” DingTalk Stream Mode" } ), "Done β€” finish setup".to_string(), @@ -3111,7 +3179,7 @@ fn setup_channels() -> Result { println!( " {} {}", style("DingTalk Setup").white().bold(), - style("β€” ι’‰ι’‰ Stream Mode").dim() + style("β€” DingTalk Stream Mode").dim() ); print_bullet("1. Go to DingTalk developer console (open.dingtalk.com)"); print_bullet("2. Create an app and enable the Stream Mode bot"); @@ -4313,6 +4381,10 @@ mod tests { default_model_for_provider("anthropic"), "claude-sonnet-4-5-20250929" ); + assert_eq!(default_model_for_provider("qwen"), "qwen-plus"); + assert_eq!(default_model_for_provider("qwen-intl"), "qwen-plus"); + assert_eq!(default_model_for_provider("glm-cn"), "glm-5"); + assert_eq!(default_model_for_provider("minimax-cn"), "MiniMax-M2.5"); assert_eq!(default_model_for_provider("gemini"), "gemini-2.5-pro"); assert_eq!(default_model_for_provider("google"), "gemini-2.5-pro"); assert_eq!( @@ -4321,6 +4393,17 @@ mod tests { ); } + #[test] + fn canonical_provider_name_normalizes_regional_aliases() { + assert_eq!(canonical_provider_name("qwen-intl"), "qwen"); + assert_eq!(canonical_provider_name("dashscope-us"), "qwen"); + assert_eq!(canonical_provider_name("moonshot-intl"), "moonshot"); + assert_eq!(canonical_provider_name("kimi-cn"), "moonshot"); + assert_eq!(canonical_provider_name("glm-cn"), "glm"); + assert_eq!(canonical_provider_name("bigmodel"), "glm"); + assert_eq!(canonical_provider_name("minimax-cn"), "minimax"); + } + #[test] fn curated_models_for_openai_include_latest_choices() { let ids: Vec = curated_models_for_provider("openai") @@ -4372,6 +4455,18 @@ mod tests { curated_models_for_provider("gemini"), curated_models_for_provider("google-gemini") ); + assert_eq!( + curated_models_for_provider("qwen"), + curated_models_for_provider("qwen-intl") + ); + assert_eq!( + curated_models_for_provider("qwen"), + curated_models_for_provider("dashscope-us") + ); + assert_eq!( + curated_models_for_provider("minimax"), + curated_models_for_provider("minimax-cn") + ); } #[test] @@ -4527,6 +4622,12 @@ mod tests { assert_eq!(provider_env_var("google"), "GEMINI_API_KEY"); // alias assert_eq!(provider_env_var("google-gemini"), "GEMINI_API_KEY"); // alias assert_eq!(provider_env_var("gemini"), "GEMINI_API_KEY"); + assert_eq!(provider_env_var("qwen"), "DASHSCOPE_API_KEY"); + assert_eq!(provider_env_var("qwen-intl"), "DASHSCOPE_API_KEY"); + assert_eq!(provider_env_var("dashscope-us"), "DASHSCOPE_API_KEY"); + assert_eq!(provider_env_var("glm-cn"), "GLM_API_KEY"); + assert_eq!(provider_env_var("minimax-cn"), "MINIMAX_API_KEY"); + assert_eq!(provider_env_var("moonshot-intl"), "MOONSHOT_API_KEY"); assert_eq!(provider_env_var("nvidia"), "NVIDIA_API_KEY"); assert_eq!(provider_env_var("nvidia-nim"), "NVIDIA_API_KEY"); // alias assert_eq!(provider_env_var("build.nvidia.com"), "NVIDIA_API_KEY"); // alias diff --git a/src/providers/mod.rs b/src/providers/mod.rs index e18e789..9dfa127 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -19,6 +19,52 @@ use compatible::{AuthStyle, OpenAiCompatibleProvider}; use reliable::ReliableProvider; const MAX_API_ERROR_CHARS: usize = 200; +const MINIMAX_INTL_BASE_URL: &str = "https://api.minimax.io/v1"; +const MINIMAX_CN_BASE_URL: &str = "https://api.minimaxi.com/v1"; +const GLM_GLOBAL_BASE_URL: &str = "https://api.z.ai/api/paas/v4"; +const GLM_CN_BASE_URL: &str = "https://open.bigmodel.cn/api/paas/v4"; +const MOONSHOT_INTL_BASE_URL: &str = "https://api.moonshot.ai/v1"; +const MOONSHOT_CN_BASE_URL: &str = "https://api.moonshot.cn/v1"; +const QWEN_CN_BASE_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1"; +const QWEN_INTL_BASE_URL: &str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"; +const QWEN_US_BASE_URL: &str = "https://dashscope-us.aliyuncs.com/compatible-mode/v1"; + +fn minimax_base_url(name: &str) -> Option<&'static str> { + match name { + "minimax" | "minimax-intl" | "minimax-io" | "minimax-global" => Some(MINIMAX_INTL_BASE_URL), + "minimax-cn" | "minimaxi" => Some(MINIMAX_CN_BASE_URL), + _ => None, + } +} + +fn glm_base_url(name: &str) -> Option<&'static str> { + match name { + "glm" | "zhipu" | "glm-global" | "zhipu-global" => Some(GLM_GLOBAL_BASE_URL), + "glm-cn" | "zhipu-cn" | "bigmodel" => Some(GLM_CN_BASE_URL), + _ => None, + } +} + +fn moonshot_base_url(name: &str) -> Option<&'static str> { + match name { + "moonshot-intl" | "moonshot-global" | "kimi-intl" | "kimi-global" => { + Some(MOONSHOT_INTL_BASE_URL) + } + "moonshot" | "kimi" | "moonshot-cn" | "kimi-cn" => Some(MOONSHOT_CN_BASE_URL), + _ => None, + } +} + +fn qwen_base_url(name: &str) -> Option<&'static str> { + match name { + "qwen" | "dashscope" | "qwen-cn" | "dashscope-cn" => Some(QWEN_CN_BASE_URL), + "qwen-intl" | "dashscope-intl" | "qwen-international" | "dashscope-international" => { + Some(QWEN_INTL_BASE_URL) + } + "qwen-us" | "dashscope-us" => Some(QWEN_US_BASE_URL), + _ => None, + } +} fn is_secret_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':') @@ -135,13 +181,24 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) -> "fireworks" | "fireworks-ai" => vec!["FIREWORKS_API_KEY"], "perplexity" => vec!["PERPLEXITY_API_KEY"], "cohere" => vec!["COHERE_API_KEY"], - "moonshot" | "kimi" => vec!["MOONSHOT_API_KEY"], - "glm" | "zhipu" => vec!["GLM_API_KEY"], - "minimax" => vec!["MINIMAX_API_KEY"], - "qianfan" | "baidu" => vec!["QIANFAN_API_KEY"], - "qwen" | "dashscope" | "qwen-intl" | "dashscope-intl" | "qwen-us" | "dashscope-us" => { - vec!["DASHSCOPE_API_KEY"] + "moonshot" | "kimi" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi-intl" + | "kimi-global" | "kimi-cn" => vec!["MOONSHOT_API_KEY"], + "glm" | "zhipu" | "glm-global" | "zhipu-global" | "glm-cn" | "zhipu-cn" | "bigmodel" => { + vec!["GLM_API_KEY"] } + "minimax" | "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" + | "minimaxi" => vec!["MINIMAX_API_KEY"], + "qianfan" | "baidu" => vec!["QIANFAN_API_KEY"], + "qwen" + | "dashscope" + | "qwen-cn" + | "dashscope-cn" + | "qwen-intl" + | "dashscope-intl" + | "qwen-international" + | "dashscope-international" + | "qwen-us" + | "dashscope-us" => vec!["DASHSCOPE_API_KEY"], "zai" | "z.ai" => vec!["ZAI_API_KEY"], "nvidia" | "nvidia-nim" | "build.nvidia.com" => vec!["NVIDIA_API_KEY"], "synthetic" => vec!["SYNTHETIC_API_KEY"], @@ -235,8 +292,11 @@ pub fn create_provider_with_url( key, AuthStyle::Bearer, ))), - "moonshot" | "kimi" => Ok(Box::new(OpenAiCompatibleProvider::new( - "Moonshot", "https://api.moonshot.cn", key, AuthStyle::Bearer, + name if moonshot_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new( + "Moonshot", + moonshot_base_url(name).expect("checked in guard"), + key, + AuthStyle::Bearer, ))), "synthetic" => Ok(Box::new(OpenAiCompatibleProvider::new( "Synthetic", "https://api.synthetic.com", key, AuthStyle::Bearer, @@ -247,12 +307,17 @@ pub fn create_provider_with_url( "zai" | "z.ai" => Ok(Box::new(OpenAiCompatibleProvider::new( "Z.AI", "https://api.z.ai/api/coding/paas/v4", key, AuthStyle::Bearer, ))), - "glm" | "zhipu" => Ok(Box::new(OpenAiCompatibleProvider::new_no_responses_fallback( - "GLM", "https://api.z.ai/api/paas/v4", key, AuthStyle::Bearer, - ))), - "minimax" => Ok(Box::new(OpenAiCompatibleProvider::new( + name if glm_base_url(name).is_some() => { + Ok(Box::new(OpenAiCompatibleProvider::new_no_responses_fallback( + "GLM", + glm_base_url(name).expect("checked in guard"), + key, + AuthStyle::Bearer, + ))) + } + name if minimax_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new( "MiniMax", - "https://api.minimaxi.com/v1", + minimax_base_url(name).expect("checked in guard"), key, AuthStyle::Bearer, ))), @@ -265,14 +330,11 @@ pub fn create_provider_with_url( "qianfan" | "baidu" => Ok(Box::new(OpenAiCompatibleProvider::new( "Qianfan", "https://aip.baidubce.com", key, AuthStyle::Bearer, ))), - "qwen" | "dashscope" => Ok(Box::new(OpenAiCompatibleProvider::new( - "Qwen", "https://dashscope.aliyuncs.com/compatible-mode/v1", key, AuthStyle::Bearer, - ))), - "qwen-intl" | "dashscope-intl" => Ok(Box::new(OpenAiCompatibleProvider::new( - "Qwen", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", key, AuthStyle::Bearer, - ))), - "qwen-us" | "dashscope-us" => Ok(Box::new(OpenAiCompatibleProvider::new( - "Qwen", "https://dashscope-us.aliyuncs.com/compatible-mode/v1", key, AuthStyle::Bearer, + name if qwen_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new( + "Qwen", + qwen_base_url(name).expect("checked in guard"), + key, + AuthStyle::Bearer, ))), // ── Extended ecosystem (community favorites) ───────── @@ -492,6 +554,31 @@ mod tests { assert_eq!(resolved, Some("explicit-key".to_string())); } + #[test] + fn regional_endpoint_aliases_map_to_expected_urls() { + assert_eq!(minimax_base_url("minimax"), Some(MINIMAX_INTL_BASE_URL)); + assert_eq!( + minimax_base_url("minimax-intl"), + Some(MINIMAX_INTL_BASE_URL) + ); + assert_eq!(minimax_base_url("minimax-cn"), Some(MINIMAX_CN_BASE_URL)); + + assert_eq!(glm_base_url("glm"), Some(GLM_GLOBAL_BASE_URL)); + assert_eq!(glm_base_url("glm-cn"), Some(GLM_CN_BASE_URL)); + assert_eq!(glm_base_url("bigmodel"), Some(GLM_CN_BASE_URL)); + + assert_eq!(moonshot_base_url("moonshot"), Some(MOONSHOT_CN_BASE_URL)); + assert_eq!( + moonshot_base_url("moonshot-intl"), + Some(MOONSHOT_INTL_BASE_URL) + ); + + assert_eq!(qwen_base_url("qwen"), Some(QWEN_CN_BASE_URL)); + assert_eq!(qwen_base_url("qwen-cn"), Some(QWEN_CN_BASE_URL)); + assert_eq!(qwen_base_url("qwen-intl"), Some(QWEN_INTL_BASE_URL)); + assert_eq!(qwen_base_url("qwen-us"), Some(QWEN_US_BASE_URL)); + } + // ── Primary providers ──────────────────────────────────── #[test] @@ -550,6 +637,10 @@ mod tests { fn factory_moonshot() { assert!(create_provider("moonshot", Some("key")).is_ok()); assert!(create_provider("kimi", Some("key")).is_ok()); + assert!(create_provider("moonshot-intl", Some("key")).is_ok()); + assert!(create_provider("moonshot-cn", Some("key")).is_ok()); + assert!(create_provider("kimi-intl", Some("key")).is_ok()); + assert!(create_provider("kimi-cn", Some("key")).is_ok()); } #[test] @@ -573,11 +664,19 @@ mod tests { fn factory_glm() { assert!(create_provider("glm", Some("key")).is_ok()); assert!(create_provider("zhipu", Some("key")).is_ok()); + assert!(create_provider("glm-cn", Some("key")).is_ok()); + assert!(create_provider("zhipu-cn", Some("key")).is_ok()); + assert!(create_provider("glm-global", Some("key")).is_ok()); + assert!(create_provider("bigmodel", Some("key")).is_ok()); } #[test] fn factory_minimax() { assert!(create_provider("minimax", Some("key")).is_ok()); + assert!(create_provider("minimax-intl", Some("key")).is_ok()); + assert!(create_provider("minimax-io", Some("key")).is_ok()); + assert!(create_provider("minimax-cn", Some("key")).is_ok()); + assert!(create_provider("minimaxi", Some("key")).is_ok()); } #[test] @@ -596,8 +695,12 @@ mod tests { fn factory_qwen() { assert!(create_provider("qwen", Some("key")).is_ok()); assert!(create_provider("dashscope", Some("key")).is_ok()); + assert!(create_provider("qwen-cn", Some("key")).is_ok()); + assert!(create_provider("dashscope-cn", Some("key")).is_ok()); assert!(create_provider("qwen-intl", Some("key")).is_ok()); assert!(create_provider("dashscope-intl", Some("key")).is_ok()); + assert!(create_provider("qwen-international", Some("key")).is_ok()); + assert!(create_provider("dashscope-international", Some("key")).is_ok()); assert!(create_provider("qwen-us", Some("key")).is_ok()); assert!(create_provider("dashscope-us", Some("key")).is_ok()); } @@ -860,15 +963,20 @@ mod tests { "vercel", "cloudflare", "moonshot", + "moonshot-intl", + "moonshot-cn", "synthetic", "opencode", "zai", "glm", + "glm-cn", "minimax", + "minimax-cn", "bedrock", "qianfan", "qwen", "qwen-intl", + "qwen-cn", "qwen-us", "lmstudio", "groq",