fix(provider): follow-up CN/global consistency for Z.AI and aliases (#554)

* fix(provider): harden CN/global routing consistency for Chinese vendors

* fix(agent): migrate CLI channel send to SendMessage

* fix(onboard): deduplicate Z.AI key URL match arms
This commit is contained in:
Chummy 2026-02-18 00:04:56 +08:00 committed by GitHub
parent cd0dd13476
commit fc6e8eb521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 97 additions and 13 deletions

View file

@ -1130,8 +1130,11 @@ pub async fn run(
} }
}; };
final_output = response.clone(); final_output = response.clone();
if let Err(e) = if let Err(e) = crate::channels::Channel::send(
crate::channels::Channel::send(&cli, &format!("\n{response}\n"), "user").await &cli,
&crate::channels::traits::SendMessage::new(format!("\n{response}\n"), "user"),
)
.await
{ {
eprintln!("\nError sending CLI response: {e}\n"); eprintln!("\nError sending CLI response: {e}\n");
} }

View file

@ -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 // Provider: ZEROCLAW_PROVIDER or PROVIDER
if let Ok(provider) = if let Ok(provider) =
std::env::var("ZEROCLAW_PROVIDER").or_else(|_| std::env::var("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"); 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] #[test]
fn env_override_model() { fn env_override_model() {
let _env_guard = env_override_test_guard(); let _env_guard = env_override_test_guard();

View file

@ -377,7 +377,10 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "Z.AI inference", description: "Z.AI inference",
category: IntegrationCategory::AiModel, category: IntegrationCategory::AiModel,
status_fn: |c| { 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 IntegrationStatus::Active
} else { } else {
IntegrationStatus::Available IntegrationStatus::Available
@ -472,7 +475,7 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "Baidu AI models", description: "Baidu AI models",
category: IntegrationCategory::AiModel, category: IntegrationCategory::AiModel,
status_fn: |c| { status_fn: |c| {
if c.default_provider.as_deref() == Some("qianfan") { if matches!(c.default_provider.as_deref(), Some("qianfan" | "baidu")) {
IntegrationStatus::Active IntegrationStatus::Active
} else { } else {
IntegrationStatus::Available IntegrationStatus::Available
@ -1011,5 +1014,19 @@ mod tests {
(qwen.status_fn)(&config), (qwen.status_fn)(&config),
IntegrationStatus::Active 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
));
} }
} }

View file

@ -463,6 +463,7 @@ fn canonical_provider_name(provider_name: &str) -> &str {
"kimi" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi-intl" "kimi" | "moonshot-intl" | "moonshot-global" | "moonshot-cn" | "kimi-intl"
| "kimi-global" | "kimi-cn" => "moonshot", | "kimi-global" | "kimi-cn" => "moonshot",
"minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" | "minimaxi" => "minimax", "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", "baidu" => "qianfan",
_ => provider_name, _ => provider_name,
} }
@ -1393,8 +1394,9 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio
("qwen", "Qwen — DashScope China endpoint"), ("qwen", "Qwen — DashScope China endpoint"),
("qwen-intl", "Qwen — DashScope international endpoint"), ("qwen-intl", "Qwen — DashScope international endpoint"),
("qwen-us", "Qwen — DashScope US endpoint"), ("qwen-us", "Qwen — DashScope US endpoint"),
("qianfan", "Qianfan — Baidu AI models"), ("qianfan", "Qianfan — Baidu AI models (China endpoint)"),
("zai", "Z.AI — Z.AI inference"), ("zai", "Z.AI — global coding endpoint"),
("zai-cn", "Z.AI — China coding endpoint (open.bigmodel.cn)"),
("synthetic", "Synthetic — Synthetic AI models"), ("synthetic", "Synthetic — Synthetic AI models"),
("opencode", "OpenCode Zen — code-focused AI"), ("opencode", "OpenCode Zen — code-focused AI"),
("cohere", "Cohere — Command R+ & embeddings"), ("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" => { | "kimi-intl" | "kimi-global" | "kimi-cn" => {
"https://platform.moonshot.cn/console/api-keys" "https://platform.moonshot.cn/console/api-keys"
} }
"glm" | "zhipu" | "glm-global" | "zhipu-global" | "zai" | "z.ai" => { "glm" | "zhipu" | "glm-global" | "zhipu-global" | "zai" | "z.ai" | "zai-global"
"https://platform.z.ai/" | "z.ai-global" => "https://platform.z.ai/",
} "glm-cn" | "zhipu-cn" | "bigmodel" | "zai-cn" | "z.ai-cn" => {
"glm-cn" | "zhipu-cn" | "bigmodel" => {
"https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys" "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys"
} }
"minimax" | "minimax-intl" | "minimax-io" | "minimax-global" | "minimax-cn" "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" => { | "dashscope-us" => {
"https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key" "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", "vercel" => "https://vercel.com/account/tokens",
"cloudflare" => "https://dash.cloudflare.com/profile/api-tokens", "cloudflare" => "https://dash.cloudflare.com/profile/api-tokens",
"nvidia" | "nvidia-nim" | "build.nvidia.com" => "https://build.nvidia.com/", "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("qwen-intl"), "qwen-plus");
assert_eq!(default_model_for_provider("glm-cn"), "glm-5"); 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("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("gemini"), "gemini-2.5-pro");
assert_eq!(default_model_for_provider("google"), "gemini-2.5-pro"); assert_eq!(default_model_for_provider("google"), "gemini-2.5-pro");
assert_eq!( assert_eq!(
@ -4541,6 +4544,8 @@ mod tests {
assert_eq!(canonical_provider_name("glm-cn"), "glm"); assert_eq!(canonical_provider_name("glm-cn"), "glm");
assert_eq!(canonical_provider_name("bigmodel"), "glm"); assert_eq!(canonical_provider_name("bigmodel"), "glm");
assert_eq!(canonical_provider_name("minimax-cn"), "minimax"); 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] #[test]
@ -4606,6 +4611,10 @@ mod tests {
curated_models_for_provider("minimax"), curated_models_for_provider("minimax"),
curated_models_for_provider("minimax-cn") curated_models_for_provider("minimax-cn")
); );
assert_eq!(
curated_models_for_provider("zai"),
curated_models_for_provider("zai-cn")
);
} }
#[test] #[test]
@ -4767,6 +4776,7 @@ mod tests {
assert_eq!(provider_env_var("glm-cn"), "GLM_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("minimax-cn"), "MINIMAX_API_KEY");
assert_eq!(provider_env_var("moonshot-intl"), "MOONSHOT_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"), "NVIDIA_API_KEY");
assert_eq!(provider_env_var("nvidia-nim"), "NVIDIA_API_KEY"); // alias assert_eq!(provider_env_var("nvidia-nim"), "NVIDIA_API_KEY"); // alias
assert_eq!(provider_env_var("build.nvidia.com"), "NVIDIA_API_KEY"); // alias assert_eq!(provider_env_var("build.nvidia.com"), "NVIDIA_API_KEY"); // alias

View file

@ -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_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_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 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> { fn minimax_base_url(name: &str) -> Option<&'static str> {
match name { 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 { fn is_secret_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':') c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':')
} }
@ -200,7 +210,9 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
| "dashscope-international" | "dashscope-international"
| "qwen-us" | "qwen-us"
| "dashscope-us" => vec!["DASHSCOPE_API_KEY"], | "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"], "nvidia" | "nvidia-nim" | "build.nvidia.com" => vec!["NVIDIA_API_KEY"],
"synthetic" => vec!["SYNTHETIC_API_KEY"], "synthetic" => vec!["SYNTHETIC_API_KEY"],
"opencode" | "opencode-zen" => vec!["OPENCODE_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" | "opencode-zen" => Ok(Box::new(OpenAiCompatibleProvider::new(
"OpenCode Zen", "https://opencode.ai/zen/v1", key, AuthStyle::Bearer, "OpenCode Zen", "https://opencode.ai/zen/v1", key, AuthStyle::Bearer,
))), ))),
"zai" | "z.ai" => Ok(Box::new(OpenAiCompatibleProvider::new( name if zai_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new(
"Z.AI", "https://api.z.ai/api/coding/paas/v4", key, AuthStyle::Bearer, "Z.AI",
zai_base_url(name).expect("checked in guard"),
key,
AuthStyle::Bearer,
))), ))),
name if glm_base_url(name).is_some() => { name if glm_base_url(name).is_some() => {
Ok(Box::new(OpenAiCompatibleProvider::new_no_responses_fallback( 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-cn"), Some(QWEN_CN_BASE_URL));
assert_eq!(qwen_base_url("qwen-intl"), Some(QWEN_INTL_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!(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 ──────────────────────────────────── // ── Primary providers ────────────────────────────────────
@ -659,6 +681,10 @@ mod tests {
fn factory_zai() { fn factory_zai() {
assert!(create_provider("zai", Some("key")).is_ok()); assert!(create_provider("zai", Some("key")).is_ok());
assert!(create_provider("z.ai", 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] #[test]
@ -976,6 +1002,7 @@ mod tests {
"synthetic", "synthetic",
"opencode", "opencode",
"zai", "zai",
"zai-cn",
"glm", "glm",
"glm-cn", "glm-cn",
"minimax", "minimax",