From 93a2d6b307674fc9975538cd7a2f903068211c95 Mon Sep 17 00:00:00 2001 From: chen1749144759 Date: Sat, 25 Apr 2026 05:55:15 +0800 Subject: [PATCH] fix: add DeepSeek reasoning_content echo for tool-call messages DeepSeek V4 thinking mode requires reasoning_content on every assistant message that includes tool_calls. When this field is missing from persisted history, replaying the session causes HTTP 400: 'The reasoning_content in the thinking mode must be passed back to the API.' Two-part fix (refs #15250): 1. _copy_reasoning_content_for_api: Merge the Kimi-only and DeepSeek detection into a single needs_tool_reasoning_echo check. This handles already-poisoned persisted sessions by injecting an empty reasoning_content on replay. 2. _build_assistant_message: Store reasoning_content='' on new DeepSeek tool-call messages at creation time, preventing future session poisoning at the source. Additional fix: 3. _handle_max_iterations: Add missing call to _copy_reasoning_content_for_api in the max-iterations flush path (previously only main loop and flush_memories had it). Detection covers: - provider == 'deepseek' - model name containing 'deepseek' (case-insensitive) - base URL matching api.deepseek.com (for custom provider) --- run_agent.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/run_agent.py b/run_agent.py index 6770f568c..3f10f36a0 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7625,6 +7625,12 @@ class AIAgent: raw_reasoning_content = getattr(assistant_message, "reasoning_content", None) if raw_reasoning_content is not None: msg["reasoning_content"] = _sanitize_surrogates(raw_reasoning_content) + elif msg.get("tool_calls") and self._needs_deepseek_tool_reasoning(): + # DeepSeek thinking mode requires reasoning_content on every + # assistant tool-call message. Without it, replaying the + # persisted message causes HTTP 400. Include empty string + # as a defensive compatibility fallback (refs #15250). + msg["reasoning_content"] = "" if hasattr(assistant_message, 'reasoning_details') and assistant_message.reasoning_details: # Pass reasoning_details back unmodified so providers (OpenRouter, @@ -7700,6 +7706,22 @@ class AIAgent: return msg + def _needs_deepseek_tool_reasoning(self) -> bool: + """Return True when the current provider is DeepSeek thinking mode. + + Used to decide whether to store reasoning_content on tool-call + assistant messages. DeepSeek V4 thinking mode requires this field + on every assistant tool-call turn; omitting it causes HTTP 400 + when the message is replayed in a subsequent API request (#15250). + """ + provider = (self.provider or "").lower() + model = (self.model or "").lower() + return ( + provider == "deepseek" + or "deepseek" in model + or base_url_host_matches(self.base_url, "api.deepseek.com") + ) + def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> None: """Copy provider-facing reasoning fields onto an API replay message.""" if source_msg.get("role") != "assistant": @@ -7715,13 +7737,17 @@ class AIAgent: api_msg["reasoning_content"] = normalized_reasoning return - kimi_requires_reasoning = ( - self.provider in {"kimi-coding", "kimi-coding-cn"} + provider = (self.provider or "").lower() + model = (self.model or "").lower() + needs_tool_reasoning_echo = ( + provider in {"kimi-coding", "kimi-coding-cn", "deepseek"} + or "deepseek" in model or base_url_host_matches(self.base_url, "api.kimi.com") or base_url_host_matches(self.base_url, "moonshot.ai") or base_url_host_matches(self.base_url, "moonshot.cn") + or base_url_host_matches(self.base_url, "api.deepseek.com") ) - if kimi_requires_reasoning and source_msg.get("tool_calls"): + if needs_tool_reasoning_echo and source_msg.get("tool_calls"): api_msg["reasoning_content"] = "" @staticmethod @@ -9095,6 +9121,7 @@ class AIAgent: api_messages = [] for msg in messages: api_msg = msg.copy() + self._copy_reasoning_content_for_api(msg, api_msg) for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"): api_msg.pop(internal_field, None) if _needs_sanitize: