From 05720793ad6999ca1919d175de0fb48606d4488d Mon Sep 17 00:00:00 2001 From: wmantly Date: Sun, 13 Sep 2026 02:18:52 +0000 Subject: [PATCH] fix(proxy): add safe_json_loads to prevent 500 errors on streaming tool call decoding --- scripts/ollama-proxy.py | 54 ++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/scripts/ollama-proxy.py b/scripts/ollama-proxy.py index 6564f0d..e57263c 100644 --- a/scripts/ollama-proxy.py +++ b/scripts/ollama-proxy.py @@ -277,6 +277,30 @@ def _ollama_tools_to_openai(tools): }) return out +def safe_json_loads(val, default=None): + """Safely decode JSON string, handling extra data or syntax errors gracefully.""" + if default is None: + default = {} + if not val: + return default + if isinstance(val, dict): + return val + s = str(val).strip() + if not s: + return default + try: + return json.loads(s) + except Exception: + pass + try: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(s) + if isinstance(obj, dict): + return obj + except Exception: + pass + return {"raw": s} + def _openai_tool_calls_to_ollama(tool_calls): """OpenAI tool_calls -> ollama message.tool_calls.""" if not tool_calls: @@ -284,10 +308,7 @@ def _openai_tool_calls_to_ollama(tool_calls): out = [] for tc in tool_calls: fn = tc.get("function", {}) - try: - args = json.loads(fn.get("arguments", "{}")) - except json.JSONDecodeError: - args = {} + args = safe_json_loads(fn.get("arguments", "{}")) out.append({ "function": { "name": fn.get("name", ""), @@ -674,15 +695,16 @@ def ollama_chat(payload): om = {"role": "assistant", "content": ""} if tc_parts: - om["tool_calls"] = [ - { + tc_list = [] + for e in tc_parts.values(): + args = safe_json_loads(e.get("args")) + tc_list.append({ "function": { - "name": e["name"], - "arguments": json.loads(e["args"]) if e["args"] else {}, + "name": e.get("name", ""), + "arguments": args, } - } - for e in tc_parts.values() - ] + }) + om["tool_calls"] = tc_list yield json.dumps({ "model": MODEL_NAME, @@ -953,10 +975,7 @@ def _openai_tool_calls_to_anthropic(tool_calls): blocks = [] for tc in tool_calls: fn = tc.get("function", {}) - try: - args = json.loads(fn.get("arguments", "{}")) - except json.JSONDecodeError: - args = {} + args = safe_json_loads(fn.get("arguments", "{}")) blocks.append({ "type": "tool_use", "id": tc.get("id") or "toolu_" + uuid.uuid4().hex[:24], @@ -1115,10 +1134,7 @@ def anthropic_messages(payload): if finish: # close any open tool_use block(s) with accumulated input for tid, entry in tc_parts.items(): - try: - args = json.loads(entry["args"]) if entry["args"] else {} - except json.JSONDecodeError: - args = {} + args = safe_json_loads(entry.get("args")) yield sse("content_block_delta", { "type": "content_block_delta", "index": entry["block_index"],