Merge 389fbd0ce5 into feaca20582
This commit is contained in:
commit
3bb831f477
@ -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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user