fix: DeepSeek V4 thinking mode reasoning_content echo on tool-call messages#15407
Merged
Conversation
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.
Contributor
|
This was referenced Apr 24, 2026
This was referenced Apr 25, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_contenton 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 instate.dband every subsequent replay hit HTTP 400.Changes
run_agent.py:_build_assistant_messagepinsreasoning_content=""on new tool-call turns when DeepSeek detected (prevents future poisoning)._copy_reasoning_content_for_apipadding now covers DeepSeek too (fixes poisoned history)._needs_kimi_tool_reasoning()+_needs_deepseek_tool_reasoning()helpers — single source of truth, used by both the creation and replay paths._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_urlhost matchesapi.deepseek.comValidation
reasoning_content→ poisonedreasoning_content=""at creationreasoning_content=""injected defensively, request succeeds_needs_kimi_tool_reasoning())tests/run_agent/pass (1 pre-existing unrelated failure)Closes #15250, #15353.
Supersedes #15228, #15354. Thanks @ruxme and @chen1749144759.