From 9daa0620a6bdfc3413b7520a7f5194931e8d97da Mon Sep 17 00:00:00 2001 From: codez Date: Sun, 26 Apr 2026 02:04:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(agent):=20ordering=20fix=20in=20=5Fcopy?= =?UTF-8?q?=5Freasoning=5Fcontent=5Ffor=5Fapi=20=E2=80=94=20cross-provider?= =?UTF-8?q?=20reasoning=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: From 88b65cc82a5f7930dcb45592459cc1245bebf94a Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Sat, 25 Apr 2026 15:57:18 -0500 Subject: [PATCH 2/3] Update run_agent.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- run_agent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run_agent.py b/run_agent.py index a28d04f278..e9e1005212 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7754,10 +7754,11 @@ class AIAgent: # 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 + # provider (MiniMax, etc.) left them empty. DeepSeek returns 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") + normalized_reasoning = source_msg.get("reasoning") + has_reasoning = isinstance(normalized_reasoning, str) and bool(normalized_reasoning.strip()) needs_empty_reasoning = ( not has_reasoning and source_msg.get("tool_calls") From 5ae608152ec420d249b44542051ac27989fd33b0 Mon Sep 17 00:00:00 2001 From: codez Date: Sun, 26 Apr 2026 06:08:54 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20remove=20has=5Freasoning=20guard=20?= =?UTF-8?q?=E2=80=94=20inject=20empty=20reasoning=5Fcontent=20for=20DeepSe?= =?UTF-8?q?ek/Kimi=20tool=5Fcalls=20unconditionally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run_agent.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/run_agent.py b/run_agent.py index e9e1005212..23cb088210 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7752,16 +7752,13 @@ class AIAgent: 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 returns HTTP 400 - # if reasoning_content is absent on replay. Inject "" to satisfy the - # provider's requirement without forwarding cross-provider content. - normalized_reasoning = source_msg.get("reasoning") - has_reasoning = isinstance(normalized_reasoning, str) and bool(normalized_reasoning.strip()) + # 2. DeepSeek / Kimi thinking mode: tool-call turns that lack + # reasoning_content are "poisoned history" — a prior provider (MiniMax, + # etc.) left them empty. DeepSeek returns HTTP 400 if reasoning_content + # is absent on replay; inject "" to satisfy the provider's requirement + # without forwarding any cross-provider reasoning content. needs_empty_reasoning = ( - not has_reasoning - and source_msg.get("tool_calls") + source_msg.get("tool_calls") and ( self._needs_kimi_tool_reasoning() or self._needs_deepseek_tool_reasoning()