fix(sanitize): drop tool_calls key when dedup removes all calls (fixes empty-array 400 class) - #77377
fix(sanitize): drop tool_calls key when dedup removes all calls (fixes empty-array 400 class)#77377andrexibiza wants to merge 4 commits into
Conversation
The dedup pass in sanitize_api_messages (introduced by NousResearch#58327) can produce an empty tool_calls array when all tool_call_ids in a message are duplicates of earlier messages in a long conversation history. DeepSeek v4 and newer OpenAI reject empty tool_calls with HTTP 400: 'Invalid messages[N].tool_calls: empty array'. When kept_tcs is empty after dedup, drop the tool_calls key entirely instead of writing tool_calls: []. Fixes NousResearch#64335 Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com> # Conflicts: # tests/run_agent/test_message_sequence_repair.py
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
|
The core change here matches #64345's approach in |
|
Heads-up: the |
The |
|
Closing this as redundant — every change this PR proposes is already merged on main. Per your heads-up, the
So there is no standalone delegate contribution left to rebase — it shipped with the delegation work. I'm closing this PR rather than re-filing already-merged changes. Credit for the original tool_calls fix remains with @liuhao1024 via #86654. |
Related #64335 #64345 #70126 #74101 #76862
What does this PR do?
Fixes the entire class of empty-
tool_callsreintroduction insanitize_api_messages— not just one provider's symptom.sanitize_api_messages()inagent/agent_runtime_helpers.pyhas aStep-3 tool_call_id dedup pass that, when ALL tool calls in an assistant
message are duplicates of calls already seen, collapses
kept_tcsto[]and writes
tool_calls: []back into the message. Strict OpenAI-compatibleproviders reject the empty array with HTTP 400 — breaking every subsequent
request in the session until it scrolls out.
The fix (salvaged from #64345, authored by @liuhao1024, cherry-picked with
authorship preserved):
kept_tcsis non-empty after dedup: write it back (unchanged behavior).tool_callskey entirely(semantically "no tool calls", and the same normalization the earlier
empty-array pass at
agent/agent_runtime_helpers.py:3204-3237alreadyapplies) — instead of writing
tool_calls: [].tool_calls: []arriving atthe chokepoint; this closes the dedup path that re-introduced it after
that pass ran.
Regression test added:
test_sanitize_dedup_drops_tool_calls_key_when_all_removedasserts the first assistant keeps its tool call and the second (all-duplicate)
assistant has the key dropped, content preserved.
Addressing existing review commentary
Cloud-compatible endpoints — "Empty tool_calls" 400. → Covered: the key is
dropped, so no empty array reaches any strict provider, Qwen included.
"still present in v2026.7.30". → This is the exact residual gap: the dedup
pass re-writes the empty array after the earlier normalization. Fixed at
the source.
of Bug: sanitize_api_messages dedup pass writes tool_calls: [] — HTTP 400 on DeepSeek/OpenAI #64335. → Both close with this fix.
How to test
What platforms were tested?
git diff --checkclean,check-windows-footguns.pyclean on both changed files.Why this matters to users
Before: a long conversation that re-emits a duplicate tool_call_id (retries,
crash/resume, compression-window re-emission) silently poisons the session —
the next request 400s with "empty tool_calls" on DeepSeek, Qwen, doubao, and
other strict providers, and every later message fails until history scrolls
past the bad turn. Users hit this as "conversation suddenly dead, restart
needed."
After: the sanitizer drops the empty key the way it already drops arriving
empty arrays — the session keeps working, no provider 400, no restart.
Fixes #64335
Closes #74101, #76862, #70126
Checklist
git diff --checkcleanPart of #70126
Part of #74101
Part of #76862
Part of #30405
Part of #53520