From 065d5db843c4108c8f8e4d9d84b27b72e4be7e28 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 30 Jan 2026 12:03:37 +0800 Subject: [PATCH] fix(proxy): improve URL building and remove redundant model mapping - Add model parameter to request logs for better debugging - Fix duplicate /v1/v1 in URL when both base_url and endpoint have version - Extend ?beta=true parameter to /v1/chat/completions endpoint - Remove model mapping from transform layer (now handled by model_mapper) - Add DeepSeek max_tokens clamping (1-8192 range) --- src-tauri/src/proxy/forwarder.rs | 6 +- src-tauri/src/proxy/providers/claude.rs | 14 +- src-tauri/src/proxy/providers/transform.rs | 169 +++++++-------------- 3 files changed, 67 insertions(+), 122 deletions(-) diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 8924d9b2f..2c741b8a6 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -666,7 +666,11 @@ impl RequestForwarder { // 输出请求信息日志 let tag = adapter.name(); - log::info!("[{tag}] >>> 请求 URL: {url}"); + let request_model = filtered_body + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or(""); + log::info!("[{tag}] >>> 请求 URL: {url} (model={request_model})"); if let Ok(body_str) = serde_json::to_string(&filtered_body) { log::debug!( "[{tag}] >>> 请求体内容 ({}字节): {}", diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 195b2be02..59d0da43f 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -252,15 +252,23 @@ impl ProviderAdapter for ClaudeAdapter { // 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。 // 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。 - let base = format!( + let mut base = format!( "{}/{}", base_url.trim_end_matches('/'), endpoint.trim_start_matches('/') ); - // 为 /v1/messages 端点添加 ?beta=true 参数 + // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致) + while base.contains("/v1/v1") { + base = base.replace("/v1/v1", "/v1"); + } + + // 为 Claude 相关端点添加 ?beta=true 参数 // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 - if endpoint.contains("/v1/messages") && !endpoint.contains("?") { + // 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致 + if (endpoint.contains("/v1/messages") || endpoint.contains("/v1/chat/completions")) + && !endpoint.contains('?') + { format!("{base}?beta=true") } else { base diff --git a/src-tauri/src/proxy/providers/transform.rs b/src-tauri/src/proxy/providers/transform.rs index 1efbb0956..a73d77f28 100644 --- a/src-tauri/src/proxy/providers/transform.rs +++ b/src-tauri/src/proxy/providers/transform.rs @@ -7,73 +7,26 @@ use crate::provider::Provider; use crate::proxy::error::ProxyError; use serde_json::{json, Value}; -/// 从 Provider 配置中获取模型映射 -fn get_model_from_provider(model: &str, provider: &Provider, body: &Value) -> String { - let env = provider.settings_config.get("env"); - let model_lower = model.to_lowercase(); +fn extract_base_url_from_provider(provider: &Provider) -> Option<&str> { + let settings = &provider.settings_config; - // 检测 thinking 参数 - let has_thinking = body - .get("thinking") - .and_then(|v| v.as_object()) - .and_then(|o| o.get("type")) - .and_then(|t| t.as_str()) - == Some("enabled"); - - if let Some(env) = env { - // 如果启用 thinking,优先使用推理模型 - if has_thinking { - if let Some(m) = env - .get("ANTHROPIC_REASONING_MODEL") - .and_then(|v| v.as_str()) - { - log::debug!("[Transform] 使用推理模型: {m}"); - return m.to_string(); - } - } - - // 根据模型类型选择配置模型 - if model_lower.contains("haiku") { - if let Some(m) = env - .get("ANTHROPIC_DEFAULT_HAIKU_MODEL") - .and_then(|v| v.as_str()) - { - return m.to_string(); - } - } - if model_lower.contains("opus") { - if let Some(m) = env - .get("ANTHROPIC_DEFAULT_OPUS_MODEL") - .and_then(|v| v.as_str()) - { - return m.to_string(); - } - } - if model_lower.contains("sonnet") { - if let Some(m) = env - .get("ANTHROPIC_DEFAULT_SONNET_MODEL") - .and_then(|v| v.as_str()) - { - return m.to_string(); - } - } - // 默认使用 ANTHROPIC_MODEL - if let Some(m) = env.get("ANTHROPIC_MODEL").and_then(|v| v.as_str()) { - return m.to_string(); - } - } - - model.to_string() + settings + .get("env") + .and_then(|env| env.get("ANTHROPIC_BASE_URL")) + .and_then(|v| v.as_str()) + .or_else(|| settings.get("base_url").and_then(|v| v.as_str())) + .or_else(|| settings.get("baseURL").and_then(|v| v.as_str())) + .or_else(|| settings.get("apiBaseUrl").and_then(|v| v.as_str())) + .or_else(|| settings.get("apiEndpoint").and_then(|v| v.as_str())) } /// Anthropic 请求 → OpenAI 请求 pub fn anthropic_to_openai(body: Value, provider: &Provider) -> Result { let mut result = json!({}); - // 模型映射:使用 Provider 配置中的模型(支持 thinking 参数) + // NOTE: 模型映射由上游统一处理(proxy::model_mapper),格式转换层只做结构转换。 if let Some(model) = body.get("model").and_then(|m| m.as_str()) { - let mapped_model = get_model_from_provider(model, provider, &body); - result["model"] = json!(mapped_model); + result["model"] = json!(model); } let mut messages = Vec::new(); @@ -107,7 +60,30 @@ pub fn anthropic_to_openai(body: Value, provider: &Provider) -> Result { + let clamped = max_tokens.clamp(DEEPSEEK_MAX_TOKENS_MIN, DEEPSEEK_MAX_TOKENS_MAX); + if clamped != max_tokens { + log::warn!( + "[Transform] DeepSeek max_tokens 超出范围,已自动调整: {max_tokens} → {clamped}" + ); + } + result["max_tokens"] = json!(clamped); + } + _ => { + // 非 DeepSeek / 非数字类型:保持原样,让上游自行处理 + result["max_tokens"] = v.clone(); + } + } } if let Some(v) = body.get("temperature") { result["temperature"] = v.clone(); @@ -418,8 +394,7 @@ mod tests { }); let result = anthropic_to_openai(input, &provider).unwrap(); - // opus 模型映射到配置的 ANTHROPIC_DEFAULT_OPUS_MODEL - assert_eq!(result["model"], "anthropic/claude-opus-4.5"); + assert_eq!(result["model"], "claude-3-opus"); assert_eq!(result["max_tokens"], 1024); assert_eq!(result["messages"][0]["role"], "user"); assert_eq!(result["messages"][0]["content"], "Hello"); @@ -564,77 +539,35 @@ mod tests { #[test] fn test_model_mapping_from_provider() { - let provider = create_openrouter_provider(); - let body = json!({"model": "test"}); + let provider = create_provider(json!({ + "ANTHROPIC_MODEL": "gpt-4o-mini", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-4o" + })); - // sonnet 模型 - assert_eq!( - get_model_from_provider("claude-sonnet-4-5-20250929", &provider, &body), - "anthropic/claude-sonnet-4.5" - ); - - // haiku 模型 - assert_eq!( - get_model_from_provider("claude-haiku-4-5-20250929", &provider, &body), - "anthropic/claude-haiku-4.5" - ); - - // opus 模型 - assert_eq!( - get_model_from_provider("claude-opus-4-5", &provider, &body), - "anthropic/claude-opus-4.5" - ); - } - - #[test] - fn test_anthropic_to_openai_model_mapping() { - let provider = create_openrouter_provider(); + // 回归:格式转换层不能再二次做模型映射,否则会把已映射的 model 覆盖成默认模型 let input = json!({ - "model": "claude-sonnet-4-5-20250929", + "model": "gpt-4o", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}] }); let result = anthropic_to_openai(input, &provider).unwrap(); - assert_eq!(result["model"], "anthropic/claude-sonnet-4.5"); + assert_eq!(result["model"], "gpt-4o"); } #[test] - fn test_thinking_parameter_detection() { - let mut provider = create_openrouter_provider(); - // 添加推理模型配置 - if let Some(env) = provider.settings_config.get_mut("env") { - env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended"); - } + fn test_deepseek_max_tokens_clamp() { + let provider = create_provider(json!({ + "ANTHROPIC_BASE_URL": "https://api.deepseek.com/v1", + })); let input = json!({ - "model": "claude-sonnet-4-5", - "max_tokens": 1024, - "thinking": {"type": "enabled"}, - "messages": [{"role": "user", "content": "Solve this problem"}] - }); - - let result = anthropic_to_openai(input, &provider).unwrap(); - // 应该使用推理模型 - assert_eq!(result["model"], "anthropic/claude-sonnet-4.5:extended"); - } - - #[test] - fn test_thinking_parameter_disabled() { - let mut provider = create_openrouter_provider(); - if let Some(env) = provider.settings_config.get_mut("env") { - env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended"); - } - - let input = json!({ - "model": "claude-sonnet-4-5", - "max_tokens": 1024, - "thinking": {"type": "disabled"}, + "model": "deepseek-chat", + "max_tokens": 20000, "messages": [{"role": "user", "content": "Hello"}] }); let result = anthropic_to_openai(input, &provider).unwrap(); - // 应该使用普通模型 - assert_eq!(result["model"], "anthropic/claude-sonnet-4.5"); + assert_eq!(result["max_tokens"], 8192); } }