From d25394b2c5ac82991bd55a3ad19677f3ce068fc2 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 25 May 2026 18:37:34 +0300 Subject: [PATCH] fix(stream): suppress compact malformed tool calls --- open-sse/utils/stream.ts | 9 ++++++++ tests/unit/stream-utils.test.ts | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 2798c8e09e..afe8e26c61 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -241,6 +241,11 @@ function containsTextualToolCallCandidate(text: unknown): boolean { return parseTextualToolCallCandidate(text) !== null; } +function containsMalformedTextualToolCall(text: unknown): boolean { + if (typeof text !== "string") return false; + return text.replace(/[\u200B-\u200D\uFEFF]/g, "").includes("[Tool call:"); +} + function collectPassthroughTextualToolCall( text: string, toolCalls: Map @@ -1746,6 +1751,8 @@ export function createSSEStream(options: StreamOptions = {}) { if (content && collectPassthroughTextualToolCall(content, passthroughToolCalls)) { passthroughHasToolCalls = true; content = ""; + } else if (containsMalformedTextualToolCall(content)) { + content = ""; } const message: Record = { role: "assistant", @@ -1987,6 +1994,8 @@ export function createSSEStream(options: StreamOptions = {}) { }, }); content = ""; + } else if (containsMalformedTextualToolCall(content)) { + content = ""; } const message: Record = { role: "assistant", diff --git a/tests/unit/stream-utils.test.ts b/tests/unit/stream-utils.test.ts index 170c977f6f..c6d93d6774 100644 --- a/tests/unit/stream-utils.test.ts +++ b/tests/unit/stream-utils.test.ts @@ -267,6 +267,46 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content assert.doesNotMatch(JSON.stringify(onCompletePayload.responseBody), /\[Tool call: terminal\]/); }); +test("createSSEStream suppresses malformed compact textual tool-call content", async () => { + let onCompletePayload = null; + + const text = await readTransformed( + [ + `data: ${JSON.stringify({ + candidates: [ + { + content: { + parts: [ + { + text: "[Tool call: search_files_ide{file_glob:*combos*.ts,path:/opt/OmniRoute,target:files}]", + }, + ], + }, + }, + ], + })}\n\n`, + `data: ${JSON.stringify({ candidates: [{ finishReason: "STOP" }] })}\n\n`, + ], + { + mode: "translate", + targetFormat: FORMATS.ANTIGRAVITY, + sourceFormat: FORMATS.OPENAI, + provider: "antigravity", + model: "antigravity/gemini-3.5-flash-low", + body: { messages: [{ role: "user", content: "inspect files" }] }, + onComplete(payload) { + onCompletePayload = payload; + }, + } + ); + + const choice = onCompletePayload.responseBody.choices[0]; + assert.equal(choice.finish_reason, "stop"); + assert.equal(choice.message.content, null); + assert.equal(choice.message.tool_calls, undefined); + assert.doesNotMatch(JSON.stringify(onCompletePayload.responseBody), /\[Tool call:/); +}); + test("createSSEStream passthrough flushes a buffered final line without a trailing newline", async () => { const text = await readTransformed( [