Skip to content

[Bug] _copy_reasoning_content_for_api: cross-provider reasoning promotion leaks stale content to DeepSeek/Kimi #15748

@Zjianru

Description

@Zjianru

Bug: Cross-provider reasoning promotion leaks stale content to DeepSeek/Kimi thinking mode

Background

DeepSeek V4 and Kimi/Moonshot thinking modes require reasoning_content="" on every assistant tool-call message. If the field is missing on replay, the API returns HTTP 400:

The reasoning_content in the thinking mode must be passed back to the API.

When a session switches providers mid-conversation (e.g. MiniMax → DeepSeek), the internal reasoning field from the prior provider can leak into the reasoning_content field sent to the new provider, causing HTTP 400.

Root Cause

In _copy_reasoning_content_for_api (run_agent.py), the normalized_reasoning promotion path returns before the DeepSeek/Kimi empty-string guard can execute:

# Current (buggy) ordering in origin/main:
explicit_reasoning = source_msg.get("reasoning_content")
if isinstance(explicit_reasoning, str):        # False when switching from MiniMax
    api_msg["reasoning_content"] = explicit_reasoning
    return

normalized_reasoning = source_msg.get("reasoning")  # "MiniMax chain of thought..."
if isinstance(normalized_reasoning, str) and normalized_reasoning:
    api_msg["reasoning_content"] = normalized_reasoning  # ← LEAKED
    return                                            # ← DeepSeek/Kimi guard never reached

# This guard is unreachable for cross-provider histories
if source_msg.get("tool_calls") and (self._needs_kimi...() or self._needs_deepseek...()):
    api_msg["reasoning_content"] = ""

When the session history contains a tool-call message from MiniMax with reasoning="MiniMax thinking..." and no reasoning_content, switching to DeepSeek causes:

  1. reasoning_content key absent → first check returns False
  2. reasoning key present → promoted to reasoning_content
  3. DeepSeek receives MiniMax's reasoning → HTTP 400

Fix

Reorder the logic so that:

  1. reasoning_content already set → preserve verbatim (including DeepSeek's own "" placeholder written at creation time)
  2. Tool-call turns with neither reasoning_content nor reasoning → inject "" for DeepSeek/Kimi (poisoned history from prior provider)
  3. reasoning present → promote to reasoning_content (healthy same-provider path)

The key addition is not has_reasoning in the guard, which distinguishes "this provider has reasoning to promote" from "a prior provider left reasoning that should not be forwarded".

Affected Scenarios

Scenario Before fix After fix
DeepSeek tool_call with reasoning_content="" set preserved preserved
DeepSeek tool_call with reasoning="DeepSeek reasoning" promoted promoted
DeepSeek tool_call after MiniMax (MiniMax reasoning present) MiniMax content sent → 400 "" injected
DeepSeek tool_call after MiniMax (both fields absent) "" injected "" injected

Related Issues

Test Coverage

All 21 existing tests in tests/run_agent/test_deepseek_reasoning_content_echo.py pass with this fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/agentCore agent loop, run_agent.py, prompt builderprovider/deepseekDeepSeek APIprovider/kimiKimi / Moonshottype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions