diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 53276d276..8f420c540 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -192,17 +192,22 @@ impl ProxyService { for key in token_keys { env.remove(key); } - env.insert( - "ANTHROPIC_API_KEY".to_string(), - json!(PROXY_TOKEN_PLACEHOLDER), - ); + // 只注入一个认证键:两者同时存在会触发 Claude Code 的 + // "Both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY set" 警告(#4919)。 + // - Codex 系保留 AUTH_TOKEN:缺该键 Claude Code 会弹登录提示(#3784)。 + // 无条件注入而非"已存在才保留":热切换路径传入的是 provider + // settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。 + // - Copilot 仅 API_KEY:避免与 /login 管理的 key 冲突(#1049)。 if keep_auth_token { - // 无条件注入而非"已存在才保留":热切换路径传入的是 provider - // settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。 env.insert( "ANTHROPIC_AUTH_TOKEN".to_string(), json!(PROXY_TOKEN_PLACEHOLDER), ); + } else { + env.insert( + "ANTHROPIC_API_KEY".to_string(), + json!(PROXY_TOKEN_PLACEHOLDER), + ); } } } @@ -3084,7 +3089,8 @@ mod tests { assert_env_str(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", Some("gpt-5.4")); assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-8")); assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", Some("gpt-5.4")); - assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + // Codex 系只保留 AUTH_TOKEN;双键会触发 Claude Code 告警(#4919) + assert_env_str(env, "ANTHROPIC_API_KEY", None); assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); } @@ -3117,7 +3123,7 @@ mod tests { .get("env") .and_then(|value| value.as_object()) .expect("env should exist"); - assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_API_KEY", None); assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); } @@ -3149,10 +3155,49 @@ mod tests { .get("env") .and_then(|value| value.as_object()) .expect("env should exist"); - assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_API_KEY", None); assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); } + // #4919 复现场景:从第三方 Claude 供应商(live 已有 AUTH_TOKEN)切换到 + // Codex 受管供应商时,只应保留 AUTH_TOKEN 占位符,不得同时写入 API_KEY。 + #[test] + fn managed_account_claude_takeover_codex_from_third_party_keeps_single_auth_key() { + let mut provider = Provider::with_id( + "codex".to_string(), + "Codex".to_string(), + json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex" + } + }), + None, + ); + provider.meta = Some(ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + let mut live_config = json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", + "ANTHROPIC_AUTH_TOKEN": "sk-third-party" + } + }); + ProxyService::apply_claude_takeover_fields_for_provider( + &mut live_config, + "http://127.0.0.1:15721", + &provider, + ); + + let env = live_config + .get("env") + .and_then(|value| value.as_object()) + .expect("env should exist"); + assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_API_KEY", None); + } + #[test] fn managed_account_claude_takeover_copilot_removes_stale_auth_token() { let mut provider = Provider::with_id(