feat(provider): add qwen-code oauth credential support

This commit is contained in:
Chummy 2026-02-19 18:04:30 +08:00
parent e9c280324f
commit bca58acdcb
3 changed files with 501 additions and 6 deletions

View file

@ -12,7 +12,8 @@ use crate::memory::{
};
use crate::providers::{
canonical_china_provider_name, is_glm_alias, is_glm_cn_alias, is_minimax_alias,
is_moonshot_alias, is_qianfan_alias, is_qwen_alias, is_zai_alias, is_zai_cn_alias,
is_moonshot_alias, is_qianfan_alias, is_qwen_alias, is_qwen_oauth_alias, is_zai_alias,
is_zai_cn_alias,
};
use anyhow::{bail, Context, Result};
use console::style;
@ -497,6 +498,10 @@ pub async fn run_quick_setup(
}
fn canonical_provider_name(provider_name: &str) -> &str {
if is_qwen_oauth_alias(provider_name) {
return "qwen-code";
}
if let Some(canonical) = canonical_china_provider_name(provider_name) {
return canonical;
}
@ -547,6 +552,7 @@ fn default_model_for_provider(provider: &str) -> String {
"glm" | "zai" => "glm-5".into(),
"minimax" => "MiniMax-M2.5".into(),
"qwen" => "qwen-plus".into(),
"qwen-code" => "qwen3-coder-plus".into(),
"ollama" => "llama3.2".into(),
"gemini" => "gemini-2.5-pro".into(),
"kimi-code" => "kimi-for-coding".into(),
@ -823,6 +829,20 @@ fn curated_models_for_provider(provider_name: &str) -> Vec<(String, String)> {
"Qwen Turbo (fast and cost-efficient)".to_string(),
),
],
"qwen-code" => 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(),
),
],
"nvidia" => vec![
(
"meta/llama-3.3-70b-instruct".to_string(),
@ -1601,6 +1621,10 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio
"kimi-code",
"Kimi Code — coding-optimized Kimi API (KimiCLI)",
),
(
"qwen-code",
"Qwen Code — OAuth tokens reused from ~/.qwen/oauth_creds.json",
),
("moonshot", "Moonshot — Kimi API (China endpoint)"),
(
"moonshot-intl",
@ -1809,11 +1833,48 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio
key
}
} else if canonical_provider_name(provider_name) == "qwen-code" {
if std::env::var("QWEN_OAUTH_TOKEN").is_ok() {
print_bullet(&format!(
"{} QWEN_OAUTH_TOKEN environment variable detected!",
style("").green().bold()
));
"qwen-oauth".to_string()
} else {
print_bullet(
"Qwen Code OAuth credentials are usually stored in ~/.qwen/oauth_creds.json.",
);
print_bullet(
"Run `qwen` once and complete OAuth login to populate cached credentials.",
);
print_bullet("You can also set QWEN_OAUTH_TOKEN directly.");
println!();
let key: String = Input::new()
.with_prompt(
" Paste your Qwen OAuth token (or press Enter to auto-detect cached OAuth)",
)
.allow_empty(true)
.interact_text()?;
if key.trim().is_empty() {
print_bullet(&format!(
"Using OAuth auto-detection. Set {} and optional {} if needed.",
style("QWEN_OAUTH_TOKEN").yellow(),
style("QWEN_OAUTH_RESOURCE_URL").yellow()
));
"qwen-oauth".to_string()
} else {
key
}
}
} else {
let key_url = if is_moonshot_alias(provider_name)
|| canonical_provider_name(provider_name) == "kimi-code"
{
"https://platform.moonshot.cn/console/api-keys"
} else if canonical_provider_name(provider_name) == "qwen-code" {
"https://qwen.readthedocs.io/en/latest/getting_started/installation.html"
} else if is_glm_cn_alias(provider_name) || is_zai_cn_alias(provider_name) {
"https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys"
} else if is_glm_alias(provider_name) || is_zai_alias(provider_name) {
@ -2066,6 +2127,10 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio
/// Map provider name to its conventional env var
fn provider_env_var(name: &str) -> &'static str {
if canonical_provider_name(name) == "qwen-code" {
return "QWEN_OAUTH_TOKEN";
}
match canonical_provider_name(name) {
"openrouter" => "OPENROUTER_API_KEY",
"anthropic" => "ANTHROPIC_API_KEY",
@ -5044,6 +5109,7 @@ 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-code"), "qwen3-coder-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");
@ -5078,6 +5144,8 @@ 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-code"), "qwen-code");
assert_eq!(canonical_provider_name("qwen-oauth"), "qwen-code");
assert_eq!(canonical_provider_name("moonshot-intl"), "moonshot");
assert_eq!(canonical_provider_name("kimi-cn"), "moonshot");
assert_eq!(canonical_provider_name("kimi_coding"), "kimi-code");
@ -5188,6 +5256,18 @@ mod tests {
assert!(ids.contains(&"kimi-k2.5".to_string()));
}
#[test]
fn curated_models_for_qwen_code_include_coding_plan_models() {
let ids: Vec<String> = curated_models_for_provider("qwen-code")
.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"));
@ -5459,6 +5539,8 @@ 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-code"), "QWEN_OAUTH_TOKEN");
assert_eq!(provider_env_var("qwen-oauth"), "QWEN_OAUTH_TOKEN");
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("kimi-code"), "KIMI_CODE_API_KEY");