diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 30380edc4..a7849bce1 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -398,6 +398,33 @@ impl ClaudeAdapter { log::warn!("[Claude] 未找到有效的 API Key"); None } + + /// 根据 env 中填写的变量名推断 Anthropic 默认走哪种鉴权策略。 + /// + /// 与 Anthropic SDK 原生语义保持一致: + /// - `ANTHROPIC_AUTH_TOKEN` → `ClaudeAuth`(发送 `Authorization: Bearer`) + /// - `ANTHROPIC_API_KEY` → `Anthropic` (发送 `x-api-key`) + /// + /// 优先级与 [`extract_key`] 一致;两者都缺时返回 `None` 由调用方决定 fallback。 + fn infer_anthropic_auth_strategy(&self, provider: &Provider) -> Option { + let env = provider.settings_config.get("env")?; + + let has_value = |key: &str| -> bool { + env.get(key) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .is_some() + }; + + if has_value("ANTHROPIC_AUTH_TOKEN") { + return Some(AuthStrategy::ClaudeAuth); + } + if has_value("ANTHROPIC_API_KEY") { + return Some(AuthStrategy::Anthropic); + } + None + } } impl Default for ClaudeAdapter { @@ -511,7 +538,16 @@ impl ProviderAdapter for ClaudeAdapter { ProviderType::Gemini => Some(AuthInfo::new(key, AuthStrategy::Google)), ProviderType::OpenRouter => Some(AuthInfo::new(key, AuthStrategy::Bearer)), ProviderType::ClaudeAuth => Some(AuthInfo::new(key, AuthStrategy::ClaudeAuth)), - _ => Some(AuthInfo::new(key, AuthStrategy::Anthropic)), + _ => { + // 按 env 中的变量名推断鉴权策略,对齐 Anthropic SDK 语义: + // ANTHROPIC_AUTH_TOKEN → Authorization: Bearer + // ANTHROPIC_API_KEY → x-api-key + // 其他来源(apiKey 直填等)默认走 x-api-key(Anthropic 官方协议)。 + let strategy = self + .infer_anthropic_auth_strategy(provider) + .unwrap_or(AuthStrategy::Anthropic); + Some(AuthInfo::new(key, strategy)) + } } } @@ -548,7 +584,13 @@ impl ProviderAdapter for ClaudeAdapter { // 注意:anthropic-version 由 forwarder.rs 统一处理(透传客户端值或设置默认值) let bearer = format!("Bearer {}", auth.api_key); match auth.strategy { - AuthStrategy::Anthropic | AuthStrategy::ClaudeAuth | AuthStrategy::Bearer => { + AuthStrategy::Anthropic => { + vec![( + HeaderName::from_static("x-api-key"), + HeaderValue::from_str(&auth.api_key).unwrap(), + )] + } + AuthStrategy::ClaudeAuth | AuthStrategy::Bearer => { vec![( HeaderName::from_static("authorization"), HeaderValue::from_str(&bearer).unwrap(), @@ -749,7 +791,9 @@ mod tests { } #[test] - fn test_extract_auth_anthropic() { + fn test_extract_auth_anthropic_auth_token_uses_claude_auth_strategy() { + // ANTHROPIC_AUTH_TOKEN 在 Anthropic SDK 里语义就是 Authorization: Bearer, + // 因此走 ClaudeAuth strategy 而不是 Anthropic(x-api-key)。 let adapter = ClaudeAdapter::new(); let provider = create_provider(json!({ "env": { @@ -760,7 +804,7 @@ mod tests { let auth = adapter.extract_auth(&provider).unwrap(); assert_eq!(auth.api_key, "sk-ant-test-key"); - assert_eq!(auth.strategy, AuthStrategy::Anthropic); + assert_eq!(auth.strategy, AuthStrategy::ClaudeAuth); } #[test] @@ -778,6 +822,73 @@ mod tests { assert_eq!(auth.strategy, AuthStrategy::Anthropic); } + #[test] + fn test_extract_auth_both_env_vars_prefer_auth_token() { + // 两个变量都填时,extract_key 选 AUTH_TOKEN,strategy 推断也必须保持一致。 + let adapter = ClaudeAdapter::new(); + let provider = create_provider(json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com", + "ANTHROPIC_AUTH_TOKEN": "sk-from-auth-token", + "ANTHROPIC_API_KEY": "sk-from-api-key" + } + })); + + let auth = adapter.extract_auth(&provider).unwrap(); + assert_eq!(auth.api_key, "sk-from-auth-token"); + assert_eq!(auth.strategy, AuthStrategy::ClaudeAuth); + } + + #[test] + fn test_extract_auth_apikey_field_fallback_uses_anthropic_strategy() { + // 当用户没填任一 ANTHROPIC_* env,而是直接使用 apiKey 字段时, + // 视为没有显式语义偏好,默认走 Anthropic 官方协议(x-api-key)。 + let adapter = ClaudeAdapter::new(); + let provider = create_provider(json!({ + "apiKey": "sk-direct", + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com" + } + })); + + let auth = adapter.extract_auth(&provider).unwrap(); + assert_eq!(auth.api_key, "sk-direct"); + assert_eq!(auth.strategy, AuthStrategy::Anthropic); + } + + #[test] + fn test_get_auth_headers_anthropic_emits_x_api_key() { + let adapter = ClaudeAdapter::new(); + let auth = AuthInfo::new("sk-ant-test".to_string(), AuthStrategy::Anthropic); + + let headers = adapter.get_auth_headers(&auth); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].0.as_str(), "x-api-key"); + assert_eq!(headers[0].1.to_str().unwrap(), "sk-ant-test"); + } + + #[test] + fn test_get_auth_headers_claude_auth_emits_authorization_bearer() { + let adapter = ClaudeAdapter::new(); + let auth = AuthInfo::new("sk-relay-test".to_string(), AuthStrategy::ClaudeAuth); + + let headers = adapter.get_auth_headers(&auth); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].0.as_str(), "authorization"); + assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-relay-test"); + } + + #[test] + fn test_get_auth_headers_bearer_emits_authorization_bearer() { + let adapter = ClaudeAdapter::new(); + let auth = AuthInfo::new("sk-or-test".to_string(), AuthStrategy::Bearer); + + let headers = adapter.get_auth_headers(&auth); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].0.as_str(), "authorization"); + assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-or-test"); + } + #[test] fn test_extract_auth_openrouter() { let adapter = ClaudeAdapter::new(); diff --git a/src-tauri/src/services/stream_check.rs b/src-tauri/src/services/stream_check.rs index d122e6f57..7b87a01fa 100644 --- a/src-tauri/src/services/stream_check.rs +++ b/src-tauri/src/services/stream_check.rs @@ -16,7 +16,9 @@ use crate::proxy::providers::copilot_auth; use crate::proxy::providers::transform::anthropic_to_openai; use crate::proxy::providers::transform_gemini::anthropic_to_gemini; use crate::proxy::providers::transform_responses::anthropic_to_responses; -use crate::proxy::providers::{get_adapter, AuthInfo, AuthStrategy}; +use crate::proxy::providers::{ + get_adapter, AuthInfo, AuthStrategy, ClaudeAdapter, ProviderAdapter, +}; /// 健康状态枚举 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -433,12 +435,13 @@ impl StreamCheckService { let os_name = Self::get_os_name(); let arch_name = Self::get_arch_name(); - request_builder = - request_builder.header("authorization", format!("Bearer {}", auth.api_key)); - - // Only Anthropic official strategy adds x-api-key - if auth.strategy == AuthStrategy::Anthropic { - request_builder = request_builder.header("x-api-key", &auth.api_key); + // 鉴权头复用 ClaudeAdapter::get_auth_headers,与代理路径(forwarder)保持单一真理来源。 + // - AuthStrategy::Anthropic → x-api-key + // - AuthStrategy::ClaudeAuth → Authorization: Bearer + // - AuthStrategy::Bearer → Authorization: Bearer + // 避免之前"无条件 Bearer + 条件 x-api-key 双发"导致的假阴性 / auth conflict。 + for (name, value) in ClaudeAdapter::new().get_auth_headers(auth) { + request_builder = request_builder.header(name, value); } request_builder = request_builder