Skip to content

fix(sanitizer): drop tool_calls key when all calls deduped, not set empty array - #70781

Closed
yanjingr119 wants to merge 1 commit into
NousResearch:mainfrom
yanjingr119:pr/empty-tool-calls-fix
Closed

fix(sanitizer): drop tool_calls key when all calls deduped, not set empty array#70781
yanjingr119 wants to merge 1 commit into
NousResearch:mainfrom
yanjingr119:pr/empty-tool-calls-fix

Conversation

@yanjingr119

Copy link
Copy Markdown

Problem

When sanitize_api_messages deduplicates tool_call_ids, if all calls in a message are duplicates, the current code sets tool_calls: [] on the message dict. Strict providers (DeepSeek v4) reject this with HTTP 400:

Invalid 'messages[N].tool_calls': empty array.
Expected an array with minimum length 1, but got an empty array instead.

The existing pre-API chokepoint (lines 2738-2770) that drops empty/invalid tool_calls arrays runs before the dedup pass and cannot see arrays emptied here.

Root Cause

# Current code (line 2896):
if len(kept_tcs) != len(msg.get("tool_calls") or []):
    msg = {**msg, "tool_calls": kept_tcs}  # ❌ kept_tcs=[], produces tool_calls:[]

Fix

When kept_tcs is empty (all calls were duplicates), drop the tool_calls key entirely instead of preserving an empty array. If dropping the key would leave the message body empty (edge case: host-fed/merged histories from WebUI sidecar), supply a placeholder content string.

if len(kept_tcs) != len(msg.get("tool_calls") or []):
    if kept_tcs:
        msg = {**msg, "tool_calls": kept_tcs}
    else:
        msg = {k: v for k, v in msg.items() if k != "tool_calls"}
        if not str(msg.get("content") or "").strip():
            msg = {**msg, "content": "(duplicate tool call removed)"}

Testing

  • Reproduced: DeepSeek v4 HTTP 400 when WebUI sidecar + state.db merge produces duplicate tool_call_ids that all get deduped
  • Verified: same payload passes after fix (key dropped, no empty array)

…mpty array

When deduplicating tool_call_ids, if every call is a duplicate (kept_tcs
is empty), the current code sets tool_calls: [] on the message dict.
Strict providers (DeepSeek v4) reject this with HTTP 400:

  Invalid 'messages[N].tool_calls': empty array.
  Expected an array with minimum length 1, but got an empty array instead.

The existing pre-API chokepoint (lines 2738-2770) that drops empty
tool_calls arrays runs *before* the dedup pass and cannot see arrays
emptied here. Drop the key entirely at the dedup site instead, and
supply a placeholder content string when dropping the key would leave
the message body empty.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/deepseek DeepSeek API sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state duplicate This issue or pull request already exists labels Jul 24, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #64345. It already implements the same dedup-output drop-key repair with focused regression coverage; this patch's placeholder-content edge case is the only material delta.

@yanjingr119

Copy link
Copy Markdown
Author

Duplicate of #64345 which covers the same fix. The empty-content placeholder edge case in this PR is the only delta and is a rare scenario — closing in favor of #64345. Thanks for the review!

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 duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants