fix(llm): guard against null arguments in streaming tool-call accumulator (#2923)

This commit is contained in:
nubs
2026-06-05 18:57:36 +00:00
committed by GitHub
parent 71dda5b106
commit 47a47bf71d
2 changed files with 26 additions and 1 deletions
+6 -1
View File
@@ -1690,7 +1690,12 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
if func.get("name"):
_tc_acc[idx]["name"] = func["name"]
if "arguments" in func:
_tc_acc[idx]["arguments"] += func["arguments"]
# Guard against a null arguments delta: `func` can be
# {"arguments": None} (JSON null), and a raw `+= None`
# raises TypeError that the broad except swallows,
# silently dropping the rest of the chunk. Matches the
# Anthropic accumulator (`partial = ... or ""`) above.
_tc_acc[idx]["arguments"] += func["arguments"] or ""
# Stream tool arg deltas for doc tools
if func["arguments"] and _tc_acc[idx].get("name") in ("create_document", "update_document", "edit_document"):
yield f'data: {json.dumps({"type": "tool_call_delta", "index": idx, "name": _tc_acc[idx]["name"], "arg_delta": func["arguments"]})}\n\n'
+20
View File
@@ -149,3 +149,23 @@ def test_sparse_integer_indices_then_null_do_not_collide(monkeypatch):
events = _drive(monkeypatch, lines)
calls = next(e["calls"] for e in events if e.get("type") == "tool_calls")
assert sorted(c["name"] for c in calls) == ["f0", "f2", "fn"], f"collision: {calls}"
def test_null_arguments_delta_does_not_drop_sibling_calls(monkeypatch):
# A gateway can emit a tool_call delta whose `arguments` is JSON null. The
# accumulator did `"" += None`, raising TypeError caught by the broad except
# that wraps the whole chunk — so it abandoned the rest of the tool_calls
# loop, silently dropping every LATER call in the same delta. Here the first
# call has arguments: null; the second (same delta) must still survive.
lines = [
_sse({"tool_calls": [
{"index": 0, "id": "a", "type": "function",
"function": {"name": "first", "arguments": None}},
{"index": 1, "id": "b", "type": "function",
"function": {"name": "second", "arguments": "{}"}},
]}),
"data: [DONE]",
]
events = _drive(monkeypatch, lines, model="gpt-4o-test")
calls = next(e["calls"] for e in events if e.get("type") == "tool_calls")
assert sorted(c["name"] for c in calls) == ["first", "second"], calls