Skip to content

Commit 71c9f84

Browse files
fix(core): forward providerMetadata on tool-result and tool-error stream chunks (#1202)
Google Vertex thinking models (e.g. gemini-3-flash-preview) attach providerMetadata containing thoughtSignature to tool-output stream events. The tool-result -> tool-output-available and tool-error -> tool-output-error conversions in convertFullStreamChunkToUIMessageStream were not forwarding this field, causing the AI SDK's UI message stream schema validation to reject the chunk with "unrecognized_keys" errors. This broke all tool calls when using @ai-sdk/google-vertex with thinking models. The fix adds providerMetadata forwarding to the tool-result, tool-error, and tool-output cases, matching the pattern already used by tool-call, text-*, reasoning-*, and source-* chunk types. Fixes #1195 Co-authored-by: Ke Wang <ke@pika.art>
1 parent 9d5ed63 commit 71c9f84

3 files changed

Lines changed: 126 additions & 1 deletion

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@voltagent/core": patch
3+
---
4+
5+
fix(core): forward providerMetadata on tool-result and tool-error stream chunks
6+
7+
Google Vertex thinking models attach `providerMetadata` (containing `thoughtSignature`) to
8+
tool-output stream events. The `tool-result``tool-output-available` and `tool-error`
9+
`tool-output-error` conversions in `convertFullStreamChunkToUIMessageStream` were not forwarding
10+
this field, causing the AI SDK's UI message stream schema validation to reject the chunk as
11+
having unrecognized keys. This broke all tool calls when using `@ai-sdk/google-vertex` with
12+
thinking models (e.g. `gemini-3-flash-preview`).
13+
14+
Fixes #1195

packages/core/src/agent/streaming/guardrail-stream.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,110 @@ describe("Output guardrail streaming integration", () => {
402402
}
403403
expect(collected.join("")).toBe("hello");
404404
});
405+
406+
it("preserves providerMetadata on tool-result events (Google Vertex thinking models)", async () => {
407+
const googleThoughtSignature = "CiQBjz1rXwFvT1I/B/3qqqGc2FdAgzW+FJY4Tg/zPaWUtF6imFw=";
408+
const parts: VoltAgentTextStreamPart[] = [
409+
{ type: "start" } as VoltAgentTextStreamPart,
410+
{
411+
type: "tool-call",
412+
toolCallId: "tool-1",
413+
toolName: "getWeather",
414+
input: { location: "Perth" },
415+
providerMetadata: { google: { thoughtSignature: googleThoughtSignature } },
416+
} as VoltAgentTextStreamPart,
417+
{
418+
type: "tool-result",
419+
toolCallId: "tool-1",
420+
output: { weather: { location: "Perth", condition: "Sunny", temperature: 25 } },
421+
providerMetadata: { google: { thoughtSignature: googleThoughtSignature } },
422+
} as VoltAgentTextStreamPart,
423+
{ type: "text-start", id: "text-1" } as VoltAgentTextStreamPart,
424+
{ type: "text-delta", id: "text-1", delta: "The weather is sunny." } as any,
425+
{ type: "text-end", id: "text-1" } as VoltAgentTextStreamPart,
426+
{ type: "finish", finishReason: "stop" } as any,
427+
];
428+
429+
const pipeline = buildPipeline(parts, {
430+
id: "passthrough",
431+
name: "Passthrough",
432+
handler: async (_ctx) => ({ pass: true }) as const,
433+
streamHandler: ({ part }) => part,
434+
});
435+
436+
const emitted: VoltAgentTextStreamPart[] = [];
437+
for await (const chunk of pipeline.fullStream) {
438+
emitted.push(chunk as VoltAgentTextStreamPart);
439+
}
440+
441+
await pipeline.finalizePromise;
442+
443+
// tool-call should preserve providerMetadata
444+
const toolCall = emitted.find((c) => c.type === "tool-call") as any;
445+
expect(toolCall).toBeDefined();
446+
expect(toolCall.providerMetadata).toEqual({
447+
google: { thoughtSignature: googleThoughtSignature },
448+
});
449+
450+
// tool-result should preserve providerMetadata (the fix for #1195)
451+
const toolResult = emitted.find((c) => c.type === "tool-result") as any;
452+
expect(toolResult).toBeDefined();
453+
expect(toolResult.providerMetadata).toEqual({
454+
google: { thoughtSignature: googleThoughtSignature },
455+
});
456+
});
457+
458+
it("forwards providerMetadata to tool-output-available in UI stream (fixes #1195)", async () => {
459+
const googleThoughtSignature = "CiQBjz1rXwFvT1I/B/3qqqGc2FdAgzW+FJY4Tg/zPaWUtF6imFw=";
460+
const parts: VoltAgentTextStreamPart[] = [
461+
{ type: "start" } as VoltAgentTextStreamPart,
462+
{
463+
type: "tool-call",
464+
toolCallId: "tool-1",
465+
toolName: "getWeather",
466+
input: { location: "Perth" },
467+
providerMetadata: { google: { thoughtSignature: googleThoughtSignature } },
468+
} as VoltAgentTextStreamPart,
469+
{
470+
type: "tool-result",
471+
toolCallId: "tool-1",
472+
output: { weather: { location: "Perth", condition: "Sunny", temperature: 25 } },
473+
providerMetadata: { google: { thoughtSignature: googleThoughtSignature } },
474+
} as VoltAgentTextStreamPart,
475+
{ type: "text-start", id: "text-1" } as VoltAgentTextStreamPart,
476+
{ type: "text-delta", id: "text-1", delta: "The weather is sunny." } as any,
477+
{ type: "text-end", id: "text-1" } as VoltAgentTextStreamPart,
478+
{ type: "finish", finishReason: "stop" } as any,
479+
];
480+
481+
const pipeline = buildPipeline(parts, {
482+
id: "passthrough",
483+
name: "Passthrough",
484+
handler: async (_ctx) => ({ pass: true }) as const,
485+
streamHandler: ({ part }) => part,
486+
});
487+
488+
const uiStream = pipeline.createUIStream();
489+
const uiChunks: Array<Record<string, unknown>> = [];
490+
for await (const chunk of uiStream) {
491+
uiChunks.push(chunk as Record<string, unknown>);
492+
}
493+
494+
await pipeline.finalizePromise;
495+
496+
// tool-input-available (from tool-call) should have providerMetadata
497+
const toolInput = uiChunks.find((c) => c.type === "tool-input-available") as any;
498+
expect(toolInput).toBeDefined();
499+
expect(toolInput.providerMetadata).toEqual({
500+
google: { thoughtSignature: googleThoughtSignature },
501+
});
502+
503+
// tool-output-available (from tool-result) should have providerMetadata
504+
// Before the fix, this was missing and caused zod schema validation to fail
505+
const toolOutput = uiChunks.find((c) => c.type === "tool-output-available") as any;
506+
expect(toolOutput).toBeDefined();
507+
expect(toolOutput.providerMetadata).toEqual({
508+
google: { thoughtSignature: googleThoughtSignature },
509+
});
510+
});
405511
});

packages/core/src/agent/streaming/guardrail-stream.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,13 +573,15 @@ function convertFullStreamChunkToUIMessageStream<UI_MESSAGE extends UIMessage>({
573573
toolCallId?: string;
574574
output?: unknown;
575575
providerExecuted?: boolean;
576+
providerMetadata?: unknown;
576577
dynamic?: unknown;
577578
};
578579
return {
579580
type: "tool-output-available",
580581
toolCallId: typed.toolCallId,
581582
output: typed.output,
582583
...(typed.providerExecuted != null ? { providerExecuted: typed.providerExecuted } : {}),
584+
...(typed.providerMetadata != null ? { providerMetadata: typed.providerMetadata } : {}),
583585
...(typed.dynamic != null ? { dynamic: typed.dynamic } : {}),
584586
} as InferUIMessageChunk<UI_MESSAGE>;
585587
}
@@ -589,23 +591,26 @@ function convertFullStreamChunkToUIMessageStream<UI_MESSAGE extends UIMessage>({
589591
toolCallId?: string;
590592
error?: unknown;
591593
providerExecuted?: boolean;
594+
providerMetadata?: unknown;
592595
dynamic?: unknown;
593596
};
594597
return {
595598
type: "tool-output-error",
596599
toolCallId: typed.toolCallId,
597600
errorText: onError(typed.error),
598601
...(typed.providerExecuted != null ? { providerExecuted: typed.providerExecuted } : {}),
602+
...(typed.providerMetadata != null ? { providerMetadata: typed.providerMetadata } : {}),
599603
...(typed.dynamic != null ? { dynamic: typed.dynamic } : {}),
600604
} as InferUIMessageChunk<UI_MESSAGE>;
601605
}
602606

603607
case "tool-output": {
604-
const typed = part as { toolCallId?: string; output: unknown };
608+
const typed = part as { toolCallId?: string; output: unknown; providerMetadata?: unknown };
605609
return {
606610
type: "tool-output-available",
607611
toolCallId: typed.toolCallId,
608612
output: typed.output,
613+
...(typed.providerMetadata != null ? { providerMetadata: typed.providerMetadata } : {}),
609614
} as InferUIMessageChunk<UI_MESSAGE>;
610615
}
611616

0 commit comments

Comments
 (0)