This commit is contained in:
khhjoe 2026-03-24 20:39:30 +08:00 committed by GitHub
commit 3bb831f477
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 191 additions and 7 deletions

View File

@ -142,6 +142,7 @@ pub async fn run_wizard(force: bool) -> Result<Config> {
default_temperature: 0.7,
provider_timeout_secs: 120,
extra_headers: std::collections::HashMap::new(),
observability: ObservabilityConfig::default(),
autonomy: AutonomyConfig::default(),
backup: crate::config::BackupConfig::default(),
@ -582,6 +583,7 @@ async fn run_quick_setup_with_home(
default_temperature: 0.7,
provider_timeout_secs: 120,
extra_headers: std::collections::HashMap::new(),
observability: ObservabilityConfig::default(),
autonomy: AutonomyConfig::default(),
backup: crate::config::BackupConfig::default(),
@ -833,6 +835,10 @@ const MINIMAX_ONBOARD_MODELS: [(&str, &str); 7] = [
];
fn default_model_for_provider(provider: &str) -> String {
if provider == "qwen-coding-plan" {
return "qwen3-coder-plus".into();
}
match canonical_provider_name(provider) {
"anthropic" => "claude-sonnet-4-5-20250929".into(),
"openai" => "gpt-5.2".into(),
@ -865,6 +871,23 @@ fn default_model_for_provider(provider: &str) -> String {
}
fn curated_models_for_provider(provider_name: &str) -> Vec<(String, String)> {
if provider_name == "qwen-coding-plan" {
return vec![
(
"qwen3-coder-plus".to_string(),
"Qwen3 Coder Plus (recommended for coding workflows)".to_string(),
),
(
"qwen3.5-plus".to_string(),
"Qwen3.5 Plus (reasoning + coding)".to_string(),
),
(
"qwen3-max-2026-01-23".to_string(),
"Qwen3 Max (high-capability coding model)".to_string(),
),
];
}
match canonical_provider_name(provider_name) {
"openrouter" => vec![
(
@ -1343,6 +1366,7 @@ fn supports_live_model_fetch(provider_name: &str) -> bool {
fn models_endpoint_for_provider(provider_name: &str) -> Option<&'static str> {
match provider_name {
"qwen-coding-plan" => Some("https://coding.dashscope.aliyuncs.com/v1/models"),
"qwen-intl" => Some("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models"),
"dashscope-us" => Some("https://dashscope-us.aliyuncs.com/compatible-mode/v1/models"),
"moonshot-cn" | "kimi-cn" => Some("https://api.moonshot.cn/v1/models"),
@ -2305,7 +2329,7 @@ async 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, Qwen/DashScope, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)",
"🔬 Specialized (Moonshot/Kimi, GLM/Zhipu, MiniMax, Qwen/DashScope, Qwen Coding, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)",
"🏠 Local / private (Ollama, llama.cpp server, vLLM — no API key needed)",
"🔧 Custom — bring your own OpenAI-compatible API",
];
@ -2380,8 +2404,17 @@ async fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String,
),
("minimax-cn", "MiniMax — China endpoint (api.minimaxi.com)"),
("qwen", "Qwen — DashScope China endpoint"),
(
"qwen-coding-plan",
"Qwen — DashScope coding plan endpoint (coding.dashscope.aliyuncs.com)",
),
("qwen-intl", "Qwen — DashScope international endpoint"),
("qwen-us", "Qwen — DashScope US endpoint"),
(
"qwen-coding",
"Qwen Coding — ModelStudio / coding-intl (Coding Plan)",
),
("qwen-coding-cn", "Qwen Coding — ModelStudio China endpoint"),
("qianfan", "Qianfan — Baidu AI models (China endpoint)"),
("zai", "Z.AI — global coding endpoint"),
("zai-cn", "Z.AI — China coding endpoint (open.bigmodel.cn)"),
@ -6828,6 +6861,10 @@ mod tests {
);
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("qwen-coding-plan"),
"qwen3-coder-plus"
);
assert_eq!(default_model_for_provider("qwen-code"), "qwen3-coder-plus");
assert_eq!(default_model_for_provider("glm-cn"), "glm-5");
assert_eq!(default_model_for_provider("minimax-cn"), "MiniMax-M2.7");
@ -6873,6 +6910,7 @@ mod tests {
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("qwen-coding-plan"), "qwen");
assert_eq!(canonical_provider_name("qwen-code"), "qwen-code");
assert_eq!(canonical_provider_name("qwen-oauth"), "qwen-code");
assert_eq!(canonical_provider_name("codex"), "openai-codex");
@ -7017,6 +7055,18 @@ mod tests {
assert!(ids.contains(&"minimax/minimax-m2.5".to_string()));
}
#[test]
fn curated_models_for_qwen_coding_plan_include_coding_models() {
let ids: Vec<String> = curated_models_for_provider("qwen-coding-plan")
.into_iter()
.map(|(id, _)| id)
.collect();
assert!(ids.contains(&"qwen3-coder-plus".to_string()));
assert!(ids.contains(&"qwen3.5-plus".to_string()));
assert!(ids.contains(&"qwen3-max-2026-01-23".to_string()));
}
#[test]
fn supports_live_model_fetch_for_supported_and_unsupported_providers() {
assert!(supports_live_model_fetch("openai"));
@ -7038,6 +7088,7 @@ mod tests {
assert!(supports_live_model_fetch("venice"));
assert!(supports_live_model_fetch("glm-cn"));
assert!(supports_live_model_fetch("qwen-intl"));
assert!(supports_live_model_fetch("qwen-coding-plan"));
assert!(!supports_live_model_fetch("minimax-cn"));
assert!(!supports_live_model_fetch("unknown-provider"));
}
@ -7068,6 +7119,10 @@ mod tests {
curated_models_for_provider("qwen"),
curated_models_for_provider("dashscope-us")
);
assert_eq!(
curated_models_for_provider("qwen-coding-plan"),
curated_models_for_provider("qwen-code")
);
assert_eq!(
curated_models_for_provider("minimax"),
curated_models_for_provider("minimax-cn")
@ -7120,6 +7175,10 @@ mod tests {
models_endpoint_for_provider("qwen-intl"),
Some("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models")
);
assert_eq!(
models_endpoint_for_provider("qwen-coding-plan"),
Some("https://coding.dashscope.aliyuncs.com/v1/models")
);
}
#[test]
@ -7430,6 +7489,9 @@ mod tests {
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("qwen-coding-plan"), "DASHSCOPE_API_KEY");
assert_eq!(provider_env_var("qwen-coding"), "DASHSCOPE_API_KEY");
assert_eq!(provider_env_var("qwen-coding-cn"), "DASHSCOPE_API_KEY");
assert_eq!(provider_env_var("qwen-code"), "QWEN_OAUTH_TOKEN");
assert_eq!(provider_env_var("qwen-oauth"), "QWEN_OAUTH_TOKEN");
assert_eq!(provider_env_var("glm-cn"), "GLM_API_KEY");

View File

@ -190,6 +190,13 @@ impl AnthropicProvider {
}
}
/// The user-agent string used in all Anthropic API requests.
/// Some Anthropic-compatible endpoints (e.g. Alibaba Coding Plan) require
/// a non-empty User-Agent header.
fn user_agent() -> &'static str {
"zeroclaw/0.5"
}
fn is_setup_token(token: &str) -> bool {
token.starts_with("sk-ant-oat01-")
}
@ -549,7 +556,21 @@ impl AnthropicProvider {
}
fn http_client(&self) -> Client {
crate::config::build_runtime_proxy_client_with_timeouts("provider.anthropic", 120, 10)
// Build via the standard proxy-aware builder, but ensure a non-empty
// User-Agent is always set. Some Anthropic-compatible endpoints
// (e.g. Alibaba Coding Plan) reject requests without one.
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.connect_timeout(std::time::Duration::from_secs(10))
.user_agent(Self::user_agent())
.build()
.unwrap_or_else(|_| {
crate::config::build_runtime_proxy_client_with_timeouts(
"provider.anthropic",
120,
10,
)
})
}
}
@ -591,18 +612,38 @@ impl Provider for AnthropicProvider {
tool_choice: None,
};
let url = format!("{}/v1/messages", self.base_url);
let request_body = serde_json::to_string(&request).unwrap_or_default();
tracing::debug!(
base_url = %self.base_url,
url = %url,
model = %model,
has_credential = !credential.is_empty(),
credential_prefix = %&credential[..credential.len().min(10)],
request_body_len = request_body.len(),
"Anthropic chat_with_system: sending request"
);
tracing::debug!(request_body = %request_body, "Anthropic request body");
let mut request = self
.http_client()
.post(format!("{}/v1/messages", self.base_url))
.post(&url)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&request);
.body(request_body);
request = self.apply_auth(request, credential);
let response = request.send().await?;
if !response.status().is_success() {
let status = response.status();
tracing::warn!(
base_url = %self.base_url,
url = %url,
status = %status,
credential_prefix = %&credential[..credential.len().min(10)],
"Anthropic chat_with_system: request failed"
);
return Err(super::api_error("Anthropic", response).await);
}
@ -661,15 +702,31 @@ impl Provider for AnthropicProvider {
tool_choice,
};
let url = format!("{}/v1/messages", self.base_url);
tracing::debug!(
base_url = %self.base_url,
url = %url,
model = %model,
has_credential = !credential.is_empty(),
credential_prefix = %&credential[..credential.len().min(10)],
"Anthropic provider: sending request"
);
let req = self
.http_client()
.post(format!("{}/v1/messages", self.base_url))
.post(&url)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&native_request);
let response = self.apply_auth(req, credential).send().await?;
if !response.status().is_success() {
let status = response.status();
tracing::warn!(
base_url = %self.base_url,
url = %url,
status = %status,
"Anthropic provider: request failed"
);
return Err(super::api_error("Anthropic", response).await);
}

View File

@ -66,6 +66,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 QWEN_CODING_INTL_BASE_URL: &str = "https://coding-intl.dashscope.aliyuncs.com/v1";
const QWEN_CODING_CN_BASE_URL: &str = "https://coding.dashscope.aliyuncs.com/v1";
const QWEN_OAUTH_BASE_FALLBACK_URL: &str = QWEN_CN_BASE_URL;
const BAILIAN_BASE_URL: &str = "https://coding.dashscope.aliyuncs.com/v1";
const QWEN_OAUTH_TOKEN_ENDPOINT: &str = "https://chat.qwen.ai/api/v1/oauth2/token";
@ -155,11 +157,31 @@ pub(crate) fn is_bailian_alias(name: &str) -> bool {
matches!(name, "bailian" | "aliyun-bailian" | "aliyun")
}
pub(crate) fn is_qwen_coding_intl_alias(name: &str) -> bool {
matches!(
name,
"qwen-coding"
| "qwen-coding-plan"
| "modelstudio"
| "qwen-coding-intl"
| "modelstudio-intl"
)
}
pub(crate) fn is_qwen_coding_cn_alias(name: &str) -> bool {
matches!(name, "qwen-coding-cn" | "modelstudio-cn")
}
pub(crate) fn is_qwen_coding_alias(name: &str) -> bool {
is_qwen_coding_intl_alias(name) || is_qwen_coding_cn_alias(name)
}
pub(crate) fn is_qwen_alias(name: &str) -> bool {
is_qwen_cn_alias(name)
|| is_qwen_intl_alias(name)
|| is_qwen_us_alias(name)
|| is_qwen_oauth_alias(name)
|| is_qwen_coding_alias(name)
}
pub(crate) fn is_zai_global_alias(name: &str) -> bool {
@ -659,7 +681,11 @@ fn moonshot_base_url(name: &str) -> Option<&'static str> {
}
fn qwen_base_url(name: &str) -> Option<&'static str> {
if is_qwen_cn_alias(name) || is_qwen_oauth_alias(name) {
if is_qwen_coding_intl_alias(name) {
Some(QWEN_CODING_INTL_BASE_URL)
} else if is_qwen_coding_cn_alias(name) {
Some(QWEN_CODING_CN_BASE_URL)
} else if is_qwen_cn_alias(name) || is_qwen_oauth_alias(name) {
Some(QWEN_CN_BASE_URL)
} else if is_qwen_intl_alias(name) {
Some(QWEN_INTL_BASE_URL)
@ -843,7 +869,11 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
if let Some(credential) = resolve_minimax_oauth_refresh_token(name) {
return Some(credential);
}
} else if name == "anthropic" || name == "openai" || name == "groq" {
} else if name == "anthropic"
|| name == "openai"
|| name == "groq"
|| is_qwen_coding_alias(name)
{
// For well-known providers, prefer provider-specific env vars over the
// global api_key override, since the global key may belong to a different
// provider (e.g. a custom: gateway). This enables multi-provider setups
@ -852,6 +882,11 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
"anthropic" => &["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
"openai" => &["OPENAI_API_KEY"],
"groq" => &["GROQ_API_KEY"],
name if is_qwen_coding_alias(name) => &[
"MODELSTUDIO_API_KEY",
"DASHSCOPE_API_KEY",
"BAILIAN_API_KEY",
],
_ => &[],
};
for env_var in env_candidates {
@ -1300,6 +1335,20 @@ fn create_provider_with_url_and_options(
true,
)
)),
name if is_qwen_coding_alias(name) => {
let base_url = if is_qwen_coding_cn_alias(name) {
QWEN_CODING_CN_BASE_URL
} else {
QWEN_CODING_INTL_BASE_URL
};
Ok(compat(OpenAiCompatibleProvider::new_with_vision(
"Qwen Coding",
base_url,
key,
AuthStyle::Bearer,
true,
)))
}
name if qwen_base_url(name).is_some() => Ok(compat(OpenAiCompatibleProvider::new_with_vision(
"Qwen",
qwen_base_url(name).expect("checked in guard"),
@ -1932,6 +1981,7 @@ pub fn list_providers() -> Vec<ProviderInfo> {
"dashscope-intl",
"qwen-us",
"dashscope-us",
"qwen-coding-plan",
"qwen-code",
"qwen-oauth",
"qwen_oauth",
@ -2404,6 +2454,7 @@ mod tests {
assert!(is_minimax_alias("minimax-portal-cn"));
assert!(is_qwen_alias("dashscope"));
assert!(is_qwen_alias("qwen-us"));
assert!(is_qwen_alias("qwen-coding-plan"));
assert!(is_qwen_alias("qwen-code"));
assert!(is_qwen_oauth_alias("qwen-code"));
assert!(is_qwen_oauth_alias("qwen_oauth"));
@ -2434,6 +2485,10 @@ mod tests {
assert_eq!(canonical_china_provider_name("minimax-cn"), Some("minimax"));
assert_eq!(canonical_china_provider_name("qwen"), Some("qwen"));
assert_eq!(canonical_china_provider_name("dashscope-us"), Some("qwen"));
assert_eq!(
canonical_china_provider_name("qwen-coding-plan"),
Some("qwen")
);
assert_eq!(canonical_china_provider_name("qwen-code"), Some("qwen"));
assert_eq!(canonical_china_provider_name("zai"), Some("zai"));
assert_eq!(canonical_china_provider_name("z.ai-cn"), Some("zai"));
@ -2473,6 +2528,14 @@ 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!(
qwen_base_url("qwen-coding"),
Some(QWEN_CODING_INTL_BASE_URL)
);
assert_eq!(
qwen_base_url("qwen-coding-cn"),
Some(QWEN_CODING_CN_BASE_URL)
);
assert_eq!(qwen_base_url("qwen-code"), Some(QWEN_CN_BASE_URL));
assert_eq!(zai_base_url("zai"), Some(ZAI_GLOBAL_BASE_URL));
@ -2684,6 +2747,7 @@ mod tests {
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());
assert!(create_provider("qwen-coding-plan", Some("key")).is_ok());
assert!(create_provider("qwen-code", Some("key")).is_ok());
assert!(create_provider("qwen-oauth", Some("key")).is_ok());
}
@ -3222,6 +3286,7 @@ mod tests {
"qwen-intl",
"qwen-cn",
"qwen-us",
"qwen-coding-plan",
"qwen-code",
"lmstudio",
"llamacpp",