From 1ce0a6f02544d0dd3ca7dab72db8d71b46fdfe59 Mon Sep 17 00:00:00 2001 From: Yaohua-Leo <3067173925@qq.com> Date: Fri, 20 Mar 2026 08:52:13 +0800 Subject: [PATCH 1/3] fix(ollama): normalize tool call arguments from string to object When OpenAI-compatible APIs return tool calls, arguments are serialized as a JSON string. Ollama expects an object for function.arguments and fails parsing: 'Value looks like object, but can't find closing }'. This fix ensures arguments are always parsed to an object in two places: 1. extractToolCalls() - when processing input content 2. buildAssistantMessage() - when building output tool calls Fixes #50713 --- src/agents/ollama-stream.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/agents/ollama-stream.ts b/src/agents/ollama-stream.ts index f332ad1fd83..ccec6ea7d13 100644 --- a/src/agents/ollama-stream.ts +++ b/src/agents/ollama-stream.ts @@ -254,9 +254,21 @@ function extractToolCalls(content: unknown): OllamaToolCall[] { const result: OllamaToolCall[] = []; for (const part of parts) { if (part.type === "toolCall") { - result.push({ function: { name: part.name, arguments: part.arguments } }); + // Ensure arguments are always an object, not a string (OpenAI format passes JSON string) + const args = part.arguments; + let parsedArgs: Record; + if (typeof args === "string") { + try { + parsedArgs = JSON.parse(args); + } catch { + parsedArgs = {}; + } + } else { + parsedArgs = args ?? {}; + } + result.push({ function: { name: part.name, arguments: parsedArgs } }); } else if (part.type === "tool_use") { - result.push({ function: { name: part.name, arguments: part.input } }); + result.push({ function: { name: part.name, arguments: part.input ?? {} } }); } } return result; @@ -351,11 +363,17 @@ export function buildAssistantMessage( const toolCalls = response.message.tool_calls; if (toolCalls && toolCalls.length > 0) { for (const tc of toolCalls) { + // Normalize arguments: ensure it's always an object, not a string + const rawArgs = tc.function.arguments; + const normalizedArgs = + typeof rawArgs === "string" + ? (JSON.parse(rawArgs) as Record) + : (rawArgs ?? {}); content.push({ type: "toolCall", id: `ollama_call_${randomUUID()}`, name: tc.function.name, - arguments: tc.function.arguments, + arguments: normalizedArgs, }); } } From 55f97ef25741fa79096816252682e1e5502c8179 Mon Sep 17 00:00:00 2001 From: Yaohua-Leo <3067173925@qq.com> Date: Fri, 20 Mar 2026 10:00:01 +0800 Subject: [PATCH 2/3] fix(ollama): add try/catch to JSON.parse in buildAssistantMessage Responds to Greptile review feedback: add try/catch around JSON.parse in buildAssistantMessage to match extractToolCalls, preventing potential crash if Ollama returns malformed JSON string. --- src/agents/ollama-stream.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/agents/ollama-stream.ts b/src/agents/ollama-stream.ts index ccec6ea7d13..5e5cf9326c5 100644 --- a/src/agents/ollama-stream.ts +++ b/src/agents/ollama-stream.ts @@ -367,7 +367,13 @@ export function buildAssistantMessage( const rawArgs = tc.function.arguments; const normalizedArgs = typeof rawArgs === "string" - ? (JSON.parse(rawArgs) as Record) + ? (() => { + try { + return JSON.parse(rawArgs) as Record; + } catch { + return {} as Record; + } + })() : (rawArgs ?? {}); content.push({ type: "toolCall", From 31da281d9b6784e2618d246caa6ef4d3b2419f4f Mon Sep 17 00:00:00 2001 From: LehaoLin Date: Sat, 21 Mar 2026 00:48:49 +0800 Subject: [PATCH 3/3] refactor(ollama): extract parseToolArgs helper for consistent tool argument parsing - Add shared parseToolArgs() function with try/catch - Use helper in both extractToolCalls() and buildAssistantMessage() - Reduces code duplication and ensures consistent error handling Addresses review feedback: https://github.com/openclaw/openclaw/pull/50724#issuecomment-4094662186 --- src/agents/ollama-stream.ts | 41 ++++++++++++++----------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/agents/ollama-stream.ts b/src/agents/ollama-stream.ts index 5e5cf9326c5..c06d0193279 100644 --- a/src/agents/ollama-stream.ts +++ b/src/agents/ollama-stream.ts @@ -216,6 +216,19 @@ interface OllamaChatResponse { eval_duration?: number; } +// ── Helpers ────────────────────────────────────────────────────────────────── + +function parseToolArgs(raw: unknown): Record { + if (typeof raw === "string") { + try { + return JSON.parse(raw) as Record; + } catch { + return {}; + } + } + return (raw as Record) ?? {}; +} + // ── Message conversion ────────────────────────────────────────────────────── type InputContentPart = @@ -254,19 +267,7 @@ function extractToolCalls(content: unknown): OllamaToolCall[] { const result: OllamaToolCall[] = []; for (const part of parts) { if (part.type === "toolCall") { - // Ensure arguments are always an object, not a string (OpenAI format passes JSON string) - const args = part.arguments; - let parsedArgs: Record; - if (typeof args === "string") { - try { - parsedArgs = JSON.parse(args); - } catch { - parsedArgs = {}; - } - } else { - parsedArgs = args ?? {}; - } - result.push({ function: { name: part.name, arguments: parsedArgs } }); + result.push({ function: { name: part.name, arguments: parseToolArgs(part.arguments) } }); } else if (part.type === "tool_use") { result.push({ function: { name: part.name, arguments: part.input ?? {} } }); } @@ -363,23 +364,11 @@ export function buildAssistantMessage( const toolCalls = response.message.tool_calls; if (toolCalls && toolCalls.length > 0) { for (const tc of toolCalls) { - // Normalize arguments: ensure it's always an object, not a string - const rawArgs = tc.function.arguments; - const normalizedArgs = - typeof rawArgs === "string" - ? (() => { - try { - return JSON.parse(rawArgs) as Record; - } catch { - return {} as Record; - } - })() - : (rawArgs ?? {}); content.push({ type: "toolCall", id: `ollama_call_${randomUUID()}`, name: tc.function.name, - arguments: normalizedArgs, + arguments: parseToolArgs(tc.function.arguments), }); } }