Skip to content

Commit 4990150

Browse files
jherrclaudeautofix-ci[bot]
authored
fix(ai-gemini): remove redundant text part from functionResponse messages (#448)
* 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) <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent ddd0386 commit 4990150

File tree

3 files changed

+20
-7
lines changed

3 files changed

+20
-7
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai-gemini': patch
3+
---
4+
5+
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.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ export class GeminiTextAdapter<
562562
for (const contentPart of msg.content) {
563563
parts.push(this.convertContentPartToGemini(contentPart))
564564
}
565-
} else if (msg.content) {
565+
} else if (msg.content && msg.role !== 'tool') {
566566
parts.push({ text: msg.content })
567567
}
568568

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -394,12 +394,19 @@ describe('GeminiAdapter through AI', () => {
394394
expect(payload.contents[1].role).toBe('model')
395395
expect(payload.contents[2].role).toBe('user')
396396

397-
// Last user message should contain both functionResponse and text
397+
// Last user message should contain functionResponse (no redundant text part
398+
// for the tool result) and the follow-up user text
398399
const lastParts = payload.contents[2].parts
399400
const hasFunctionResponse = lastParts.some((p: any) => p.functionResponse)
400-
const hasText = lastParts.some((p: any) => p.text === 'What about Paris?')
401+
const hasFollowUp = lastParts.some(
402+
(p: any) => p.text === 'What about Paris?',
403+
)
404+
const hasToolResultText = lastParts.some(
405+
(p: any) => p.text === '{"temp":72}',
406+
)
401407
expect(hasFunctionResponse).toBe(true)
402-
expect(hasText).toBe(true)
408+
expect(hasFollowUp).toBe(true)
409+
expect(hasToolResultText).toBe(false)
403410
})
404411

405412
it('handles full multi-turn with duplicate tool results and empty model message', async () => {
@@ -487,15 +494,16 @@ describe('GeminiAdapter through AI', () => {
487494
expect(payload.contents).toHaveLength(3)
488495

489496
// Last user should have deduplicated functionResponses + follow-up text
497+
// (no redundant text parts for tool results)
490498
const lastParts = payload.contents[2].parts
491499
const functionResponses = lastParts.filter((p: any) => p.functionResponse)
492500
// 2 unique tool call IDs, not 3 (duplicate removed)
493501
expect(functionResponses).toHaveLength(2)
494502

495-
const textParts = lastParts.filter(
496-
(p: any) => p.text === "what's a good electric guitar?",
497-
)
503+
const textParts = lastParts.filter((p: any) => p.text)
504+
// Only the follow-up user message text, no tool result text parts
498505
expect(textParts).toHaveLength(1)
506+
expect(textParts[0].text).toBe("what's a good electric guitar?")
499507
})
500508

501509
it('preserves thoughtSignature in functionCall parts when sending history back to Gemini', async () => {

0 commit comments

Comments
 (0)