fix(ai): drop streaming parse internals before persistence

Addresses review feedback on #1507.
This commit is contained in:
can1357
2026-05-31 04:49:23 +02:00
parent e1d2dfeffa
commit bc7afc1437
3 changed files with 62 additions and 0 deletions
@@ -1234,6 +1234,8 @@ function handleToolCallArgumentsDone(
if (typeof args === "string") {
currentBlock.partialJson = args;
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
delete (currentBlock as { partialJson?: string }).partialJson;
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
}
}
@@ -1312,6 +1314,10 @@ function handleOutputItemDone(
name: item.name,
arguments: parseStreamingJson(item.arguments || "{}"),
};
if (runtime.currentBlock?.type === "toolCall") {
delete (runtime.currentBlock as { partialJson?: string }).partialJson;
delete (runtime.currentBlock as { lastParseLen?: number }).lastParseLen;
}
runtime.canSafelyReplayWebsocketOverSse = false;
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
return;
@@ -560,6 +560,8 @@ export async function processResponsesStream<TApi extends Api>(
if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
currentBlock.partialJson = event.arguments;
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
delete (currentBlock as { partialJson?: string }).partialJson;
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
}
} else if (event.type === "response.custom_tool_call_input.delta") {
if (currentItem?.type === "custom_tool_call" && currentBlock?.type === "toolCall") {
@@ -625,6 +627,10 @@ export async function processResponsesStream<TApi extends Api>(
name: item.name,
arguments: args,
};
if (currentBlock?.type === "toolCall") {
delete (currentBlock as { partialJson?: string }).partialJson;
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
}
currentBlock = null;
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
} else if (item.type === "custom_tool_call") {
@@ -225,6 +225,56 @@ describe("custom_tool_call stream receive", () => {
for (const e of events) yield e as ResponseStreamEvent;
}
test("strips streaming parse bookkeeping from function-call output blocks", async () => {
const output: AssistantMessage = {
role: "assistant",
content: [],
timestamp: Date.now(),
provider: "openai",
model: "gpt-5",
api: "openai-responses",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
};
const emitted: unknown[] = [];
const stream = {
push: (e: unknown) => emitted.push(e),
end: () => {},
} as never;
const args = JSON.stringify({ command: "x".repeat(300) });
await processResponsesStream(
makeStream([
{
type: "response.output_item.added",
item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "bash", arguments: "" },
},
{ type: "response.function_call_arguments.delta", delta: args },
{ type: "response.function_call_arguments.done", arguments: args },
{
type: "response.output_item.done",
item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "bash", arguments: args },
},
]),
output,
stream,
makeModel(),
);
const block = output.content[0] as Record<string, unknown>;
expect(block.type).toBe("toolCall");
expect(block.arguments).toEqual({ command: "x".repeat(300) });
expect("partialJson" in block).toBe(false);
expect("lastParseLen" in block).toBe(false);
});
test("aggregates delta events into a ToolCall with input arg", async () => {
const output: AssistantMessage = {
role: "assistant",