From f853a3fc679c6bd08883768116dc1f7f872beb81 Mon Sep 17 00:00:00 2001 From: Areon Lundkvist Date: Mon, 1 Jun 2026 16:09:17 +0200 Subject: [PATCH] Harden streaming deltas against null payloads --- src/llm_core.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/llm_core.py b/src/llm_core.py index 55af620..210ed49 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -387,8 +387,8 @@ def _build_anthropic_payload(model, messages, temperature, max_tokens, stream=Fa if m.get("content"): content.append({"type": "text", "text": m["content"]}) for tc in m["tool_calls"]: - fn = tc.get("function", {}) - args_str = fn.get("arguments", "{}") + fn = tc.get("function") or {} + args_str = fn.get("arguments") or "{}" try: args = json.loads(args_str) if isinstance(args_str, str) else args_str except (json.JSONDecodeError, TypeError): @@ -886,26 +886,26 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl evt = j.get("type", "") if evt == "content_block_start": _anth_block_idx = j.get("index", _anth_block_idx + 1) - cb = j.get("content_block", {}) + cb = j.get("content_block") or {} _anth_block_type = cb.get("type", "text") if _anth_block_type == "tool_use": _anth_tool_blocks[_anth_block_idx] = { - "id": cb.get("id", f"call_{_anth_block_idx}"), - "name": cb.get("name", ""), + "id": cb.get("id") or f"call_{_anth_block_idx}", + "name": cb.get("name") or "", "arguments": "", } elif evt == "content_block_delta": - delta = j.get("delta", {}) + delta = j.get("delta") or {} delta_type = delta.get("type", "") if delta_type == "text_delta": - text = delta.get("text", "") + text = delta.get("text") or "" if text: yield f'data: {json.dumps({"delta": text})}\n\n' elif delta_type == "input_json_delta": # Accumulate tool arguments JSON idx = j.get("index", _anth_block_idx) if idx in _anth_tool_blocks: - partial = delta.get("partial_json", "") + partial = delta.get("partial_json") or "" _anth_tool_blocks[idx]["arguments"] += partial # Stream tool arg deltas for doc tools if partial and _anth_tool_blocks[idx].get("name") in ("create_document", "update_document", "edit_document"): @@ -1000,14 +1000,14 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl u = j["usage"] yield f'data: {json.dumps({"type": "usage", "data": {"input_tokens": u.get("prompt_tokens", 0), "output_tokens": u.get("completion_tokens", 0)}})}\n\n' elif "choices" in j: - delta = j["choices"][0].get("delta", {}) + delta = j["choices"][0].get("delta") or {} if isinstance(delta, dict): # Text content # Reasoning tokens (VLLM --reasoning-parser, e.g. Qwen3/DeepSeek-R1) - reasoning = delta.get("reasoning_content", "") + reasoning = delta.get("reasoning_content") or "" if reasoning: yield f'data: {json.dumps({"delta": reasoning, "thinking": True})}\n\n' - content = delta.get("content", "") + content = delta.get("content") or "" if content: # Some thinking backends start normal content with a # stray closing tag. Repair only that shape; do not @@ -1018,13 +1018,13 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl _first_content_sent = True yield f'data: {json.dumps({"delta": content})}\n\n' # Native tool calls — accumulate across chunks - for tc in delta.get("tool_calls", []): + for tc in delta.get("tool_calls") or []: idx = tc.get("index", 0) if idx not in _tc_acc: _tc_acc[idx] = {"id": "", "name": "", "arguments": ""} if tc.get("id"): _tc_acc[idx]["id"] = tc["id"] - func = tc.get("function", {}) + func = tc.get("function") or {} if func.get("name"): _tc_acc[idx]["name"] = func["name"] if "arguments" in func: