Skip to content

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation#15749

Merged
OutThisLife merged 3 commits into
NousResearch:mainfrom
Zjianru:fix/copy-reasoning-content-ordering-and-cross-provider-isolation
Apr 25, 2026
Merged

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation#15749
OutThisLife merged 3 commits into
NousResearch:mainfrom
Zjianru:fix/copy-reasoning-content-ordering-and-cross-provider-isolation

Conversation

@Zjianru

@Zjianru Zjianru commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation

What this fixes

Fixes a logic-ordering bug in _copy_reasoning_content_for_api (run_agent.py) that allows reasoning content from a prior provider to be promoted to reasoning_content and sent to DeepSeek/Kimi thinking mode, causing HTTP 400.

The bug

The normalized_reasoning promotion path (reasoningreasoning_content) returns before the DeepSeek/Kimi empty-string guard can run. For cross-provider histories, the reasoning field from MiniMax (or any other provider) gets forwarded to DeepSeek as reasoning_content, which DeepSeek rejects.

Current flow (buggy):

explicit_reasoning = source_msg.get("reasoning_content")  # None
→ first check returns False

normalized_reasoning = source_msg.get("reasoning")       # "MiniMax..."
→ promoted to reasoning_content
→ return   ← guard never reached

# unreachable:
if tool_calls and (kimi or deepseek):
    reasoning_content = ""

The fix

Reorder to 3-way branching:

# 1. reasoning_content already set — preserve verbatim
#    (covers DeepSeek's own "" placeholder + any valid content)
existing = source_msg.get("reasoning_content")
if isinstance(existing, str):
    api_msg["reasoning_content"] = existing
    return

# 2. Cross-provider poisoned history: neither reasoning_content nor reasoning.
#    Inject "" to satisfy DeepSeek/Kimi without forwarding stale content.
has_reasoning = isinstance(source_msg.get("reasoning"), str) and source_msg.get("reasoning")
needs_empty_reasoning = (
    not has_reasoning                              # ← key addition
    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 same-provider path: promote reasoning to reasoning_content
normalized_reasoning = source_msg.get("reasoning")
if isinstance(normalized_reasoning, str) and normalized_reasoning:
    api_msg["reasoning_content"] = normalized_reasoning
    return

The not has_reasoning guard distinguishes "this provider's own reasoning to promote" from "a prior provider's reasoning that must not be forwarded".

Test results

tests/run_agent/test_deepseek_reasoning_content_echo.py
21 passed, 10 warnings

Related

…rovider reasoning isolation

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: NousResearch#15250, NousResearch#15213
@Zjianru

Zjianru commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 @OutThisLife @0xbyt4 @kshitijk4poor — this PR fixes a critical logic-ordering bug in _copy_reasoning_content_for_api that causes HTTP 400 errors on DeepSeek/Kimi when reasoning content from a prior provider (e.g. MiniMax) gets forwarded across provider switches. This is a high-impact production issue affecting any multi-provider agent workflow. All 21 regression tests pass. Please review when available. Related: #15250, #15213.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent loop, run_agent.py, prompt builder provider/deepseek DeepSeek API provider/kimi Kimi / Moonshot labels Apr 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an ordering issue in run_agent.py::_copy_reasoning_content_for_api so DeepSeek/Kimi tool-call replay messages reliably include a provider-compatible reasoning_content placeholder (empty string) when the stored history lacks both reasoning_content and reasoning, avoiding DeepSeek thinking-mode 400s on replay.

Changes:

  • Reorders _copy_reasoning_content_for_api into explicit branches: preserve existing reasoning_content, inject "" for DeepSeek/Kimi poisoned tool-call history, otherwise promote internal reasoning to reasoning_content.
  • Ensures reasoning_content is removed from outgoing API messages when it would otherwise be non-string/null.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread run_agent.py Outdated
Comment thread run_agent.py Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Zjianru Zjianru force-pushed the fix/copy-reasoning-content-ordering-and-cross-provider-isolation branch from cc36068 to 88b65cc Compare April 25, 2026 21:49
@Zjianru

Zjianru commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

@OutThisLife Thanks for the approval! Both suggestions are addressed in the latest force-push: (1) now replaces the repeated call and whitespace-only strings are correctly treated as falsy via ; (2) comment wording updated from "rejects HTTP 400" to "returns HTTP 400". The diff is clean (+2 lines) — ready for merge whenever CI clears.

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current ordering still does not fix the cross-provider reasoning leak described by the PR.

The new needs_empty_reasoning branch is gated by not has_reasoning, so if a prior provider left an internal reasoning string but no provider-native reasoning_content, DeepSeek/Kimi will still receive that foreign reasoning via the later promotion branch:

if needs_empty_reasoning:
    api_msg["reasoning_content"] = ""
    return

if isinstance(normalized_reasoning, str) and normalized_reasoning:
    api_msg["reasoning_content"] = normalized_reasoning
    return

That means the stated MiniMax -> DeepSeek leak still happens whenever source_msg["reasoning"] is non-empty. I reproduced the helper behavior locally with a DeepSeek agent and a source assistant message containing reasoning + tool_calls but no reasoning_content; the outgoing API message became {"reasoning_content": "minimax prior reasoning"}.

For this fix, provider-required placeholder handling should win when reasoning_content is absent:

if isinstance(existing, str):
    api_msg["reasoning_content"] = existing
    return

if source_msg.get("tool_calls") and (self._needs_kimi_tool_reasoning() or self._needs_deepseek_tool_reasoning()):
    api_msg["reasoning_content"] = ""
    return

if isinstance(normalized_reasoning, str) and normalized_reasoning:
    api_msg["reasoning_content"] = normalized_reasoning
    return

In short: trust explicit reasoning_content; otherwise inject the required placeholder for DeepSeek/Kimi tool-call replay; only promote generic reasoning for providers where that is safe.

@Zjianru

Zjianru commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

@OutThisLife You're right — I miscounted the promotion path as a desirable feature when it's actually the leak vector. The guard created a false positive: any non-empty reasoning (including foreign-provider content) would skip the empty-string injection and fall through to block 3.

The new logic is clean: DeepSeek/Kimi tool-call replay always gets the placeholder, block 3 promotion only fires for non-thinking-mode providers. Applied your suggestion verbatim (+6/-9 lines). Thanks for the precise reproduction — that made the root cause unmistakable.

@OutThisLife OutThisLife merged commit f93d462 into NousResearch:main Apr 25, 2026
teknium1 pushed a commit that referenced this pull request Apr 26, 2026
… tool-call replay

PR #15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes #15812, relates #15749, #15478.
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
…-content-ordering-and-cross-provider-isolation

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…-content-ordering-and-cross-provider-isolation

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…-content-ordering-and-cross-provider-isolation

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…-content-ordering-and-cross-provider-isolation

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…-content-ordering-and-cross-provider-isolation

fix(agent): ordering fix in _copy_reasoning_content_for_api — cross-provider reasoning isolation
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
… tool-call replay

PR NousResearch#15478 fixed missing reasoning_content for DeepSeek API but introduced
a regression: tool-call messages with genuine 'reasoning' field were
overwritten by empty-string fallback before promotion.

Re-order _copy_reasoning_content_for_api steps:
  1. Preserve explicit reasoning_content
  2. Promote 'reasoning' field (MOVED UP)
  3. DeepSeek/Kimi tool-call empty-string fallback (MOVED DOWN)
  4. Non-thinking provider cleanup

Fixes NousResearch#15812, relates NousResearch#15749, NousResearch#15478.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API provider/kimi Kimi / Moonshot type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants