Skip to content

Commit 1063aca

Browse files
authored
fix(ai): v5: preserve providerMetadata on invalid tool calls and tool results (#10733)
## Background Provider metadata (such as Google Gemini's `thoughtSignature`) was being lost in two scenarios: 1. **Tool result propagation**: When tool results were converted to model messages, the `callProviderMetadata` from tool UI parts was not being propagated to tool-result parts. This was fixed in main branch (PR #10361, merged November 19, 2025) but not backported to v5.0. 2. **Invalid tool call handling**: When a tool call failed input validation (e.g., Zod schema mismatch), the error handler was creating an invalid tool call object without preserving the `providerMetadata` field from the original tool call. This is particularly problematic for providers like Google Gemini that use provider metadata to track `thoughtSignature` through reasoning and tool execution cycles. ## Summary <!-- What did you change? --> This PR includes two related fixes for provider metadata preservation on the `release-v5.0` branch: ### Fix 1: Tool Result Metadata Propagation (Backport from main) Backported changes ensure that `callProviderMetadata` from tool UI parts is correctly propagated to tool-result parts as `providerOptions` in two locations: 1. **Provider-executed tool results** (within assistant message content) - [convert-to-model-messages.ts:223-225](packages/ai/src/ui/convert-to-model-messages.ts#L223-L225) 2. **Client-executed tool results** (in separate tool messages) - [convert-to-model-messages.ts:281-283](packages/ai/src/ui/convert-to-model-messages.ts#L281-L283) ### Fix 2: Invalid Tool Call Metadata Preservation (New) Added `providerMetadata` preservation in the error handler of `parse-tool-call.ts` (line 88). When tool input validation fails, the invalid tool call object now includes the `providerMetadata` from the original `LanguageModelV2ToolCall`, matching the behavior of the success path. **File**: [packages/ai/src/generate-text/parse-tool-call.ts:88](packages/ai/src/generate-text/parse-tool-call.ts#L88) Both fixes ensure consistency in how provider metadata flows through the entire tool execution lifecycle (tool-call → tool-result) and error paths. ## Manual Verification Added comprehensive test coverage: - Client-executed tool result with provider metadata - Provider-executed tool result with provider metadata - Error state tool result with provider metadata - Dynamic tool result with provider metadata - Updated existing test snapshot to reflect the fix In addition, I manually ran: [google-vertex-code-execution.](examples/ai-core/src/stream-text/google-vertex-code-execution.ts) [google-vertex-fullstream](examples/ai-core/src/stream-text/google-vertex-fullstream.ts) [google-vertex-grounding](examples/ai-core/src/stream-text/google-vertex-grounding.ts) with both the `gemini-3-pro-preview` and `gemini-2.5-pro` models and confirmed they function correctly. Finally, I manually ran the provided reproduction script in #10560, and verified that it works correctly. ## Related Issues Fixes #10560 Fixes #10721 **Related PR:** - #10361 - Original fix in main branch **Potentially Related PRs:** - #10462 - `fix(google): add thought signature to gemini 3 pro image parts` - #10508 - Potentially related to provider metadata handling
1 parent 55c582c commit 1063aca

File tree

7 files changed

+297
-0
lines changed

7 files changed

+297
-0
lines changed

.changeset/fresh-insects-vanish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
fix(ai): Preverse providerMetadata in tool-call and tool-result parts

packages/ai/src/generate-text/generate-text.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3040,6 +3040,7 @@ describe('generateText', () => {
30403040
"cities": "San Francisco",
30413041
},
30423042
"invalid": true,
3043+
"providerMetadata": undefined,
30433044
"toolCallId": "call-1",
30443045
"toolName": "cityAttractions",
30453046
"type": "tool-call",

packages/ai/src/generate-text/parse-tool-call.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ describe('parseToolCall', () => {
166166
"error": [AI_NoSuchToolError: Model tried to call unavailable tool 'testTool'. No tools are available.],
167167
"input": {},
168168
"invalid": true,
169+
"providerMetadata": undefined,
169170
"toolCallId": "123",
170171
"toolName": "testTool",
171172
"type": "tool-call",
@@ -200,6 +201,7 @@ describe('parseToolCall', () => {
200201
"error": [AI_NoSuchToolError: Model tried to call unavailable tool 'nonExistentTool'. Available tools: testTool.],
201202
"input": {},
202203
"invalid": true,
204+
"providerMetadata": undefined,
203205
"toolCallId": "123",
204206
"toolName": "nonExistentTool",
205207
"type": "tool-call",
@@ -246,6 +248,7 @@ describe('parseToolCall', () => {
246248
"param1": "test",
247249
},
248250
"invalid": true,
251+
"providerMetadata": undefined,
249252
"toolCallId": "123",
250253
"toolName": "testTool",
251254
"type": "tool-call",
@@ -342,6 +345,7 @@ describe('parseToolCall', () => {
342345
Error message: Unexpected token 'i', "invalid json" is not valid JSON],
343346
"input": "invalid json",
344347
"invalid": true,
348+
"providerMetadata": undefined,
345349
"toolCallId": "123",
346350
"toolName": "testTool",
347351
"type": "tool-call",
@@ -378,6 +382,7 @@ describe('parseToolCall', () => {
378382
"error": [AI_ToolCallRepairError: Error repairing tool call: test error],
379383
"input": "invalid json",
380384
"invalid": true,
385+
"providerMetadata": undefined,
381386
"toolCallId": "123",
382387
"toolName": "testTool",
383388
"type": "tool-call",

packages/ai/src/generate-text/parse-tool-call.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export async function parseToolCall<TOOLS extends ToolSet>({
8585
dynamic: true,
8686
invalid: true,
8787
error,
88+
providerMetadata: toolCall.providerMetadata,
8889
};
8990
}
9091
}

packages/ai/src/generate-text/stream-text.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13146,6 +13146,7 @@ describe('streamText', () => {
1314613146
"cities": "San Francisco",
1314713147
},
1314813148
"invalid": true,
13149+
"providerMetadata": undefined,
1314913150
"toolCallId": "call-1",
1315013151
"toolName": "cityAttractions",
1315113152
"type": "tool-call",
@@ -13218,6 +13219,7 @@ describe('streamText', () => {
1321813219
"cities": "San Francisco",
1321913220
},
1322013221
"invalid": true,
13222+
"providerMetadata": undefined,
1322113223
"toolCallId": "call-1",
1322213224
"toolName": "cityAttractions",
1322313225
"type": "tool-call",

packages/ai/src/ui/convert-to-model-messages.test.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,217 @@ describe('convertToModelMessages', () => {
505505
"type": "text",
506506
"value": "3",
507507
},
508+
"providerOptions": {
509+
"testProvider": {
510+
"signature": "1234567890",
511+
},
512+
},
513+
"toolCallId": "call1",
514+
"toolName": "calculator",
515+
"type": "tool-result",
516+
},
517+
],
518+
"role": "tool",
519+
},
520+
]
521+
`);
522+
});
523+
524+
it('should propagate provider metadata to tool-result (client-executed)', () => {
525+
const result = convertToModelMessages([
526+
{
527+
role: 'assistant',
528+
parts: [
529+
{ type: 'step-start' },
530+
{
531+
type: 'tool-calculator',
532+
state: 'output-available',
533+
toolCallId: 'call1',
534+
input: { operation: 'add', numbers: [1, 2] },
535+
output: '3',
536+
callProviderMetadata: {
537+
testProvider: {
538+
executionTime: 100,
539+
},
540+
},
541+
},
542+
],
543+
},
544+
]);
545+
546+
expect(result).toMatchInlineSnapshot(`
547+
[
548+
{
549+
"content": [
550+
{
551+
"input": {
552+
"numbers": [
553+
1,
554+
2,
555+
],
556+
"operation": "add",
557+
},
558+
"providerExecuted": undefined,
559+
"providerOptions": {
560+
"testProvider": {
561+
"executionTime": 100,
562+
},
563+
},
564+
"toolCallId": "call1",
565+
"toolName": "calculator",
566+
"type": "tool-call",
567+
},
568+
],
569+
"role": "assistant",
570+
},
571+
{
572+
"content": [
573+
{
574+
"output": {
575+
"type": "text",
576+
"value": "3",
577+
},
578+
"providerOptions": {
579+
"testProvider": {
580+
"executionTime": 100,
581+
},
582+
},
583+
"toolCallId": "call1",
584+
"toolName": "calculator",
585+
"type": "tool-result",
586+
},
587+
],
588+
"role": "tool",
589+
},
590+
]
591+
`);
592+
});
593+
594+
it('should propagate provider metadata to tool-result (provider-executed)', () => {
595+
const result = convertToModelMessages([
596+
{
597+
role: 'assistant',
598+
parts: [
599+
{ type: 'step-start' },
600+
{
601+
type: 'tool-calculator',
602+
state: 'output-available',
603+
toolCallId: 'call1',
604+
input: { operation: 'subtract', numbers: [10, 5] },
605+
output: '5',
606+
providerExecuted: true,
607+
callProviderMetadata: {
608+
testProvider: {
609+
executionTime: 50,
610+
},
611+
},
612+
},
613+
],
614+
},
615+
]);
616+
617+
expect(result).toMatchInlineSnapshot(`
618+
[
619+
{
620+
"content": [
621+
{
622+
"input": {
623+
"numbers": [
624+
10,
625+
5,
626+
],
627+
"operation": "subtract",
628+
},
629+
"providerExecuted": true,
630+
"providerOptions": {
631+
"testProvider": {
632+
"executionTime": 50,
633+
},
634+
},
635+
"toolCallId": "call1",
636+
"toolName": "calculator",
637+
"type": "tool-call",
638+
},
639+
{
640+
"output": {
641+
"type": "text",
642+
"value": "5",
643+
},
644+
"providerOptions": {
645+
"testProvider": {
646+
"executionTime": 50,
647+
},
648+
},
649+
"toolCallId": "call1",
650+
"toolName": "calculator",
651+
"type": "tool-result",
652+
},
653+
],
654+
"role": "assistant",
655+
},
656+
]
657+
`);
658+
});
659+
660+
it('should propagate provider metadata to tool-result with error state', () => {
661+
const result = convertToModelMessages([
662+
{
663+
role: 'assistant',
664+
parts: [
665+
{ type: 'step-start' },
666+
{
667+
type: 'tool-calculator',
668+
state: 'output-error',
669+
toolCallId: 'call1',
670+
input: { operation: 'divide', numbers: [10, 0] },
671+
errorText: 'Error: Division by zero',
672+
callProviderMetadata: {
673+
testProvider: {
674+
errorCode: 'DIVISION_BY_ZERO',
675+
},
676+
},
677+
},
678+
],
679+
},
680+
]);
681+
682+
expect(result).toMatchInlineSnapshot(`
683+
[
684+
{
685+
"content": [
686+
{
687+
"input": {
688+
"numbers": [
689+
10,
690+
0,
691+
],
692+
"operation": "divide",
693+
},
694+
"providerExecuted": undefined,
695+
"providerOptions": {
696+
"testProvider": {
697+
"errorCode": "DIVISION_BY_ZERO",
698+
},
699+
},
700+
"toolCallId": "call1",
701+
"toolName": "calculator",
702+
"type": "tool-call",
703+
},
704+
],
705+
"role": "assistant",
706+
},
707+
{
708+
"content": [
709+
{
710+
"output": {
711+
"type": "error-text",
712+
"value": "Error: Division by zero",
713+
},
714+
"providerOptions": {
715+
"testProvider": {
716+
"errorCode": "DIVISION_BY_ZERO",
717+
},
718+
},
508719
"toolCallId": "call1",
509720
"toolName": "calculator",
510721
"type": "tool-result",
@@ -1184,6 +1395,72 @@ describe('convertToModelMessages', () => {
11841395
]
11851396
`);
11861397
});
1398+
1399+
it('should propagate provider metadata to dynamic tool-result', () => {
1400+
const result = convertToModelMessages([
1401+
{
1402+
role: 'assistant',
1403+
parts: [
1404+
{ type: 'step-start' },
1405+
{
1406+
type: 'dynamic-tool',
1407+
toolName: 'custom-tool',
1408+
state: 'output-available',
1409+
toolCallId: 'call-dynamic-1',
1410+
input: { param: 'test' },
1411+
output: 'dynamic-result',
1412+
callProviderMetadata: {
1413+
testProvider: {
1414+
dynamicToolExecution: true,
1415+
},
1416+
},
1417+
},
1418+
],
1419+
},
1420+
]);
1421+
1422+
expect(result).toMatchInlineSnapshot(`
1423+
[
1424+
{
1425+
"content": [
1426+
{
1427+
"input": {
1428+
"param": "test",
1429+
},
1430+
"providerOptions": {
1431+
"testProvider": {
1432+
"dynamicToolExecution": true,
1433+
},
1434+
},
1435+
"toolCallId": "call-dynamic-1",
1436+
"toolName": "custom-tool",
1437+
"type": "tool-call",
1438+
},
1439+
],
1440+
"role": "assistant",
1441+
},
1442+
{
1443+
"content": [
1444+
{
1445+
"output": {
1446+
"type": "text",
1447+
"value": "dynamic-result",
1448+
},
1449+
"providerOptions": {
1450+
"testProvider": {
1451+
"dynamicToolExecution": true,
1452+
},
1453+
},
1454+
"toolCallId": "call-dynamic-1",
1455+
"toolName": "custom-tool",
1456+
"type": "tool-result",
1457+
},
1458+
],
1459+
"role": "tool",
1460+
},
1461+
]
1462+
`);
1463+
});
11871464
});
11881465

11891466
describe('data part conversion', () => {

packages/ai/src/ui/convert-to-model-messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ export function convertToModelMessages<UI_MESSAGE extends UIMessage>(
220220
errorMode:
221221
part.state === 'output-error' ? 'json' : 'none',
222222
}),
223+
...(part.callProviderMetadata != null
224+
? { providerOptions: part.callProviderMetadata }
225+
: {}),
223226
});
224227
}
225228
}
@@ -278,6 +281,9 @@ export function convertToModelMessages<UI_MESSAGE extends UIMessage>(
278281
? 'text'
279282
: 'none',
280283
}),
284+
...(toolPart.callProviderMetadata != null
285+
? { providerOptions: toolPart.callProviderMetadata }
286+
: {}),
281287
};
282288
}
283289
default: {

0 commit comments

Comments
 (0)