Skip to content

Commit ab05e4c

Browse files
author
Nathan Young
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 48cf4a2 commit ab05e4c

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
@@ -351,14 +351,10 @@ export class GeminiTextAdapter<
351351
`${functionCall.name}_${Date.now()}_${nextToolIndex}`
352352
const functionArgs = functionCall.args || {}
353353

354-
// Gemini 3.x emits thoughtSignature as a Part-level sibling of
355-
// functionCall (see @google/genai Part type), not nested inside
356-
// functionCall. Read from the Part first, fall back to
357-
// functionCall for older Gemini 2.x models.
358-
const partThoughtSignature =
359-
(part as any).thoughtSignature ||
360-
(functionCall as any).thoughtSignature ||
361-
undefined
354+
// Gemini emits thoughtSignature as a Part-level sibling of
355+
// functionCall (per @google/genai Part type), not nested inside
356+
// functionCall itself.
357+
const partThoughtSignature = part.thoughtSignature || undefined
362358

363359
let toolCallData = toolCallMap.get(toolCallId)
364360
if (!toolCallData) {
@@ -686,18 +682,21 @@ export class GeminiTextAdapter<
686682

687683
const thoughtSignature = toolCall.providerMetadata
688684
?.thoughtSignature as string | undefined
689-
// Gemini 3.x requires thoughtSignature at the Part level (sibling
690-
// of functionCall), not nested inside functionCall. Nesting it
691-
// causes the API to reject the next turn with
685+
// Gemini requires thoughtSignature at the Part level (sibling of
686+
// functionCall), not nested inside functionCall. Nesting it causes
687+
// the API to reject the next turn with
692688
// "Function call is missing a thought_signature".
693-
parts.push({
689+
const part: Part = {
694690
functionCall: {
695691
id: toolCall.id,
696692
name: toolCall.function.name,
697693
args: parsedArgs,
698694
},
699-
...(thoughtSignature && { thoughtSignature }),
700-
} as Part)
695+
}
696+
if (thoughtSignature) {
697+
part.thoughtSignature = thoughtSignature
698+
}
699+
parts.push(part)
701700
}
702701
}
703702

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)