diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 6ff27b4..8e4ecb1 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -1130,8 +1130,11 @@ pub async fn run( } }; final_output = response.clone(); - if let Err(e) = - crate::channels::Channel::send(&cli, &format!("\n{response}\n"), "user").await + if let Err(e) = crate::channels::Channel::send( + &cli, + &crate::channels::traits::SendMessage::new(format!("\n{response}\n"), "user"), + ) + .await { eprintln!("\nError sending CLI response: {e}\n"); } diff --git a/src/config/schema.rs b/src/config/schema.rs index 99ac0fe..e1258f6 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -1879,6 +1879,18 @@ impl Config { } } + // API Key: ZAI_API_KEY overrides when provider is a Z.AI variant. + if matches!( + self.default_provider.as_deref(), + Some("zai" | "z.ai" | "zai-global" | "z.ai-global" | "zai-cn" | "z.ai-cn") + ) { + if let Ok(key) = std::env::var("ZAI_API_KEY") { + if !key.is_empty() { + self.api_key = Some(key); + } + } + } + // Provider: ZEROCLAW_PROVIDER or PROVIDER if let Ok(provider) = std::env::var("ZEROCLAW_PROVIDER").or_else(|_| std::env::var("PROVIDER")) @@ -3147,6 +3159,21 @@ default_temperature = 0.7 std::env::remove_var("GLM_API_KEY"); } + #[test] + fn env_override_zai_api_key_for_regional_aliases() { + let _env_guard = env_override_test_guard(); + let mut config = Config { + default_provider: Some("zai-cn".to_string()), + ..Config::default() + }; + + std::env::set_var("ZAI_API_KEY", "zai-regional-key"); + config.apply_env_overrides(); + assert_eq!(config.api_key.as_deref(), Some("zai-regional-key")); + + std::env::remove_var("ZAI_API_KEY"); + } + #[test] fn env_override_model() { let _env_guard = env_override_test_guard(); diff --git a/src/integrations/registry.rs b/src/integrations/registry.rs index ac1ee7b..6024300 100644 --- a/src/integrations/registry.rs +++ b/src/integrations/registry.rs @@ -377,7 +377,10 @@ pub fn all_integrations() -> Vec { description: "Z.AI inference", category: IntegrationCategory::AiModel, status_fn: |c| { - if c.default_provider.as_deref() == Some("zai") { + if matches!( + c.default_provider.as_deref(), + Some("zai" | "z.ai" | "zai-global" | "z.ai-global" | "zai-cn" | "z.ai-cn") + ) { IntegrationStatus::Active } else { IntegrationStatus::Available @@ -472,7 +475,7 @@ pub fn all_integrations() -> Vec { description: "Baidu AI models", category: IntegrationCategory::AiModel, status_fn: |c| { - if c.default_provider.as_deref() == Some("qianfan") { + if matches!(c.default_provider.as_deref(), Some("qianfan" | "baidu")) { IntegrationStatus::Active } else { IntegrationStatus::Available @@ -1011,5 +1014,19 @@ mod tests { (qwen.status_fn)(&config), IntegrationStatus::Active )); + + config.default_provider = Some("zai-cn".to_string()); + let zai = entries.iter().find(|e| e.name == "Z.AI").unwrap(); + assert!(matches!( + (zai.status_fn)(&config), + IntegrationStatus::Active + )); + + config.default_provider = Some("baidu".to_string()); + let qianfan = entries.iter().find(|e| e.name == "Qianfan").unwrap(); + assert!(matches!( + (qianfan.status_fn)(&config), + IntegrationStatus::Active + )); } } diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index c28f00d..2152a4a 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -463,6 +463,7 @@ fn canonical_provider_name(provider_name: &str) -> &str { "kimi" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi-intl" | "kimi-global" | "kimi-cn" => "moonshot", "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" | "minimaxi" => "minimax", + "z.ai" | "zai-global" | "z.ai-global" | "zai-cn" | "z.ai-cn" => "zai", "baidu" => "qianfan", _ => provider_name, } @@ -1393,8 +1394,9 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio ("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"), + ("qianfan", "Qianfan — Baidu AI models (China endpoint)"), + ("zai", "Z.AI — global coding endpoint"), + ("zai-cn", "Z.AI — China coding endpoint (open.bigmodel.cn)"), ("synthetic", "Synthetic — Synthetic AI models"), ("opencode", "OpenCode Zen — code-focused AI"), ("cohere", "Cohere — Command R+ & embeddings"), @@ -1602,10 +1604,9 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio | "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" => { + "glm" | "zhipu" | "glm-global" | "zhipu-global" | "zai" | "z.ai" | "zai-global" + | "z.ai-global" => "https://platform.z.ai/", + "glm-cn" | "zhipu-cn" | "bigmodel" | "zai-cn" | "z.ai-cn" => { "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys" } "minimax" | "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" @@ -1622,6 +1623,7 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio | "dashscope-us" => { "https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key" } + "qianfan" | "baidu" => "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/7lm0vxo78", "vercel" => "https://vercel.com/account/tokens", "cloudflare" => "https://dash.cloudflare.com/profile/api-tokens", "nvidia" | "nvidia-nim" | "build.nvidia.com" => "https://build.nvidia.com/", @@ -4524,6 +4526,7 @@ mod tests { 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("zai-cn"), "glm-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!( @@ -4541,6 +4544,8 @@ mod tests { assert_eq!(canonical_provider_name("glm-cn"), "glm"); assert_eq!(canonical_provider_name("bigmodel"), "glm"); assert_eq!(canonical_provider_name("minimax-cn"), "minimax"); + assert_eq!(canonical_provider_name("zai-cn"), "zai"); + assert_eq!(canonical_provider_name("z.ai-global"), "zai"); } #[test] @@ -4606,6 +4611,10 @@ mod tests { curated_models_for_provider("minimax"), curated_models_for_provider("minimax-cn") ); + assert_eq!( + curated_models_for_provider("zai"), + curated_models_for_provider("zai-cn") + ); } #[test] @@ -4767,6 +4776,7 @@ mod tests { 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("zai-cn"), "ZAI_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 636be75..85fa3ad 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -28,6 +28,8 @@ 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"; +const ZAI_GLOBAL_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4"; +const ZAI_CN_BASE_URL: &str = "https://open.bigmodel.cn/api/coding/paas/v4"; fn minimax_base_url(name: &str) -> Option<&'static str> { match name { @@ -66,6 +68,14 @@ fn qwen_base_url(name: &str) -> Option<&'static str> { } } +fn zai_base_url(name: &str) -> Option<&'static str> { + match name { + "zai" | "z.ai" | "zai-global" | "z.ai-global" => Some(ZAI_GLOBAL_BASE_URL), + "zai-cn" | "z.ai-cn" => Some(ZAI_CN_BASE_URL), + _ => None, + } +} + fn is_secret_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':') } @@ -200,7 +210,9 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) -> | "dashscope-international" | "qwen-us" | "dashscope-us" => vec!["DASHSCOPE_API_KEY"], - "zai" | "z.ai" => vec!["ZAI_API_KEY"], + "zai" | "z.ai" | "zai-global" | "z.ai-global" | "zai-cn" | "z.ai-cn" => { + vec!["ZAI_API_KEY"] + } "nvidia" | "nvidia-nim" | "build.nvidia.com" => vec!["NVIDIA_API_KEY"], "synthetic" => vec!["SYNTHETIC_API_KEY"], "opencode" | "opencode-zen" => vec!["OPENCODE_API_KEY"], @@ -305,8 +317,11 @@ pub fn create_provider_with_url( "opencode" | "opencode-zen" => Ok(Box::new(OpenAiCompatibleProvider::new( "OpenCode Zen", "https://opencode.ai/zen/v1", key, AuthStyle::Bearer, ))), - "zai" | "z.ai" => Ok(Box::new(OpenAiCompatibleProvider::new( - "Z.AI", "https://api.z.ai/api/coding/paas/v4", key, AuthStyle::Bearer, + name if zai_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new( + "Z.AI", + zai_base_url(name).expect("checked in guard"), + key, + AuthStyle::Bearer, ))), name if glm_base_url(name).is_some() => { Ok(Box::new(OpenAiCompatibleProvider::new_no_responses_fallback( @@ -578,6 +593,13 @@ mod tests { 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)); + + assert_eq!(zai_base_url("zai"), Some(ZAI_GLOBAL_BASE_URL)); + assert_eq!(zai_base_url("z.ai"), Some(ZAI_GLOBAL_BASE_URL)); + assert_eq!(zai_base_url("zai-global"), Some(ZAI_GLOBAL_BASE_URL)); + assert_eq!(zai_base_url("z.ai-global"), Some(ZAI_GLOBAL_BASE_URL)); + assert_eq!(zai_base_url("zai-cn"), Some(ZAI_CN_BASE_URL)); + assert_eq!(zai_base_url("z.ai-cn"), Some(ZAI_CN_BASE_URL)); } // ── Primary providers ──────────────────────────────────── @@ -659,6 +681,10 @@ mod tests { fn factory_zai() { assert!(create_provider("zai", Some("key")).is_ok()); assert!(create_provider("z.ai", Some("key")).is_ok()); + assert!(create_provider("zai-global", Some("key")).is_ok()); + assert!(create_provider("z.ai-global", Some("key")).is_ok()); + assert!(create_provider("zai-cn", Some("key")).is_ok()); + assert!(create_provider("z.ai-cn", Some("key")).is_ok()); } #[test] @@ -976,6 +1002,7 @@ mod tests { "synthetic", "opencode", "zai", + "zai-cn", "glm", "glm-cn", "minimax",