From 8c700724868e13c5b4211ace498ba13f848fbf83 Mon Sep 17 00:00:00 2001 From: Prateek Date: Sun, 9 Aug 2026 09:46:42 +0530 Subject: [PATCH] fix: drop empty tool_calls arrays on assistant messages (strict providers 400) DeepSeek and other strict providers reject assistant messages carrying tool_calls: [] as a 400 error. The pre-call sanitizer dedups duplicate tool_call_ids; when every call in a turn was a duplicate, the result was an empty tool_calls array that the early sanitizer pass had already run on. Drop the key entirely in that case, and add a final pass that removes any surviving empty tool_calls arrays before send. --- agent/agent_runtime_helpers.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index e64fd9ad72309..1ddf9930da9cb 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -3458,7 +3458,14 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] seen_assistant_call_ids.add(cid) kept_tcs.append(tc) if len(kept_tcs) != len(msg.get("tool_calls") or []): - msg = {**msg, "tool_calls": kept_tcs} + if kept_tcs: + msg = {**msg, "tool_calls": kept_tcs} + else: + # All calls were duplicate ids — drop the key entirely. + # Setting tool_calls: [] makes strict providers (DeepSeek) + # 400 with "empty array" even though the early sanitizer + # pass already ran. + msg = {k: v for k, v in msg.items() if k != "tool_calls"} deduped.append(msg) elif role == "tool": cid = (msg.get("tool_call_id") or "").strip() @@ -3476,6 +3483,28 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] "Pre-call sanitizer: removed %d duplicate tool_call_id reference(s)", removed_dupes, ) + + # Final pass: dedup can leave assistant messages with tool_calls: [] when + # every call in a turn was a duplicate id. Strict providers reject that. + final: List[Dict[str, Any]] = [] + dropped_empty_after_dedup = 0 + for msg in messages: + if ( + isinstance(msg, dict) + and msg.get("role") == "assistant" + and "tool_calls" in msg + and not (isinstance(msg["tool_calls"], list) and msg["tool_calls"]) + ): + msg = {k: v for k, v in msg.items() if k != "tool_calls"} + dropped_empty_after_dedup += 1 + final.append(msg) + if dropped_empty_after_dedup: + _ra().logger.debug( + "Pre-call sanitizer: dropped empty tool_calls on %d assistant " + "message(s) after dedup", + dropped_empty_after_dedup, + ) + messages = final return messages