Skip to content

Commit 6858973

Browse files
committed
refactor: use typed Part interface instead of any casts
Per review feedback: use the @google/genai typed `Part` interface directly instead of `as any` casts. - Read side: `part.thoughtSignature` is properly typed on `Part`, so the cast is removed entirely. The Gemini 2.x fallback to `functionCall.thoughtSignature` is also removed since the SDK has never typed it there and Gemini has always emitted it at Part level. - Write side: construct a typed `Part` and conditionally assign `thoughtSignature`, avoiding the `as Part` cast on a spread literal. The only remaining `as any` in this area is the pre-existing functionResponse cast, which is unrelated to this fix.
1 parent 01814f3 commit 6858973

3 files changed

Lines changed: 24 additions & 26 deletions

File tree

.changeset/fix-gemini-thought-signature-part-level.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
'@tanstack/ai-gemini': patch
33
---
44

5-
fix(ai-gemini): read/write thoughtSignature at Part level for Gemini 3.x
5+
fix(ai-gemini): read/write thoughtSignature at Part level
66

7-
Gemini 3.x models emit `thoughtSignature` as a Part-level sibling of `functionCall` (per the `@google/genai` `Part` type definition), not nested inside `functionCall`. The adapter was reading from `functionCall.thoughtSignature` (which doesn't exist in the SDK types) and writing it back nested inside `functionCall`, causing the Gemini API to reject subsequent tool-call turns with `400 INVALID_ARGUMENT: "Function call is missing a thought_signature"`.
7+
Gemini emits `thoughtSignature` as a Part-level sibling of `functionCall` (per the `@google/genai` `Part` type definition), not nested inside `functionCall`. The `FunctionCall` type has never had a `thoughtSignature` property. The adapter was reading from `functionCall.thoughtSignature` (which doesn't exist in the SDK types) and writing it back nested inside `functionCall`, causing Gemini 3.x to reject subsequent tool-call turns with `400 INVALID_ARGUMENT: "Function call is missing a thought_signature"`.
88

99
This fix:
1010

11-
- **Read side:** reads `part.thoughtSignature` first, falls back to `functionCall.thoughtSignature` for older Gemini 2.x models
12-
- **Write side:** emits `thoughtSignature` as a Part-level sibling of `functionCall` instead of nesting it inside
11+
- **Read side:** reads `part.thoughtSignature` directly, using the SDK's typed `Part` interface
12+
- **Write side:** emits `thoughtSignature` as a Part-level sibling of `functionCall`, using the SDK's typed `Part` interface

packages/typescript/ai-gemini/src/adapters/text.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -385,14 +385,10 @@ export class GeminiTextAdapter<
385385
`${functionCall.name}_${Date.now()}_${nextToolIndex}`
386386
const functionArgs = functionCall.args || {}
387387

388-
// Gemini 3.x emits thoughtSignature as a Part-level sibling of
389-
// functionCall (see @google/genai Part type), not nested inside
390-
// functionCall. Read from the Part first, fall back to
391-
// functionCall for older Gemini 2.x models.
392-
const partThoughtSignature =
393-
(part as any).thoughtSignature ||
394-
(functionCall as any).thoughtSignature ||
395-
undefined
388+
// Gemini emits thoughtSignature as a Part-level sibling of
389+
// functionCall (per @google/genai Part type), not nested inside
390+
// functionCall itself.
391+
const partThoughtSignature = part.thoughtSignature || undefined
396392

397393
let toolCallData = toolCallMap.get(toolCallId)
398394
if (!toolCallData) {
@@ -720,18 +716,21 @@ export class GeminiTextAdapter<
720716

721717
const thoughtSignature = toolCall.providerMetadata
722718
?.thoughtSignature as string | undefined
723-
// Gemini 3.x requires thoughtSignature at the Part level (sibling
724-
// of functionCall), not nested inside functionCall. Nesting it
725-
// causes the API to reject the next turn with
719+
// Gemini requires thoughtSignature at the Part level (sibling of
720+
// functionCall), not nested inside functionCall. Nesting it causes
721+
// the API to reject the next turn with
726722
// "Function call is missing a thought_signature".
727-
parts.push({
723+
const part: Part = {
728724
functionCall: {
729725
id: toolCall.id,
730726
name: toolCall.function.name,
731727
args: parsedArgs,
732728
},
733-
...(thoughtSignature && { thoughtSignature }),
734-
} as Part)
729+
}
730+
if (thoughtSignature) {
731+
part.thoughtSignature = thoughtSignature
732+
}
733+
parts.push(part)
735734
}
736735
}
737736

packages/typescript/ai-gemini/tests/gemini-adapter.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,9 @@ describe('GeminiAdapter through AI', () => {
592592
expect(functionCallPart.functionCall.thoughtSignature).toBeUndefined()
593593
})
594594

595-
it('falls back to functionCall.thoughtSignature for Gemini 2.x models', async () => {
596-
const thoughtSig = 'legacy-thought-signature'
597-
598-
// Gemini 2.x nests thoughtSignature inside functionCall
595+
it('ignores thoughtSignature nested inside functionCall (not part of @google/genai Part type)', async () => {
596+
// The @google/genai SDK has never typed thoughtSignature on FunctionCall;
597+
// it only exists on Part. A nested value should be ignored.
599598
const firstStream = [
600599
{
601600
candidates: [
@@ -604,10 +603,10 @@ describe('GeminiAdapter through AI', () => {
604603
parts: [
605604
{
606605
functionCall: {
607-
id: 'fc_legacy',
606+
id: 'fc_nested',
608607
name: 'sum_tool',
609608
args: { numbers: [3, 4] },
610-
thoughtSignature: thoughtSig,
609+
thoughtSignature: 'should-be-ignored',
611610
},
612611
},
613612
],
@@ -671,8 +670,8 @@ describe('GeminiAdapter through AI', () => {
671670

672671
const functionCallPart = modelTurn.parts.find((p: any) => p.functionCall)
673672
expect(functionCallPart).toBeDefined()
674-
// Even for legacy input, the write side should emit at Part level
675-
expect(functionCallPart.thoughtSignature).toBe(thoughtSig)
673+
// No thoughtSignature should be emitted since none was at Part level
674+
expect(functionCallPart.thoughtSignature).toBeUndefined()
676675
expect(functionCallPart.functionCall.thoughtSignature).toBeUndefined()
677676
})
678677

0 commit comments

Comments
 (0)