Skip to content

fix: DeepSeek V4 thinking mode reasoning_content echo on tool-call messages#15407

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-9367aed6
Apr 24, 2026
Merged

fix: DeepSeek V4 thinking mode reasoning_content echo on tool-call messages#15407
teknium1 merged 3 commits into
mainfrom
hermes/hermes-9367aed6

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

DeepSeek V4 sessions with tool calls no longer die with 400 reasoning_content must be passed back. Fixes the creation path (new sessions are not poisoned) and the replay path (already-poisoned sessions recover).

Salvages @chen1749144759's #15354 with detection logic consolidated into helpers and a new regression test.

Root cause

DeepSeek V4 thinking mode requires reasoning_content on every assistant tool-call turn. Hermes had a Kimi-specific fallback in _copy_reasoning_content_for_api, but no DeepSeek coverage at creation time (_build_assistant_message) or replay time. Sessions got poisoned in state.db and every subsequent replay hit HTTP 400.

Changes

  • run_agent.py:
    • _build_assistant_message pins reasoning_content="" on new tool-call turns when DeepSeek detected (prevents future poisoning).
    • _copy_reasoning_content_for_api padding now covers DeepSeek too (fixes poisoned history).
    • Extracted _needs_kimi_tool_reasoning() + _needs_deepseek_tool_reasoning() helpers — single source of truth, used by both the creation and replay paths.
    • Added missing _copy_reasoning_content_for_api() call in _handle_max_iterations() flush path (latent bug; was missing for Kimi too).
  • tests/run_agent/test_deepseek_reasoning_content_echo.py: 21 tests covering all 3 DeepSeek signals (provider/model/host), poisoned replay, creation path, Kimi regression.
  • scripts/release.py: AUTHOR_MAP entry for @chen1749144759.

Detection signals (DeepSeek)

  • provider == "deepseek" (native)
  • "deepseek" in model (custom-provider setups using deepseek model names)
  • base_url host matches api.deepseek.com

Validation

Before After
New DeepSeek tool-call messages Persisted without reasoning_content → poisoned Pinned reasoning_content="" at creation
Replay of poisoned history HTTP 400 on next turn reasoning_content="" injected defensively, request succeeds
Kimi / Moonshot Unchanged (kimi-specific block preserved via _needs_kimi_tool_reasoning()) Unchanged
Test suite 21/21 targeted pass; 1046/1047 tests/run_agent/ pass (1 pre-existing unrelated failure)

Closes #15250, #15353.
Supersedes #15228, #15354. Thanks @ruxme and @chen1749144759.

chen1749144759 and others added 3 commits April 24, 2026 16:04
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)
…gression tests

Extracts _needs_kimi_tool_reasoning() for symmetry with the existing
_needs_deepseek_tool_reasoning() helper, so _copy_reasoning_content_for_api
uses the same detection logic as _build_assistant_message. Future changes
to either provider's signals now only touch one function.

Adds tests/run_agent/test_deepseek_reasoning_content_echo.py covering:
- All 3 DeepSeek detection signals (provider, model, host)
- Poisoned history replay (empty string fallback)
- Plain assistant turns NOT padded
- Explicit reasoning_content preserved
- Reasoning field promoted to reasoning_content
- Existing Kimi/Moonshot detection intact
- Non-thinking providers left alone

21 tests, all pass.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ npm lockfile hash out of date

Checked against commit b020ec6 (PR head at check time).

The hash = "sha256-..." line in these nix files no longer matches the committed package-lock.json:

Apply the fix

  • Apply lockfile fix — tick to push a commit with the correct hashes to this PR branch
  • Or run the Nix Lockfile Fix workflow manually (pass PR #15407)
  • Or locally: nix run .#fix-lockfiles -- --apply and commit the diff

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent loop, run_agent.py, prompt builder provider/deepseek DeepSeek API labels Apr 24, 2026
@teknium1 teknium1 merged commit d58b305 into main Apr 24, 2026
8 of 12 checks passed
@teknium1 teknium1 deleted the hermes/hermes-9367aed6 branch April 24, 2026 23:38
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 P1 High — major feature broken, no workaround provider/deepseek DeepSeek API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DeepSeek V4 Flash Discord session poisoned when tool-call assistant lacks reasoning_content

3 participants