From acfa9877b32a6de63cab204890c98df0c2f63c5d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 02:02:16 +0100 Subject: [PATCH] fix: parse Ollama tool call arguments --- extensions/ollama/src/stream-runtime.test.ts | 69 +++++++++++++++++++- extensions/ollama/src/stream.ts | 8 ++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/extensions/ollama/src/stream-runtime.test.ts b/extensions/ollama/src/stream-runtime.test.ts index d6598dd04eb..dcd664b1f2d 100644 --- a/extensions/ollama/src/stream-runtime.test.ts +++ b/extensions/ollama/src/stream-runtime.test.ts @@ -606,6 +606,73 @@ describe("buildAssistantMessage", () => { expect(toolCall.id).toMatch(/^ollama_call_[0-9a-f-]{36}$/); }); + it("parses stringified tool call arguments from Ollama responses", () => { + const response = { + model: "qwen3:32b", + created_at: "2026-01-01T00:00:00Z", + message: { + role: "assistant" as const, + content: "", + tool_calls: [{ function: { name: "bash", arguments: '{"command":"ls","path":"/tmp"}' } }], + }, + done: true, + }; + const result = buildAssistantMessage(response, modelInfo); + expect(result.content[0]).toMatchObject({ + type: "toolCall", + name: "bash", + arguments: { command: "ls", path: "/tmp" }, + }); + }); + + it("preserves unsafe integers in stringified tool call arguments", () => { + const response = { + model: "qwen3:32b", + created_at: "2026-01-01T00:00:00Z", + message: { + role: "assistant" as const, + content: "", + tool_calls: [ + { + function: { + name: "send", + arguments: '{"target":9223372036854775807,"nested":{"thread":1234567890123456789}}', + }, + }, + ], + }, + done: true, + }; + const result = buildAssistantMessage(response, modelInfo); + expect(result.content[0]).toMatchObject({ + type: "toolCall", + name: "send", + arguments: { + target: "9223372036854775807", + nested: { thread: "1234567890123456789" }, + }, + }); + }); + + it("falls back to empty arguments for malformed stringified tool call arguments", () => { + const response = { + model: "qwen3:32b", + created_at: "2026-01-01T00:00:00Z", + message: { + role: "assistant" as const, + content: "", + tool_calls: [{ function: { name: "bash", arguments: '{"command":"ls"' } }], + }, + done: true, + }; + const result = buildAssistantMessage(response, modelInfo); + expect(result.content[0]).toMatchObject({ + type: "toolCall", + name: "bash", + arguments: {}, + }); + }); + it("sets all costs to zero for local models", () => { const response = { model: "qwen3:32b", @@ -701,7 +768,7 @@ describe("parseNdjsonStream", () => { // Simulate the accumulation logic from createOllamaStreamFn const accumulatedToolCalls: Array<{ - function: { name: string; arguments: Record }; + function: { name: string; arguments: unknown }; }> = []; const chunks = []; for await (const chunk of parseNdjsonStream(reader)) { diff --git a/extensions/ollama/src/stream.ts b/extensions/ollama/src/stream.ts index c1f45a2070e..4dfd694fcdd 100644 --- a/extensions/ollama/src/stream.ts +++ b/extensions/ollama/src/stream.ts @@ -350,7 +350,7 @@ interface OllamaTool { interface OllamaToolCall { function: { name: string; - arguments: Record; + arguments: Record | string; }; } @@ -406,6 +406,10 @@ function ensureArgsObject(value: unknown): Record { return parseJsonObjectPreservingUnsafeIntegers(value) ?? {}; } +function normalizeOllamaToolCallArguments(value: unknown): Record { + return ensureArgsObject(value); +} + function normalizeOllamaCompatMessageToolArgs(payloadRecord: Record): void { const messages = payloadRecord.messages; if (!Array.isArray(messages)) { @@ -653,7 +657,7 @@ export function buildAssistantMessage( type: "toolCall", id: `ollama_call_${randomUUID()}`, name: toolCall.function.name, - arguments: toolCall.function.arguments, + arguments: normalizeOllamaToolCallArguments(toolCall.function.arguments), }); } }