Skip to content

fix(agent): pad reasoning_content on DeepSeek/Kimi tool-call turns#17489

Closed
season179 wants to merge 1 commit into
NousResearch:mainfrom
season179:fix/17400-deepseek-reasoning-content
Closed

fix(agent): pad reasoning_content on DeepSeek/Kimi tool-call turns#17489
season179 wants to merge 1 commit into
NousResearch:mainfrom
season179:fix/17400-deepseek-reasoning-content

Conversation

@season179

Copy link
Copy Markdown
Contributor

What does this PR do?

The defensive branch in _build_assistant_message that was supposed to pin reasoning_content="" on DeepSeek thinking-mode tool-call turns never actually fired. It was gated on msg.get("tool_calls") — but tool_calls doesn't get added to msg until ~60 lines later in the same method, so the check was always falsy. Whenever DeepSeek returned reasoning_content=None on a tool-call turn (which happens intermittently in long sessions) and streaming captured no reasoning text, the message got persisted bare. The next replay 400'd:

Error from provider (DeepSeek): The reasoning_content in the thinking mode must be passed back to the API.

The same defect is reachable on Kimi/Moonshot thinking — same enforcement, no separate ticket but the shape's identical.

The fix replaces the dead elif with a single post-tool_calls pad that fires when:

  • assistant_message.tool_calls is truthy (reading the SDK source of truth, not a not-yet-populated dict key, which is what caused the original bug); and
  • reasoning_content isn't already set by an earlier branch (so we never clobber legitimate reasoning); and
  • the active provider enforces echo-back.

Pulled the predicate into a new _needs_thinking_reasoning_pad() helper so _copy_reasoning_content_for_api shares it instead of re-inlining kimi or deepseek.

Related Issue

Fixes #17400

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • run_agent.py: removed the dead pad branch in _build_assistant_message; added the unified post-tool_calls pad covering DeepSeek and Kimi/Moonshot; extracted _needs_thinking_reasoning_pad() and reused it in _copy_reasoning_content_for_api.
  • tests/run_agent/test_deepseek_reasoning_content_echo.py: new TestBuildAssistantMessagePadsStrictProviders class — one parametrized test covering five provider/attribute combos (deepseek with reasoning_content=None, deepseek without the attribute at all, kimi-coding, custom-provider via api.moonshot.ai, plus an openrouter case to confirm we don't pad needlessly), and three standalone tests for "real reasoning preserved", "text-only turn untouched", "streamed reasoning wins over the empty pad". Folded the new helper's defaults into the existing _make_agent so the file doesn't end up with two parallel agent factories.

How to Test

  1. pytest tests/run_agent/test_deepseek_reasoning_content_echo.py -q — 31 pass.
  2. To confirm the new tests actually exercise the fix and not just pass trivially: git stash the change to run_agent.py, re-run the file, and 4 of the 8 new cases fail (the four that hit the previously-dead pad). Pop the stash; they pass again.
  3. Wider sweep: pytest tests/run_agent/ -q — 1164 passed, 7 unrelated skips.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run the relevant test slice (tests/run_agent/) and all tests pass — 1164 passed, 7 unrelated skips. Skipped the full tests/ suite as the change is contained.
  • I've added tests for my changes
  • I've tested on my platform: macOS 15 (Darwin 25.4, arm64)

Documentation & Housekeeping

  • Documentation updates — N/A (internal builder logic, no public surface change)
  • cli-config.yaml.example — N/A
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact considered — pure Python message-builder logic, no platform-specific paths
  • Tool descriptions/schemas — N/A

The defensive elif at _build_assistant_message was gated on
msg.get("tool_calls") but tool_calls only lands on msg ~60 lines later,
so the branch never fired. DeepSeek tool-call turns with no streamed
reasoning got persisted without reasoning_content and the next replay
400'd. Same defect reachable on Kimi/Moonshot thinking.

Drops the dead branch; adds a single post-tool_calls pad gated on
assistant_message.tool_calls (the source of truth) for both providers.
Extracts _needs_thinking_reasoning_pad() so _copy_reasoning_content_for_api
shares the predicate.
@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 labels Apr 29, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #18045. Your Kimi/Moonshot extension was salvaged on top of @lsdsjy's #16855 — the shared _needs_thinking_reasoning_pad() predicate and the TestBuildAssistantMessagePadsStrictProviders parametrized coverage (including the Moonshot-via-base_url and OpenRouter-no-pad negative control cases) all landed. You're co-authored on commit 76edc40ab. Thanks!

Stash-and-rerun confirmed 2/5 parametrized cases fail without your extension (kimi-attr-none, moonshot-base-url), so the tests are doing real work.

#18045

@season179 season179 deleted the fix/17400-deepseek-reasoning-content branch April 30, 2026 23:10
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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reasoning_content dropped in multi-turn tool calls with DeepSeek v4 (causes HTTP 400)

3 participants