fix(stream): suppress compact malformed tool calls

This commit is contained in:
OpenClaw
2026-05-25 18:37:34 +03:00
parent 212d0466e5
commit d25394b2c5
2 changed files with 49 additions and 0 deletions

View File

@@ -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<string, ToolCall>
@@ -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<string, unknown> = {
role: "assistant",
@@ -1987,6 +1994,8 @@ export function createSSEStream(options: StreamOptions = {}) {
},
});
content = "";
} else if (containsMalformedTextualToolCall(content)) {
content = "";
}
const message: Record<string, unknown> = {
role: "assistant",

View File

@@ -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(
[