From 16609f7603e14ba8eb262a80c71ae9a91c973784 Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Mon, 13 Apr 2026 10:39:58 -0700 Subject: [PATCH 1/2] fix(ai-gemini): remove redundant text part from functionResponse messages (#436) Tool result messages were emitting both a text part and a functionResponse part, causing 400 errors on newer Gemini models that reject mixed parts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-gemini-function-response.md | 5 +++++ .../typescript/ai-gemini/src/adapters/text.ts | 2 +- .../ai-gemini/tests/gemini-adapter.test.ts | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-gemini-function-response.md diff --git a/.changeset/fix-gemini-function-response.md b/.changeset/fix-gemini-function-response.md new file mode 100644 index 000000000..4c96862fd --- /dev/null +++ b/.changeset/fix-gemini-function-response.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-gemini': patch +--- + +Fix 400 error when sending tool results to Gemini API by removing redundant text part from functionResponse messages. Newer models (gemini-3.1-flash-lite, gemma-4) reject messages that mix text and functionResponse parts. diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts index 844c4a12f..4430534c0 100644 --- a/packages/typescript/ai-gemini/src/adapters/text.ts +++ b/packages/typescript/ai-gemini/src/adapters/text.ts @@ -562,7 +562,7 @@ export class GeminiTextAdapter< for (const contentPart of msg.content) { parts.push(this.convertContentPartToGemini(contentPart)) } - } else if (msg.content) { + } else if (msg.content && msg.role !== 'tool') { parts.push({ text: msg.content }) } diff --git a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts index 8f1aabefb..f9e5197fa 100644 --- a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts +++ b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts @@ -394,12 +394,17 @@ describe('GeminiAdapter through AI', () => { expect(payload.contents[1].role).toBe('model') expect(payload.contents[2].role).toBe('user') - // Last user message should contain both functionResponse and text + // Last user message should contain functionResponse (no redundant text part + // for the tool result) and the follow-up user text const lastParts = payload.contents[2].parts const hasFunctionResponse = lastParts.some((p: any) => p.functionResponse) - const hasText = lastParts.some((p: any) => p.text === 'What about Paris?') + const hasFollowUp = lastParts.some((p: any) => p.text === 'What about Paris?') + const hasToolResultText = lastParts.some( + (p: any) => p.text === '{"temp":72}', + ) expect(hasFunctionResponse).toBe(true) - expect(hasText).toBe(true) + expect(hasFollowUp).toBe(true) + expect(hasToolResultText).toBe(false) }) it('handles full multi-turn with duplicate tool results and empty model message', async () => { @@ -487,15 +492,16 @@ describe('GeminiAdapter through AI', () => { expect(payload.contents).toHaveLength(3) // Last user should have deduplicated functionResponses + follow-up text + // (no redundant text parts for tool results) const lastParts = payload.contents[2].parts const functionResponses = lastParts.filter((p: any) => p.functionResponse) // 2 unique tool call IDs, not 3 (duplicate removed) expect(functionResponses).toHaveLength(2) - const textParts = lastParts.filter( - (p: any) => p.text === "what's a good electric guitar?", - ) + const textParts = lastParts.filter((p: any) => p.text) + // Only the follow-up user message text, no tool result text parts expect(textParts).toHaveLength(1) + expect(textParts[0].text).toBe("what's a good electric guitar?") }) it('preserves thoughtSignature in functionCall parts when sending history back to Gemini', async () => { From c2ce593e80774bb57641df5117835fbe2470996e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:41:41 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- packages/typescript/ai-gemini/tests/gemini-adapter.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts index f9e5197fa..d713cfc1f 100644 --- a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts +++ b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts @@ -398,7 +398,9 @@ describe('GeminiAdapter through AI', () => { // for the tool result) and the follow-up user text const lastParts = payload.contents[2].parts const hasFunctionResponse = lastParts.some((p: any) => p.functionResponse) - const hasFollowUp = lastParts.some((p: any) => p.text === 'What about Paris?') + const hasFollowUp = lastParts.some( + (p: any) => p.text === 'What about Paris?', + ) const hasToolResultText = lastParts.some( (p: any) => p.text === '{"temp":72}', )