From 3e38889ccc97dc17ae0dbaeea283f1297138e46e Mon Sep 17 00:00:00 2001 From: Mask <5113279+maskshell@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:23:16 +0000 Subject: [PATCH] feat(proxy): strip effort params when thinking:disabled for DeepSeek endpoint (#4239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepSeek's Anthropic-compatible endpoint rejects requests where thinking.type=disabled coexists with effort parameters, returning HTTP 400. This breaks Claude Code 2.1.166+ sub-agents (Workflow/Dynamic Workflow), which hardcode thinking:disabled. Rather than overriding thinking:disabled, remove the conflicting effort parameters (output_config.effort / reasoning_effort) to respect Claude Code's intent — sub-agents don't need to display reasoning. Fixes: https://github.com/deepseek-ai/DeepSeek-V3/issues/1397 Co-authored-by: Claude Opus 4.8 (1M context) --- src-tauri/src/proxy/providers/claude.rs | 313 ++++++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 51a996386..6443f70a0 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -145,6 +145,77 @@ pub fn normalize_anthropic_tool_thinking_history_for_provider( normalize_anthropic_tool_thinking_history(body) } +/// DeepSeek official Anthropic-compatible endpoint URL +const DEEPSEEK_OFFICIAL_ANTHROPIC_URL: &str = "https://api.deepseek.com/anthropic"; + +/// Check whether the provider is configured to use DeepSeek's official +/// Anthropic-compatible endpoint. +fn is_deepseek_official_anthropic_endpoint(provider: &Provider) -> bool { + let settings = &provider.settings_config; + let base_url = 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("apiEndpoint").and_then(|v| v.as_str())); + + base_url.map(|u| u.trim_end_matches('/')) == Some(DEEPSEEK_OFFICIAL_ANTHROPIC_URL) +} + +/// DeepSeek's official Anthropic-compatible endpoint treats +/// `thinking: { type: "disabled" }` and effort parameters (`output_config.effort` +/// or `reasoning_effort`) as mutually exclusive, returning HTTP 400: +/// "thinking options type cannot be disabled when reasoning_effort is set". +/// This breaks Claude Code 2.1.166+ Workflow/Dynamic Workflow features. +/// +/// Rather than overriding Claude Code's intentional `thinking: disabled` for +/// sub-agents, we respect that decision and remove the conflicting effort +/// parameters instead. `thinking: disabled` means "don't output thinking +/// blocks", which is the correct behavior for sub-agents that don't need +/// to display reasoning to the user. +/// +/// +pub fn normalize_deepseek_thinking_disabled_strip_effort( + body: &mut Value, + provider: &Provider, +) -> bool { + if !is_deepseek_official_anthropic_endpoint(provider) { + return false; + } + + let thinking_type = body + .get("thinking") + .and_then(|t| t.get("type")) + .and_then(|t| t.as_str()); + + if thinking_type != Some("disabled") { + return false; + } + + let mut changed = false; + + // Remove output_config.effort (Anthropic format) + if let Some(oc) = body + .get_mut("output_config") + .and_then(|v| v.as_object_mut()) + { + changed |= oc.remove("effort").is_some(); + // Clean up empty output_config + if oc.is_empty() { + body.as_object_mut().unwrap().remove("output_config"); + } + } + + // Remove reasoning_effort (OpenAI format, may be present in passthrough) + if body.get("reasoning_effort").is_some() { + body.as_object_mut().unwrap().remove("reasoning_effort"); + changed = true; + } + + changed +} + pub fn normalize_anthropic_messages_for_provider( body: &mut Value, provider: &Provider, @@ -156,6 +227,7 @@ pub fn normalize_anthropic_messages_for_provider( let mut changed = normalize_anthropic_system_role_messages(body); changed |= normalize_anthropic_tool_thinking_history_for_provider(body, provider, api_format); + changed |= normalize_deepseek_thinking_disabled_strip_effort(body, provider); changed } @@ -2300,4 +2372,245 @@ mod tests { assert!(!changed); assert_eq!(body, original); } + + // ==================== normalize_deepseek_thinking_disabled_strip_effort 测试 ==================== + + fn deepseek_official_provider() -> Provider { + create_provider(json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", + "ANTHROPIC_API_KEY": "test-key" + } + })) + } + + #[test] + fn test_deepseek_official_strips_output_config_effort() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max" }, + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(changed); + assert_eq!(body["thinking"]["type"], "disabled"); + assert!(body.get("output_config").is_none()); + } + + #[test] + fn test_deepseek_official_strips_reasoning_effort() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "reasoning_effort": "high", + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(changed); + assert_eq!(body["thinking"]["type"], "disabled"); + assert!(body.get("reasoning_effort").is_none()); + } + + #[test] + fn test_deepseek_official_strips_both_effort_fields() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max" }, + "reasoning_effort": "high", + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(changed); + assert_eq!(body["thinking"]["type"], "disabled"); + assert!(body.get("output_config").is_none()); + assert!(body.get("reasoning_effort").is_none()); + } + + #[test] + fn test_deepseek_official_no_effort_no_change() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "max_tokens": 100000 + }); + let original = body.clone(); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(!changed); + assert_eq!(body, original); + } + + #[test] + fn test_deepseek_official_preserves_output_config_other_fields() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max", "temperature": 0.5 }, + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(changed); + assert_eq!(body["output_config"]["temperature"], 0.5); + assert!(body["output_config"].get("effort").is_none()); + } + + #[test] + fn test_deepseek_official_non_disabled_not_modified() { + let cases = vec![ + ( + "enabled", + json!({ "type": "enabled", "budget_tokens": 16000 }), + ), + ("adaptive", json!({ "type": "adaptive" })), + ]; + + for (label, thinking_value) in cases { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": thinking_value, + "output_config": { "effort": "max" }, + "max_tokens": 100000 + }); + let original = body.clone(); + + let changed = normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider(), + ); + + assert!(!changed, "should not modify thinking.type={label}"); + assert_eq!(body, original); + } + + // missing thinking field entirely + let mut body = json!({ + "model": "deepseek-v4-pro", + "output_config": { "effort": "max" }, + "max_tokens": 100000 + }); + let original = body.clone(); + assert!(!normalize_deepseek_thinking_disabled_strip_effort( + &mut body, + &deepseek_official_provider() + )); + assert_eq!(body, original); + } + + #[test] + fn test_deepseek_official_url_with_trailing_slash() { + let provider = create_provider(json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic/", + "ANTHROPIC_API_KEY": "test-key" + } + })); + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max" }, + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider); + + assert!(changed); + assert!(body.get("output_config").is_none()); + } + + #[test] + fn test_deepseek_official_detected_via_base_url_fallback() { + let provider = create_provider(json!({ + "base_url": "https://api.deepseek.com/anthropic", + "ANTHROPIC_API_KEY": "test-key" + })); + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "reasoning_effort": "high", + "max_tokens": 100000 + }); + + let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider); + + assert!(changed); + assert!(body.get("reasoning_effort").is_none()); + } + + #[test] + fn test_non_deepseek_endpoint_not_modified() { + let providers = vec![ + create_provider(json!({ + "env": { "ANTHROPIC_BASE_URL": "https://other-api.com/anthropic", "ANTHROPIC_API_KEY": "test-key" } + })), + create_provider(json!({ + "env": { "ANTHROPIC_BASE_URL": "https://api.anthropic.com", "ANTHROPIC_API_KEY": "test-key" } + })), + ]; + + for provider in providers { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max" }, + "max_tokens": 100000 + }); + let original = body.clone(); + + let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider); + + assert!( + !changed, + "should not modify for {}", + provider.settings_config["env"]["ANTHROPIC_BASE_URL"] + ); + assert_eq!(body, original); + } + } + + #[test] + fn test_normalize_messages_pipeline_strips_effort_for_deepseek() { + let mut body = json!({ + "model": "deepseek-v4-pro", + "thinking": { "type": "disabled" }, + "output_config": { "effort": "max" }, + "max_tokens": 100000, + "messages": [{ "role": "user", "content": "hello" }] + }); + + let changed = normalize_anthropic_messages_for_provider( + &mut body, + &deepseek_official_provider(), + "anthropic", + ); + + assert!(changed); + assert_eq!(body["thinking"]["type"], "disabled"); + assert!(body.get("output_config").is_none()); + } }