Skip to content

fix: merge split assistant messages to prevent DeepSeek 400 on role alternation - #49162

Closed
woaini30050 wants to merge 2 commits into
NousResearch:mainfrom
woaini30050:fix/merge-split-assistant-messages
Closed

fix: merge split assistant messages to prevent DeepSeek 400 on role alternation#49162
woaini30050 wants to merge 2 commits into
NousResearch:mainfrom
woaini30050:fix/merge-split-assistant-messages

Conversation

@woaini30050

Copy link
Copy Markdown
Contributor

Description

When Hermes builds the API request payload for the next turn, some code paths (streaming accumulation, provider adapters, session replay) can split a single assistant response that contains both content (text reasoning) and tool_calls into two consecutive assistant messages:

msg[N]:   {"role": "assistant", "content": "Let me search..."}
msg[N+1]: {"role": "assistant", "tool_calls": [...]}

Strict providers like DeepSeek v4 Flash enforce strict assistant ↔ tool alternation and reject two consecutive assistant messages with HTTP 400:

Error code: 400 - Messages with role 'tool' must be a response to 
a preceding message with 'tool_calls'

The DB storage is correct (1 row with both content + tool_calls), but the API payload incorrectly has 2 messages.

Fix

Adds merge_split_assistant_messages() in agent.agent_runtime_helpers.py that merges adjacent assistant messages where:

  • msg[N]: has content but NO tool_calls
  • msg[N+1]: has tool_calls but NO content

Result: a single {"role": "assistant", "content": "...", "tool_calls": [...]}

The merge runs inside sanitize_api_messages(), which executes before every API call — covering both the main conversation loop and the summary/max-iterations path. This is a defense-in-depth approach that catches the split regardless of where it originates.

Safety (what is NOT merged)

  • Two content-only assistant messages → left alone (legitimate flow)
  • Two tool_calls-only assistant messages → left alone (parallel calls)
  • Already-correct messages with both content + tool_calls → left alone

Testing

  • Unit tests pass for all 7 edge cases

Related Issues

Closes #49147

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists labels Jun 19, 2026
@woaini30050
woaini30050 force-pushed the fix/merge-split-assistant-messages branch 2 times, most recently from b6553e3 to cec71de Compare June 20, 2026 15:26
- Remove dead `merged_count` tracking in sanitize_api_messages
- Fix duplicated docstring after merge function insertion
- Restore missing `# --- Role allowlist` comment
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #55603 (commit cbe397ef458bc715ce82451e722ee0e45a67687e) — your fix is preserved via a Co-authored-by trailer.

We consolidated this with the sibling reports (#29148, #49147) and the other open PRs (#49162, #34510) into one pass in repair_message_sequence() that covers both adjacency shapes: tool_calls-split-across-two-assistants (your case) and content-only → tool_calls-only. Added a guard so a tool result between two assistants doesn't collapse distinct rounds, and an exemption for Codex Responses interim turns. Thanks for the fix — closing as merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DeepSeek 400: assistant message with content+tool_calls split into 2 messages in API payload

3 participants