hermes - ✅(Solved) Fix DeepSeek V4 thinking mode: missing reasoning_content on tool-call messages causes 400 [3 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#15353Fetched 2026-04-25 06:22:53
View on GitHub
Comments
1
Participants
2
Timeline
11
Reactions
1
Timeline (top)
labeled ×4cross-referenced ×3closed ×1commented ×1

Error Message

BadRequestError [HTTP 400] Error: The reasoning_content in the thinking mode must be passed back to the API.

Root Cause

Two gaps in run_agent.py:

  1. _copy_reasoning_content_for_api(): The Kimi-only detection does not cover DeepSeek providers. Already-poisoned sessions replay without reasoning_content and hit 400.

  2. _build_assistant_message(): When a streaming response yields a tool-call message without reasoning text, reasoning_content is set to None and the stored message omits the field. New messages are persisted poisoned.

Fix Action

Fix

Part 1 — Merge DeepSeek detection into the existing needs_tool_reasoning_echo check:

  • provider == "deepseek"
  • "deepseek" in model (case-insensitive)
  • base_url_host_matches(base_url, "api.deepseek.com") (covers custom provider)

Part 2 — Add _needs_deepseek_tool_reasoning() helper, wired into _build_assistant_message() to store reasoning_content="" on new tool-call messages at creation time (prevents future poisoning).

Bonus — Add missing _copy_reasoning_content_for_api() call in _handle_max_iterations() flush path.

PR fix notes

PR #15354: fix: add DeepSeek reasoning_content echo for tool-call messages (fixes #15353)

Description (problem / solution / changelog)

Summary

DeepSeek V4 thinking mode requires reasoning_content on every assistant message that includes tool_calls. When missing, replay causes HTTP 400.

Closes #15353 Related: #14938 #14933 #15213

Changes

1. Merge DeepSeek into needs_tool_reasoning_echo check

In _copy_reasoning_content_for_api(), replaced the Kimi-only detection with a combined check covering:

  • provider == "deepseek"
  • "deepseek" in model (case-insensitive)
  • api.deepseek.com base URL (custom provider)

This handles already-poisoned persisted sessions by injecting empty reasoning_content on replay.

2. Store reasoning_content on new tool-call messages

Added _needs_deepseek_tool_reasoning() helper method, wired into _build_assistant_message(). When a DeepSeek tool-call message is created without reasoning text (common for streaming tool-only turns), stores reasoning_content="" instead of omitting the field. Prevents future session poisoning at the source.

3. Fix _handle_max_iterations path

Added missing call to _copy_reasoning_content_for_api() in the max-iterations flush path. Previously only the main loop and flush_memories() had this call.

Test Plan

  • Verified in state.db: new tool-call messages store reasoning_content
  • Previously poisoned messages handled by replay fix
  • Tested on Rocky Linux 9.7 with deepseek-v4-pro via custom provider

Diff

1 file changed: run_agent.py (+30, -3)

Changed files

  • run_agent.py (modified, +30/-3)

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

Description (problem / solution / changelog)

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

BeforeAfter
New DeepSeek tool-call messagesPersisted without reasoning_content → poisonedPinned reasoning_content="" at creation
Replay of poisoned historyHTTP 400 on next turnreasoning_content="" injected defensively, request succeeds
Kimi / MoonshotUnchanged (kimi-specific block preserved via _needs_kimi_tool_reasoning())Unchanged
Test suite21/21 targeted pass; 1046/1047 tests/run_agent/ pass (1 pre-existing unrelated failure)

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

Changed files

  • run_agent.py (modified, +44/-7)
  • scripts/release.py (modified, +1/-0)
  • tests/run_agent/test_deepseek_reasoning_content_echo.py (added, +213/-0)

PR #15446: fix(agent): comprehensive DeepSeek V4 support — context windows, thinking mode, reasoning replay

Description (problem / solution / changelog)

Summary

Unifies 5 fragmented DeepSeek V4 PRs into a single cohesive implementation:

  • Context windows: Add 1M entries for deepseek-v4-pro, deepseek-v4-flash, deepseek-chat, deepseek-reasoner (128K fallback preserved for older models)
  • Thinking mode toggle: Plumb thinking.type and reasoning_effort for native DeepSeek API — maps effort values to DeepSeek's supported "high"/"max" pair, strips incompatible sampling params (temperature, top_p, etc.) when thinking is enabled
  • reasoning_content replay: Inject reasoning_content="" on all assistant messages for DeepSeek replay, scoped to api.deepseek.com and OpenRouter deepseek/ prefix. Respects enabled: false to skip injection
  • _extract_reasoning guards: Use isinstance(str) checks instead of truthy checks, preventing crashes on non-string reasoning values
  • reasoning_content normalization: Preserve empty string "" in normalize_response (semantically valid for DeepSeek, was being dropped by truthy check)
  • _handle_max_iterations: Add missing _copy_reasoning_content_for_api call so the max-iterations summary path doesn't produce 400s
  • deepseek-chat preserved: deepseek-chat (the non-thinking alias) is NOT forced into thinking mode by default — only deepseek-v4-* and deepseek-reasoner models, or when the user explicitly opts in via reasoning_config

Test plan

  • 34 tests across 5 test classes (context windows, thinking mode, replay, isinstance guards, normalization)
  • Verified deepseek-chat does NOT force thinking mode by default
  • Verified deepseek-chat CAN opt-in to thinking with explicit config
  • Verified temperature stripping only happens when thinking is enabled
  • Verified non-DeepSeek models are not affected by any of these changes
  • Verified explicit reasoning_content is preserved (not overwritten with "")

Fixes #15353. Supersedes #14952, #14958, #15325, #15228, #15354.

Changed files

  • agent/model_metadata.py (modified, +6/-2)
  • agent/transports/chat_completions.py (modified, +33/-1)
  • run_agent.py (modified, +38/-7)
  • tests/agent/test_deepseek_v4.py (added, +322/-0)

Code Example

BadRequestError [HTTP 400] Error: The reasoning_content in the thinking mode must be passed back to the API.
RAW_BUFFERClick to expand / collapse

Bug Description

DeepSeek V4/V4-Pro 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:

BadRequestError [HTTP 400] Error: The reasoning_content in the thinking mode must be passed back to the API.

Related: #14938 #14933 #15213

Root Cause

Two gaps in run_agent.py:

  1. _copy_reasoning_content_for_api(): The Kimi-only detection does not cover DeepSeek providers. Already-poisoned sessions replay without reasoning_content and hit 400.

  2. _build_assistant_message(): When a streaming response yields a tool-call message without reasoning text, reasoning_content is set to None and the stored message omits the field. New messages are persisted poisoned.

Fix

Part 1 — Merge DeepSeek detection into the existing needs_tool_reasoning_echo check:

  • provider == "deepseek"
  • "deepseek" in model (case-insensitive)
  • base_url_host_matches(base_url, "api.deepseek.com") (covers custom provider)

Part 2 — Add _needs_deepseek_tool_reasoning() helper, wired into _build_assistant_message() to store reasoning_content="" on new tool-call messages at creation time (prevents future poisoning).

Bonus — Add missing _copy_reasoning_content_for_api() call in _handle_max_iterations() flush path.

Verification

  • Verified via state.db: all tool-call messages created after the fix have reasoning_content correctly stored
  • Previously poisoned messages handled by the replay fix

Environment

  • Provider: custom with base_url api.deepseek.com
  • Model: deepseek-v4-pro
  • OS: Rocky Linux 9.7 / Python 3.11

PR

Submitted from chen1749144759/hermes-agent branch fix/deepseek-reasoning-content-15250.

extent analysis

TL;DR

To fix the issue, merge DeepSeek detection into the needs_tool_reasoning_echo check and add a helper function _needs_deepseek_tool_reasoning() to store reasoning_content on new tool-call messages.

Guidance

  • Update the _copy_reasoning_content_for_api() function to include DeepSeek providers by checking for provider == "deepseek", "deepseek" in model, or base_url_host_matches(base_url, "api.deepseek.com").
  • Create a new helper function _needs_deepseek_tool_reasoning() to store reasoning_content="" on new tool-call messages in _build_assistant_message().
  • Add the missing _copy_reasoning_content_for_api() call in the _handle_max_iterations() flush path to prevent future poisoning.
  • Verify the fix by checking that all tool-call messages created after the fix have reasoning_content correctly stored in the state.db.

Example

No code snippet is provided as the issue already includes the necessary code changes.

Notes

The fix is specific to the DeepSeek V4/V4-Pro thinking mode and custom providers with a base URL of api.deepseek.com. The changes should be applied to the run_agent.py file.

Recommendation

Apply the workaround by merging the DeepSeek detection into the needs_tool_reasoning_echo check and adding the _needs_deepseek_tool_reasoning() helper function, as this will prevent future poisoning and fix the issue for existing poisoned messages.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix DeepSeek V4 thinking mode: missing reasoning_content on tool-call messages causes 400 [3 pull requests, 1 comments, 2 participants]