From 1678114f1fe10d80ef55486d60788fd49093a523 Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Thu, 23 Apr 2026 11:53:35 +0800 Subject: [PATCH] fix: split concatenated JSON tool args in _parse_openai_sse to prevent _raw death spiral --- llmcore.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/llmcore.py b/llmcore.py index c9058d7..0c988e1 100644 --- a/llmcore.py +++ b/llmcore.py @@ -1,7 +1,7 @@ import os, json, re, time, requests, sys, threading, urllib3, base64, mimetypes, uuid from datetime import datetime urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -_RESP_CACHE_KEY = str(uuid.uuid4()) +_RESP_CACHE_KEY = str(uuid.uuid4()) def _load_mykeys(): try: @@ -157,6 +157,22 @@ def _parse_claude_sse(resp_lines): content_blocks.append({"type": "text", "text": warn}); yield warn return content_blocks + +def _try_parse_tool_args(raw): + """Parse tool args string; split concatenated JSON objects like {..}{..} if needed. + Returns list of parsed dicts.""" + if not raw: return [{}] + try: return [json.loads(raw)] + except: pass + parts = re.split(r'(?<=\})(?=\{)', raw) + if len(parts) > 1: + parsed = [] + for p in parts: + try: parsed.append(json.loads(p)) + except: return [{"_raw": raw}] + return parsed + return [{"_raw": raw}] + def _parse_openai_sse(resp_lines, api_mode="chat_completions"): """Parse OpenAI SSE stream (chat_completions or responses API). Yields text chunks, returns list[content_block]. @@ -205,9 +221,11 @@ def _parse_openai_sse(resp_lines, api_mode="chat_completions"): if content_text: blocks.append({"type": "text", "text": content_text}) for idx in sorted(fc_buf): fc = fc_buf[idx] - try: inp = json.loads(fc["args"]) if fc["args"] else {} - except: inp = {"_raw": fc["args"]} - blocks.append({"type": "tool_use", "id": fc["id"] or '', "name": fc["name"], "input": inp}) + inps = _try_parse_tool_args(fc["args"]) + for i, inp in enumerate(inps): + bid = fc["id"] or '' + if len(inps) > 1: bid = f"{bid}_{i}" if bid else f"split_{i}" + blocks.append({"type": "tool_use", "id": bid, "name": fc["name"], "input": inp}) return blocks else: tc_buf = {} # index -> {id, name, args} @@ -234,9 +252,11 @@ def _parse_openai_sse(resp_lines, api_mode="chat_completions"): if content_text: blocks.append({"type": "text", "text": content_text}) for idx in sorted(tc_buf): tc = tc_buf[idx] - try: inp = json.loads(tc["args"]) if tc["args"] else {} - except: inp = {"_raw": tc["args"]} - blocks.append({"type": "tool_use", "id": tc["id"] or '', "name": tc["name"], "input": inp}) + inps = _try_parse_tool_args(tc["args"]) + for i, inp in enumerate(inps): + bid = tc["id"] or '' + if len(inps) > 1: bid = f"{bid}_{i}" if bid else f"split_{i}" + blocks.append({"type": "tool_use", "id": bid, "name": tc["name"], "input": inp}) return blocks def _record_usage(usage, api_mode):