diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 2c741b8a6..6ea8e91b5 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -561,15 +561,17 @@ impl RequestForwarder { // 检查是否需要格式转换 let needs_transform = adapter.needs_transform(provider); - let effective_endpoint = - if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" { - "/v1/chat/completions" - } else { - endpoint - }; + let effective_endpoint = if needs_transform + && adapter.name() == "Claude" + && endpoint.starts_with("/v1/messages") + { + endpoint.replacen("/v1/messages", "/v1/chat/completions", 1) + } else { + endpoint.to_string() + }; // 使用适配器构建 URL - let url = adapter.build_url(&base_url, effective_endpoint); + let url = adapter.build_url(&base_url, &effective_endpoint); // 应用模型映射(独立于格式转换) let (mapped_body, _original_model, _mapped_model) = diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 644ef353c..a3e83e340 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -56,6 +56,7 @@ pub async fn get_status(State(state): State) -> Result, + uri: axum::http::Uri, headers: axum::http::HeaderMap, Json(body): Json, ) -> Result { @@ -67,12 +68,18 @@ pub async fn handle_messages( .and_then(|s| s.as_bool()) .unwrap_or(false); + // 透传客户端 query 参数(例如 ?beta=true),路径统一为 /v1/messages + let endpoint = uri + .query() + .map(|q| format!("/v1/messages?{q}")) + .unwrap_or_else(|| "/v1/messages".to_string()); + // 转发请求 let forwarder = ctx.create_forwarder(&state); let result = match forwarder .forward_with_retry( &AppType::Claude, - "/v1/messages", + &endpoint, body.clone(), headers, ctx.get_providers(), diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 6d43c4687..397b5a45b 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -280,17 +280,7 @@ impl ProviderAdapter for ClaudeAdapter { // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致) let base = dedup_v1_v1_boundary_safe(base); - // 为 Claude Messages 端点添加 ?beta=true 参数 - // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 - // 仅对 /v1/messages 生效,不对 /v1/chat/completions 生效 - let needs_beta = - base.contains("/v1/messages") && !base.contains('?') && !suffix.starts_with('?'); - - if needs_beta { - format!("{base}?beta=true{suffix}") - } else { - format!("{base}{suffix}") - } + format!("{base}{suffix}") } fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder { @@ -502,23 +492,21 @@ mod tests { #[test] fn test_build_url_anthropic() { let adapter = ClaudeAdapter::new(); - // /v1/messages 端点会自动添加 ?beta=true 参数 let url = adapter.build_url("https://api.anthropic.com", "/v1/messages"); - assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true"); + assert_eq!(url, "https://api.anthropic.com/v1/messages"); } #[test] fn test_build_url_openrouter() { let adapter = ClaudeAdapter::new(); - // /v1/messages 端点会自动添加 ?beta=true 参数 let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages"); - assert_eq!(url, "https://openrouter.ai/api/v1/messages?beta=true"); + assert_eq!(url, "https://openrouter.ai/api/v1/messages"); } #[test] fn test_build_url_no_beta_for_other_endpoints() { let adapter = ClaudeAdapter::new(); - // 非 /v1/messages 端点不添加 ?beta=true + // 非 /v1/messages 端点保持原样 let url = adapter.build_url("https://api.anthropic.com", "/v1/complete"); assert_eq!(url, "https://api.anthropic.com/v1/complete"); } @@ -536,7 +524,7 @@ mod tests { let adapter = ClaudeAdapter::new(); // base_url 已包含完整路径 /v1/messages,不再追加 let url = adapter.build_url("https://example.com/api/v1/messages", "/v1/messages"); - assert_eq!(url, "https://example.com/api/v1/messages?beta=true"); + assert_eq!(url, "https://example.com/api/v1/messages"); } #[test] @@ -574,14 +562,11 @@ mod tests { "https://integrate.api.nvidia.com/v1", "/v1/chat/completions", ); - assert_eq!( - url, - "https://integrate.api.nvidia.com/v1/chat/completions" - ); + assert_eq!(url, "https://integrate.api.nvidia.com/v1/chat/completions"); // 另一个场景:/v1 + /v1/messages let url2 = adapter.build_url("https://api.example.com/v1", "/v1/messages"); - assert_eq!(url2, "https://api.example.com/v1/messages?beta=true"); + assert_eq!(url2, "https://api.example.com/v1/messages"); } #[test] diff --git a/src-tauri/src/proxy/url_builder.rs b/src-tauri/src/proxy/url_builder.rs index 42ff13ff3..5bfa578df 100644 --- a/src-tauri/src/proxy/url_builder.rs +++ b/src-tauri/src/proxy/url_builder.rs @@ -144,7 +144,7 @@ fn build_runtime_like_url( is_proxy: bool, ) -> String { match app_type { - // Claude 代理预览需要展示运行时附加参数(如 ?beta=true) + // Claude 代理预览需要展示与运行时一致的 URL 归一化结果 AppType::Claude if is_proxy => ClaudeAdapter::new().build_url(base_url, endpoint), // Codex/OpenCode 预览复用运行时 /v1 归一化规则 AppType::Codex | AppType::OpenCode => CodexAdapter::new().build_url(base_url, endpoint), @@ -226,7 +226,7 @@ pub fn check_proxy_requirement( } } - // 如果直连地址和代理地址路径不同,需要代理(忽略查询参数差异,如 Claude ?beta=true) + // 如果直连地址和代理地址路径不同,需要代理(忽略查询参数差异) let (direct_base, _) = split_url_suffix(&preview.direct_url); let (proxy_base, _) = split_url_suffix(&preview.proxy_url); if direct_base != proxy_base { @@ -248,10 +248,7 @@ mod tests { Some("anthropic"), ); assert_eq!(preview.direct_url, "https://api.example.com/v1/messages"); - assert_eq!( - preview.proxy_url, - "https://api.example.com/v1/messages?beta=true" - ); + assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages"); assert!(!preview.is_full_url); } @@ -282,10 +279,7 @@ mod tests { preview.direct_url, "https://api.example.com/v1/messages/v1/messages" ); - assert_eq!( - preview.proxy_url, - "https://api.example.com/v1/messages?beta=true" - ); + assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages"); assert!(preview.is_full_url); } @@ -397,11 +391,8 @@ mod tests { Some("anthropic"), ); assert_eq!(preview.direct_url, "https://api.example.com/v1/v1/messages"); - // 代理地址按运行时规则构建,会去重并附加 ?beta=true - assert_eq!( - preview.proxy_url, - "https://api.example.com/v1/messages?beta=true" - ); + // 代理地址按运行时规则构建,会进行路径去重 + assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages"); } #[test] @@ -431,7 +422,7 @@ mod tests { ); assert_eq!( preview.proxy_url, - "https://api.example.com/v1/messages?beta=true#frag" + "https://api.example.com/v1/messages#frag" ); assert!(preview.is_full_url); } diff --git a/src-tauri/src/services/stream_check.rs b/src-tauri/src/services/stream_check.rs index c7afaa55e..ce5040abe 100644 --- a/src-tauri/src/services/stream_check.rs +++ b/src-tauri/src/services/stream_check.rs @@ -285,11 +285,11 @@ impl StreamCheckService { timeout: std::time::Duration, ) -> Result<(u16, String), AppError> { let base = base_url.trim_end_matches('/'); - // URL 必须包含 ?beta=true 参数(某些中转服务依赖此参数验证请求来源) + // 健康检查不强制附加查询参数,保持与默认 Claude 路径一致 let url = if base.ends_with("/v1") { - format!("{base}/messages?beta=true") + format!("{base}/messages") } else { - format!("{base}/v1/messages?beta=true") + format!("{base}/v1/messages") }; let body = json!({