mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
fix(llm): guard against null arguments in streaming tool-call accumulator (#2923)
This commit is contained in:
+6
-1
@@ -1690,7 +1690,12 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
|||||||
if func.get("name"):
|
if func.get("name"):
|
||||||
_tc_acc[idx]["name"] = func["name"]
|
_tc_acc[idx]["name"] = func["name"]
|
||||||
if "arguments" in func:
|
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
|
# Stream tool arg deltas for doc tools
|
||||||
if func["arguments"] and _tc_acc[idx].get("name") in ("create_document", "update_document", "edit_document"):
|
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'
|
yield f'data: {json.dumps({"type": "tool_call_delta", "index": idx, "name": _tc_acc[idx]["name"], "arg_delta": func["arguments"]})}\n\n'
|
||||||
|
|||||||
@@ -149,3 +149,23 @@ def test_sparse_integer_indices_then_null_do_not_collide(monkeypatch):
|
|||||||
events = _drive(monkeypatch, lines)
|
events = _drive(monkeypatch, lines)
|
||||||
calls = next(e["calls"] for e in events if e.get("type") == "tool_calls")
|
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}"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user