From 9daa0620a6bdfc3413b7520a7f5194931e8d97da Mon Sep 17 00:00:00 2001 From: codez Date: Sun, 26 Apr 2026 02:04:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20ordering=20fix=20in=20=5Fcopy=5Fr?= =?UTF-8?q?easoning=5Fcontent=5Ffor=5Fapi=20=E2=80=94=20cross-provider=20r?= =?UTF-8?q?easoning=20isolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix logic-ordering bug where normalized_reasoning promotion returns before the DeepSeek/Kimi needs_empty_reasoning guard, causing cross-provider reasoning content (MiniMax → DeepSeek) to leak into reasoning_content and trigger HTTP 400. Changes: - Reorder branching: existing reasoning_content check first - Add 'not has_reasoning' guard so poisoned histories (no reasoning) still get '' injected for DeepSeek/Kimi - Healthy same-provider reasoning promotion path unchanged Refs: #15250, #15213 --- run_agent.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/run_agent.py b/run_agent.py index 55cbe04193..a28d04f278 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7744,25 +7744,42 @@ class AIAgent: if source_msg.get("role") != "assistant": return - explicit_reasoning = source_msg.get("reasoning_content") - if isinstance(explicit_reasoning, str): - api_msg["reasoning_content"] = explicit_reasoning + # 1. Explicit reasoning_content already set — preserve it verbatim + # (includes DeepSeek/Kimi's own empty-string placeholder written at + # creation time, and any valid reasoning content from the same provider). + existing = source_msg.get("reasoning_content") + if isinstance(existing, str): + api_msg["reasoning_content"] = existing return + # 2. DeepSeek / Kimi thinking mode: tool-call turns with neither + # reasoning_content nor reasoning are "poisoned history" — a prior + # provider (MiniMax, etc.) left them empty. DeepSeek rejects HTTP 400 + # if reasoning_content is absent on replay. Inject "" to satisfy the + # provider's requirement without forwarding cross-provider content. + has_reasoning = isinstance(source_msg.get("reasoning"), str) and source_msg.get("reasoning") + needs_empty_reasoning = ( + not has_reasoning + and source_msg.get("tool_calls") + and ( + self._needs_kimi_tool_reasoning() + or self._needs_deepseek_tool_reasoning() + ) + ) + if needs_empty_reasoning: + api_msg["reasoning_content"] = "" + return + + # 3. Healthy session: promote 'reasoning' field to 'reasoning_content' + # for providers that use the internal 'reasoning' key. normalized_reasoning = source_msg.get("reasoning") if isinstance(normalized_reasoning, str) and normalized_reasoning: api_msg["reasoning_content"] = normalized_reasoning return - # Providers that require an echoed reasoning_content on every - # assistant tool-call turn. Detection logic lives in the per-provider - # helpers so both the creation path (_build_assistant_message) and - # this replay path stay in sync. - if source_msg.get("tool_calls") and ( - self._needs_kimi_tool_reasoning() - or self._needs_deepseek_tool_reasoning() - ): - api_msg["reasoning_content"] = "" + # 4. reasoning_content was present but not a string (e.g. None after + # context compaction). Don't pass null to the API. + api_msg.pop("reasoning_content", None) @staticmethod def _sanitize_tool_calls_for_strict_api(api_msg: dict) -> dict: