From c391977f3808bb668d7c35e0671898c55862d06b Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 9 Jul 2026 13:33:52 +0200 Subject: [PATCH 1/4] reproduction (reproduced) --- ...issue-16298-gemini3-parallel-tool-calls.ts | 113 ++++++++++++++++++ ...sue-16298-gemini3-parallel-tool-calls.json | 40 +++++++ .../src/convert-to-google-messages.test.ts | 109 +++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts create mode 100644 packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json diff --git a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts new file mode 100644 index 000000000000..ab235fe04dc6 --- /dev/null +++ b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts @@ -0,0 +1,113 @@ +import { createGateway, stepCountIs, streamText, tool } from 'ai'; +import { z } from 'zod'; + +async function main() { + const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY! }); + + const result = streamText({ + model: gateway('google/gemini-3.1-flash-lite'), + abortSignal: AbortSignal.timeout(60_000), + stopWhen: stepCountIs(3), + tools: { + get_weather: tool({ + description: 'Get the current weather for a given city.', + inputSchema: z.object({ city: z.string() }), + execute: async ({ city }) => ({ + city, + tempC: city === 'Paris' ? 21 : 24, + conditions: 'sunny', + }), + }), + }, + prompt: + 'Use the get_weather tool to check the weather in Paris and Tokyo in parallel in the same assistant step, then summarize.', + }); + + for await (const _ of result.textStream) { + // drain + } + + const warnings = await result.warnings; + const steps = await result.steps; + + console.log('WARNINGS:'); + console.log(JSON.stringify(warnings, null, 2)); + + console.log('\nTOOL-CALL PARTS:'); + let toolCallCount = 0; + let signedToolCallCount = 0; + let unsignedToolCallCount = 0; + + for (const [stepIndex, step] of steps.entries()) { + for (const part of step.content) { + if (part.type !== 'tool-call') { + continue; + } + + toolCallCount++; + const thoughtSignature = + ( + part.providerMetadata as + | { vertex?: { thoughtSignature?: unknown } } + | null + | undefined + )?.vertex?.thoughtSignature ?? + ( + part.providerMetadata as + | { google?: { thoughtSignature?: unknown } } + | null + | undefined + )?.google?.thoughtSignature ?? + ( + part.providerMetadata as + | { googleVertex?: { thoughtSignature?: unknown } } + | null + | undefined + )?.googleVertex?.thoughtSignature; + + if (thoughtSignature == null) { + unsignedToolCallCount++; + } else { + signedToolCallCount++; + } + + console.log( + [ + `step[${stepIndex}]`, + `tool-call=${part.toolName}`, + `thoughtSignature=${thoughtSignature == null ? 'ABSENT' : 'PRESENT'}`, + `providerMetadata=${JSON.stringify(part.providerMetadata ?? null)}`, + ].join(' '), + ); + } + } + + const warningText = JSON.stringify(warnings); + const reproduced = + toolCallCount >= 2 && + signedToolCallCount >= 1 && + unsignedToolCallCount >= 1 && + warningText.includes('skip_thought_signature_validator'); + + console.log('\nSUMMARY:'); + console.log( + JSON.stringify( + { + toolCallCount, + signedToolCallCount, + unsignedToolCallCount, + hasSkipThoughtSignatureValidatorWarning: warningText.includes( + 'skip_thought_signature_validator', + ), + reproduced, + }, + null, + 2, + ), + ); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json b/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json new file mode 100644 index 000000000000..bab5928cfc48 --- /dev/null +++ b/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json @@ -0,0 +1,40 @@ +{ + "capturedAt": "2026-07-09", + "provider": "gateway", + "model": "google/gemini-3.1-flash-lite", + "prompt": "Use the get_weather tool to check the weather in Paris and Tokyo in parallel in the same assistant step, then summarize.", + "warnings": [ + { + "type": "other", + "message": "Replayed 1 `functionCall` part(s) for a Gemini 3 model without a `thoughtSignature` (tools: `get_weather`). Injected the documented `skip_thought_signature_validator` sentinel to keep the request from failing with HTTP 400. The likely cause is application code that drops `providerOptions.google.thoughtSignature` when persisting or serializing assistant tool-call messages. See https://ai.google.dev/gemini-api/docs/thought-signatures." + } + ], + "toolCalls": [ + { + "step": 0, + "toolName": "get_weather", + "thoughtSignature": "present", + "providerMetadata": { + "googleVertex": { + "thoughtSignature": "AY89a18cQIBavtrXtrOBITFPVtz8vFtAN3GAhYMpFToaLZLimjftIYce6awl9EQyMF8=" + }, + "vertex": { + "thoughtSignature": "AY89a18cQIBavtrXtrOBITFPVtz8vFtAN3GAhYMpFToaLZLimjftIYce6awl9EQyMF8=" + } + } + }, + { + "step": 0, + "toolName": "get_weather", + "thoughtSignature": "absent", + "providerMetadata": null + } + ], + "summary": { + "toolCallCount": 2, + "signedToolCallCount": 1, + "unsignedToolCallCount": 1, + "hasSkipThoughtSignatureValidatorWarning": true, + "reproduced": true + } +} diff --git a/packages/google/src/convert-to-google-messages.test.ts b/packages/google/src/convert-to-google-messages.test.ts index 4ca10e6a4fa4..938974940cd8 100644 --- a/packages/google/src/convert-to-google-messages.test.ts +++ b/packages/google/src/convert-to-google-messages.test.ts @@ -1,9 +1,24 @@ +import fs from 'node:fs'; import { describe, expect, it, vi } from 'vitest'; import { convertToGoogleMessages, SKIP_THOUGHT_SIGNATURE_VALIDATOR, } from './convert-to-google-messages'; +const issue16298Fixture = JSON.parse( + fs.readFileSync( + 'src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json', + 'utf8', + ), +) as { + toolCalls: Array<{ + providerMetadata: { + googleVertex?: { thoughtSignature?: string }; + vertex?: { thoughtSignature?: string }; + } | null; + }>; +}; + describe('system messages', () => { it('should store system message in system instruction', async () => { const result = convertToGoogleMessages([ @@ -1686,6 +1701,100 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { expect(onWarning.mock.calls[0][0].message).toContain('`weather`'); }); + it('does NOT inject the sentinel or warn for a documented Gemini 3 parallel function-call batch with only the first call signed', () => { + const firstSignature = + issue16298Fixture.toolCalls[0].providerMetadata?.googleVertex + ?.thoughtSignature ?? + issue16298Fixture.toolCalls[0].providerMetadata?.vertex?.thoughtSignature; + + expect(firstSignature).toBeTruthy(); + expect(issue16298Fixture.toolCalls[1].providerMetadata).toBeNull(); + + const onWarning = vi.fn(); + const result = convertToGoogleMessages( + [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Check the weather in Paris and Tokyo.', + }, + ], + }, + { + role: 'assistant', + content: [ + { + type: 'tool-call', + toolCallId: 'tc_paris', + toolName: 'get_weather', + input: { city: 'Paris' }, + providerOptions: { + googleVertex: { thoughtSignature: firstSignature }, + vertex: { thoughtSignature: firstSignature }, + }, + }, + { + type: 'tool-call', + toolCallId: 'tc_tokyo', + toolName: 'get_weather', + input: { city: 'Tokyo' }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc_paris', + toolName: 'get_weather', + output: { + type: 'json', + value: { city: 'Paris', tempC: 21, conditions: 'sunny' }, + }, + }, + { + type: 'tool-result', + toolCallId: 'tc_tokyo', + toolName: 'get_weather', + output: { + type: 'json', + value: { city: 'Tokyo', tempC: 24, conditions: 'sunny' }, + }, + }, + ], + }, + ], + { + isGemini3Model: true, + providerOptionsNames: ['googleVertex', 'vertex'], + onWarning, + }, + ); + + const assistant = result.contents.find(c => c.role === 'model'); + expect(assistant?.parts).toStrictEqual([ + { + functionCall: { + id: 'tc_paris', + name: 'get_weather', + args: { city: 'Paris' }, + }, + thoughtSignature: firstSignature, + }, + { + functionCall: { + id: 'tc_tokyo', + name: 'get_weather', + args: { city: 'Tokyo' }, + }, + }, + ]); + expect(onWarning).not.toHaveBeenCalled(); + }); + it('does NOT inject the sentinel for non-Gemini-3 models', () => { const onWarning = vi.fn(); const result = convertToGoogleMessages(promptWithToolCallMissingSignature, { From 190075e6523a057d122baa14438117f3c8fc48c5 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 9 Jul 2026 13:59:33 +0200 Subject: [PATCH 2/4] fix #16298 --- .changeset/gemini-parallel-tool-calls.md | 5 +++ ...issue-16298-gemini3-parallel-tool-calls.ts | 19 +++++---- ...sue-16298-gemini3-parallel-tool-calls.json | 40 ------------------- .../src/convert-to-google-messages.test.ts | 30 ++------------ .../google/src/convert-to-google-messages.ts | 18 ++++++++- 5 files changed, 38 insertions(+), 74 deletions(-) create mode 100644 .changeset/gemini-parallel-tool-calls.md delete mode 100644 packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json diff --git a/.changeset/gemini-parallel-tool-calls.md b/.changeset/gemini-parallel-tool-calls.md new file mode 100644 index 000000000000..ca020d6ca62d --- /dev/null +++ b/.changeset/gemini-parallel-tool-calls.md @@ -0,0 +1,5 @@ +--- +"@ai-sdk/google": patch +--- + +fix(google): avoid Gemini 3 missing-signature warnings for signed parallel tool call batches diff --git a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts index ab235fe04dc6..093fe6c43ef9 100644 --- a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts +++ b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts @@ -1,11 +1,10 @@ -import { createGateway, stepCountIs, streamText, tool } from 'ai'; +import { google } from '@ai-sdk/google'; +import { stepCountIs, streamText, tool } from 'ai'; import { z } from 'zod'; async function main() { - const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY! }); - const result = streamText({ - model: gateway('google/gemini-3.1-flash-lite'), + model: google('gemini-3.1-flash-lite-preview'), abortSignal: AbortSignal.timeout(60_000), stopWhen: stepCountIs(3), tools: { @@ -83,11 +82,11 @@ async function main() { } const warningText = JSON.stringify(warnings); - const reproduced = + const fixed = toolCallCount >= 2 && signedToolCallCount >= 1 && unsignedToolCallCount >= 1 && - warningText.includes('skip_thought_signature_validator'); + !warningText.includes('skip_thought_signature_validator'); console.log('\nSUMMARY:'); console.log( @@ -99,12 +98,18 @@ async function main() { hasSkipThoughtSignatureValidatorWarning: warningText.includes( 'skip_thought_signature_validator', ), - reproduced, + fixed, }, null, 2, ), ); + + if (!fixed) { + throw new Error( + 'Expected parallel Gemini 3 tool calls with one signed call and no skip_thought_signature_validator warning.', + ); + } } main().catch(error => { diff --git a/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json b/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json deleted file mode 100644 index bab5928cfc48..000000000000 --- a/packages/google/src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "capturedAt": "2026-07-09", - "provider": "gateway", - "model": "google/gemini-3.1-flash-lite", - "prompt": "Use the get_weather tool to check the weather in Paris and Tokyo in parallel in the same assistant step, then summarize.", - "warnings": [ - { - "type": "other", - "message": "Replayed 1 `functionCall` part(s) for a Gemini 3 model without a `thoughtSignature` (tools: `get_weather`). Injected the documented `skip_thought_signature_validator` sentinel to keep the request from failing with HTTP 400. The likely cause is application code that drops `providerOptions.google.thoughtSignature` when persisting or serializing assistant tool-call messages. See https://ai.google.dev/gemini-api/docs/thought-signatures." - } - ], - "toolCalls": [ - { - "step": 0, - "toolName": "get_weather", - "thoughtSignature": "present", - "providerMetadata": { - "googleVertex": { - "thoughtSignature": "AY89a18cQIBavtrXtrOBITFPVtz8vFtAN3GAhYMpFToaLZLimjftIYce6awl9EQyMF8=" - }, - "vertex": { - "thoughtSignature": "AY89a18cQIBavtrXtrOBITFPVtz8vFtAN3GAhYMpFToaLZLimjftIYce6awl9EQyMF8=" - } - } - }, - { - "step": 0, - "toolName": "get_weather", - "thoughtSignature": "absent", - "providerMetadata": null - } - ], - "summary": { - "toolCallCount": 2, - "signedToolCallCount": 1, - "unsignedToolCallCount": 1, - "hasSkipThoughtSignatureValidatorWarning": true, - "reproduced": true - } -} diff --git a/packages/google/src/convert-to-google-messages.test.ts b/packages/google/src/convert-to-google-messages.test.ts index 938974940cd8..8406f35a2447 100644 --- a/packages/google/src/convert-to-google-messages.test.ts +++ b/packages/google/src/convert-to-google-messages.test.ts @@ -1,24 +1,9 @@ -import fs from 'node:fs'; import { describe, expect, it, vi } from 'vitest'; import { convertToGoogleMessages, SKIP_THOUGHT_SIGNATURE_VALIDATOR, } from './convert-to-google-messages'; -const issue16298Fixture = JSON.parse( - fs.readFileSync( - 'src/__fixtures__/issue-16298-gemini3-parallel-tool-calls.json', - 'utf8', - ), -) as { - toolCalls: Array<{ - providerMetadata: { - googleVertex?: { thoughtSignature?: string }; - vertex?: { thoughtSignature?: string }; - } | null; - }>; -}; - describe('system messages', () => { it('should store system message in system instruction', async () => { const result = convertToGoogleMessages([ @@ -1702,14 +1687,6 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { }); it('does NOT inject the sentinel or warn for a documented Gemini 3 parallel function-call batch with only the first call signed', () => { - const firstSignature = - issue16298Fixture.toolCalls[0].providerMetadata?.googleVertex - ?.thoughtSignature ?? - issue16298Fixture.toolCalls[0].providerMetadata?.vertex?.thoughtSignature; - - expect(firstSignature).toBeTruthy(); - expect(issue16298Fixture.toolCalls[1].providerMetadata).toBeNull(); - const onWarning = vi.fn(); const result = convertToGoogleMessages( [ @@ -1731,8 +1708,8 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { toolName: 'get_weather', input: { city: 'Paris' }, providerOptions: { - googleVertex: { thoughtSignature: firstSignature }, - vertex: { thoughtSignature: firstSignature }, + googleVertex: { thoughtSignature: 'sig_parallel_batch' }, + vertex: { thoughtSignature: 'sig_parallel_batch' }, }, }, { @@ -1782,7 +1759,7 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { name: 'get_weather', args: { city: 'Paris' }, }, - thoughtSignature: firstSignature, + thoughtSignature: 'sig_parallel_batch', }, { functionCall: { @@ -1790,6 +1767,7 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { name: 'get_weather', args: { city: 'Tokyo' }, }, + thoughtSignature: undefined, }, ]); expect(onWarning).not.toHaveBeenCalled(); diff --git a/packages/google/src/convert-to-google-messages.ts b/packages/google/src/convert-to-google-messages.ts index 2f4fe76e84c1..db03935b3f3c 100644 --- a/packages/google/src/convert-to-google-messages.ts +++ b/packages/google/src/convert-to-google-messages.ts @@ -335,10 +335,16 @@ export function convertToGoogleMessages( case 'assistant': { systemMessagesAllowed = false; + let currentToolCallBatchHasRealThoughtSignature = false; + contents.push({ role: 'model', parts: content .map(part => { + if (part.type !== 'tool-call') { + currentToolCallBatchHasRealThoughtSignature = false; + } + const providerOpts = readProviderOpts(part); const thoughtSignature = providerOpts?.thoughtSignature != null @@ -457,12 +463,22 @@ export function convertToGoogleMessages( providerOpts?.serverToolType != null ? String(providerOpts.serverToolType) : undefined; + const shouldSkipMissingSignatureMitigation = + // Gemini 3 can return a single signature for a parallel + // function-call batch on the first call. Subsequent calls + // in that batch legitimately have no signature to replay. + thoughtSignature == null && + currentToolCallBatchHasRealThoughtSignature; + const effectiveThoughtSignature = thoughtSignature ?? - (isGemini3Model + (isGemini3Model && !shouldSkipMissingSignatureMitigation ? injectSkipSignature(part.toolName) : undefined); + currentToolCallBatchHasRealThoughtSignature ||= + thoughtSignature != null; + if (serverToolCallId && serverToolType) { return { toolCall: { From f2b2f9466e3f34ffa60280e0a52ac9842749872d Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 9 Jul 2026 14:20:29 +0200 Subject: [PATCH 3/4] addressing comments --- ...issue-16298-gemini3-parallel-tool-calls.ts | 10 ++++---- .../src/convert-to-google-messages.test.ts | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts index 093fe6c43ef9..d86587cad875 100644 --- a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts +++ b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts @@ -13,13 +13,13 @@ async function main() { inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ city, - tempC: city === 'Paris' ? 21 : 24, + tempC: city === 'Paris' ? 21 : city === 'Tokyo' ? 24 : 19, conditions: 'sunny', }), }), }, prompt: - 'Use the get_weather tool to check the weather in Paris and Tokyo in parallel in the same assistant step, then summarize.', + 'Use the get_weather tool to check the weather in Paris, Tokyo, and New York. Make exactly three get_weather tool calls in parallel in the same assistant step before you summarize.', }); for await (const _ of result.textStream) { @@ -83,9 +83,9 @@ async function main() { const warningText = JSON.stringify(warnings); const fixed = - toolCallCount >= 2 && + toolCallCount >= 3 && signedToolCallCount >= 1 && - unsignedToolCallCount >= 1 && + unsignedToolCallCount >= 2 && !warningText.includes('skip_thought_signature_validator'); console.log('\nSUMMARY:'); @@ -107,7 +107,7 @@ async function main() { if (!fixed) { throw new Error( - 'Expected parallel Gemini 3 tool calls with one signed call and no skip_thought_signature_validator warning.', + 'Expected at least three parallel Gemini 3 tool calls with one signed call, at least two unsigned calls, and no skip_thought_signature_validator warning.', ); } } diff --git a/packages/google/src/convert-to-google-messages.test.ts b/packages/google/src/convert-to-google-messages.test.ts index 8406f35a2447..ee6a5e8d0c17 100644 --- a/packages/google/src/convert-to-google-messages.test.ts +++ b/packages/google/src/convert-to-google-messages.test.ts @@ -1695,7 +1695,7 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { content: [ { type: 'text', - text: 'Check the weather in Paris and Tokyo.', + text: 'Check the weather in Paris, Tokyo, and New York.', }, ], }, @@ -1718,6 +1718,12 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { toolName: 'get_weather', input: { city: 'Tokyo' }, }, + { + type: 'tool-call', + toolCallId: 'tc_new_york', + toolName: 'get_weather', + input: { city: 'New York' }, + }, ], }, { @@ -1741,6 +1747,15 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { value: { city: 'Tokyo', tempC: 24, conditions: 'sunny' }, }, }, + { + type: 'tool-result', + toolCallId: 'tc_new_york', + toolName: 'get_weather', + output: { + type: 'json', + value: { city: 'New York', tempC: 19, conditions: 'sunny' }, + }, + }, ], }, ], @@ -1769,6 +1784,14 @@ describe('Gemini 3 missing thoughtSignature mitigation', () => { }, thoughtSignature: undefined, }, + { + functionCall: { + id: 'tc_new_york', + name: 'get_weather', + args: { city: 'New York' }, + }, + thoughtSignature: undefined, + }, ]); expect(onWarning).not.toHaveBeenCalled(); }); From 4301f5bc304f4c5a53fc71571cbc5572bc48dfad Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 9 Jul 2026 15:16:04 +0200 Subject: [PATCH 4/4] addressing comments --- ...issue-16298-gemini3-parallel-tool-calls.ts | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts index d86587cad875..6d73ad82e4c9 100644 --- a/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts +++ b/examples/ai-functions/src/reproduction/issue-16298-gemini3-parallel-tool-calls.ts @@ -5,8 +5,8 @@ import { z } from 'zod'; async function main() { const result = streamText({ model: google('gemini-3.1-flash-lite-preview'), - abortSignal: AbortSignal.timeout(60_000), - stopWhen: stepCountIs(3), + abortSignal: AbortSignal.timeout(120_000), + stopWhen: stepCountIs(5), tools: { get_weather: tool({ description: 'Get the current weather for a given city.', @@ -26,19 +26,49 @@ async function main() { // drain } + const finalText = await result.text; const warnings = await result.warnings; const steps = await result.steps; console.log('WARNINGS:'); console.log(JSON.stringify(warnings, null, 2)); + console.log('\nFINAL TEXT:'); + console.log(finalText); + console.log('\nTOOL-CALL PARTS:'); let toolCallCount = 0; let signedToolCallCount = 0; let unsignedToolCallCount = 0; + let toolResultCount = 0; + let multiStepRoundtrip = false; for (const [stepIndex, step] of steps.entries()) { + const stepToolCallCount = step.content.filter( + part => part.type === 'tool-call', + ).length; + const stepToolResultCount = step.content.filter( + part => part.type === 'tool-result', + ).length; + + if (stepToolCallCount >= 3 && stepToolResultCount >= 3) { + multiStepRoundtrip = + multiStepRoundtrip || + steps + .slice(stepIndex + 1) + .some(laterStep => + laterStep.content.some( + part => part.type === 'text' && part.text.trim().length > 0, + ), + ); + } + for (const part of step.content) { + if (part.type === 'tool-result') { + toolResultCount++; + continue; + } + if (part.type !== 'tool-call') { continue; } @@ -84,17 +114,23 @@ async function main() { const warningText = JSON.stringify(warnings); const fixed = toolCallCount >= 3 && + toolResultCount >= 3 && signedToolCallCount >= 1 && unsignedToolCallCount >= 2 && + multiStepRoundtrip && !warningText.includes('skip_thought_signature_validator'); console.log('\nSUMMARY:'); console.log( JSON.stringify( { + stepCount: steps.length, toolCallCount, + toolResultCount, signedToolCallCount, unsignedToolCallCount, + finalTextPresent: finalText.trim().length > 0, + multiStepRoundtrip, hasSkipThoughtSignatureValidatorWarning: warningText.includes( 'skip_thought_signature_validator', ), @@ -107,7 +143,7 @@ async function main() { if (!fixed) { throw new Error( - 'Expected at least three parallel Gemini 3 tool calls with one signed call, at least two unsigned calls, and no skip_thought_signature_validator warning.', + 'Expected a Gemini 3 multi-step roundtrip with at least three parallel tool calls, matching tool results, one signed call, at least two unsigned calls, final text, and no skip_thought_signature_validator warning.', ); } }