Skip to content

fix(ai-gemini): dedup functionResponse by id, not name#960

Open
feiiiiii5 wants to merge 1 commit into
TanStack:mainfrom
feiiiiii5:fix/ai-gemini-parallel-function-response-dedup
Open

fix(ai-gemini): dedup functionResponse by id, not name#960
feiiiiii5 wants to merge 1 commit into
TanStack:mainfrom
feiiiiii5:fix/ai-gemini-parallel-function-response-dedup

Conversation

@feiiiiii5

@feiiiiii5 feiiiiii5 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

GeminiTextAdapter.mergeConsecutiveSameRoleMessages deduplicated functionResponse parts by tool name. When Gemini returned two or more parallel calls to the same tool in one turn, the second matching response was filtered out, so the follow-up request 400'd:

INVALID_ARGUMENT: Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn.

Fixes #894.

Root cause

packages/ai-gemini/src/adapters/text.ts, mergeConsecutiveSameRoleMessages (~L852):

const seenFunctionResponseNames = new Set<string>()
msg.parts = msg.parts.filter((part) => {
  if ('functionResponse' in part && part.functionResponse?.name) {
    if (seenFunctionResponseNames.has(part.functionResponse.name)) {
      return false
    }
    seenFunctionResponseNames.add(part.functionResponse.name)
  }
  return true
})

Two parallel calls to the same tool share a name but have different ids, so the second response is dropped. The comment above the filter already called the key "tool call ID", but the implementation was actually keying on name.

Each functionResponse is built upstream with a unique id set to the originating toolCallId (L785 / L794), so the unique-per-call invariant already holds at the data layer — the dedup just wasn't using it.

Fix

Key the dedup on part.functionResponse.id instead of .name:

  • Different ids (parallel calls to the same tool) → both kept.
  • Same id (genuine duplicate, e.g. a caller re-sends the same toolCallId's result) → second one still dropped, dedup guarantee preserved.
  • Pre-existing comment describing the key as "tool call ID" is now accurate.

Tests

New packages/ai-gemini/tests/parallel-function-response.test.ts covers four cases:

# Scenario Pre-fix Post-fix
1 Two parallel calls, same tool name, distinct ids ❌ drops one ✅ keeps both
2 Genuine duplicate (same toolCallId re-sent) ✅ collapses to one ✅ collapses to one
3 Two parallel calls, different tool names ✅ keeps both ✅ keeps both
4 Three parallel calls, same tool name, distinct ids ❌ collapses to one ✅ keeps all three

formatMessages is private on the adapter (TS2341 blocks subclass access, so the Probe pattern used in ai-bedrock/tests for protected hooks doesn't apply). The test casts through unknown to a minimal typed shape — keeps the assertion type-safe without widening the adapter's public API just to make a private method testable.

Backward compatibility

mergeConsecutiveSameRoleMessages is a private method; the change is observable only through the wire payload sent to Gemini. The set of cases that change behavior:

  • Pre-fix: parallel calls to the same tool were broken (400 from Gemini). Post-fix: they work.
  • Pre-fix: a genuine duplicate functionResponse (same toolCallId re-sent) was collapsed. Post-fix: still collapsed (now keyed on id).

No caller's behavior regresses; only previously-broken paths become correct.

Verification

  • pnpm --filter @tanstack/ai-gemini test:lib — 241/241 passing (including the 4 new tests).
  • pnpm --filter @tanstack/ai-gemini test:eslint — 0 errors (7 pre-existing warnings in unrelated files).
  • pnpm --filter @tanstack/ai-gemini test:typestsc clean.
  • Red→green verified: temporarily reverted the dedup key back to name; cases 1 and 4 fail with the exact symptom from the issue (expected ['call_1'] to deeply equal ['call_1', 'call_2']). Restored the fix; all 4 cases pass.

Out of scope

Changeset

.changeset/ai-gemini-parallel-function-response-dedup.md@tanstack/ai-gemini: patch.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where parallel tool/function calls could be incorrectly dropped when they shared the same tool name.
    • Deduplication now uses each call’s identifier, so distinct parallel calls are retained while true duplicates are still collapsed.
  • Tests
    • Added new test coverage for parallel scenarios (two and three calls) and verification of proper duplicate handling.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8170153d-fbe7-4f5d-91b2-307cb69df5d4

📥 Commits

Reviewing files that changed from the base of the PR and between f4119be and eb10a60.

📒 Files selected for processing (3)
  • .changeset/ai-gemini-parallel-function-response-dedup.md
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/tests/parallel-function-response.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/ai-gemini-parallel-function-response-dedup.md
  • packages/ai-gemini/tests/parallel-function-response.test.ts
  • packages/ai-gemini/src/adapters/text.ts

📝 Walkthrough

Walkthrough

The Gemini text adapter now deduplicates functionResponse parts by tool call ID rather than tool name. Tests cover distinct parallel calls, repeated IDs, different tools, and three-call scenarios, with a patch changeset documenting the fix.

Changes

Gemini response handling

Layer / File(s) Summary
Deduplicate responses by call ID
packages/ai-gemini/src/adapters/text.ts, .changeset/...
mergeConsecutiveSameRoleMessages now tracks functionResponse.id, preserving same-name responses with distinct IDs while collapsing repeated IDs.
Validate parallel response behavior
packages/ai-gemini/tests/parallel-function-response.test.ts
Adds formatting helpers and tests for two- and three-call parallel response deduplication scenarios.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: deduplicating functionResponse by id instead of name.
Description check ✅ Passed The description covers the bug, root cause, fix, tests, verification, and changeset, so it is sufficiently complete.
Linked Issues check ✅ Passed The PR implements the linked issue's required behavior change and adds tests covering the parallel-call mismatch scenario.
Out of Scope Changes check ✅ Passed The added changeset and tests are directly related to the fix, with no unrelated scope evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-gemini/tests/parallel-function-response.test.ts`:
- Line 23: Update the return type of functionResponseIds to use Array<string>
instead of string[], preserving the function’s behavior.
- Around line 1-4: Move parallel-function-response.test.ts alongside the covered
GeminiTextAdapter source under src/adapters, rename it appropriately if needed,
and update its relative imports to match the new location. Ensure imports in the
relocated test follow the project’s import/order convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea858c55-5484-439f-8030-5581672eab46

📥 Commits

Reviewing files that changed from the base of the PR and between bf870fa and f4119be.

📒 Files selected for processing (3)
  • .changeset/ai-gemini-parallel-function-response-dedup.md
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/tests/parallel-function-response.test.ts

Comment on lines +1 to +4
import { describe, expect, it } from 'vitest'
import type { ModelMessage } from '@tanstack/ai'
import type { Content } from '@google/genai'
import { GeminiTextAdapter } from '../src/adapters/text'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move test file alongside the source it covers and fix import order.

As per coding guidelines, "Place unit tests in *.test.ts files alongside the source they cover." This test file is currently in the tests/ directory but covers src/adapters/text.ts. Please move this file to packages/ai-gemini/src/adapters/ (e.g., as text.test.ts) and update the relative imports accordingly. (Note: While previous conventions placed tests in a dedicated tests/ directory as noted in retrieved learnings, this explicit coding guideline takes precedence).

Additionally, static analysis indicates an import/order violation. If you keep the file in its current location temporarily, ensure the imports are ordered correctly.

🐛 Proposed fix for import order
 import { describe, expect, it } from 'vitest'
+import { GeminiTextAdapter } from '../src/adapters/text'
 import type { ModelMessage } from '`@tanstack/ai`'
 import type { Content } from '`@google/genai`'
-import { GeminiTextAdapter } from '../src/adapters/text'
🧰 Tools
🪛 ESLint

[error] 4-4: ../src/adapters/text import should occur before type import of @tanstack/ai

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-gemini/tests/parallel-function-response.test.ts` around lines 1 -
4, Move parallel-function-response.test.ts alongside the covered
GeminiTextAdapter source under src/adapters, rename it appropriately if needed,
and update its relative imports to match the new location. Ensure imports in the
relocated test follow the project’s import/order convention.

Sources: Coding guidelines, Learnings, Linters/SAST tools

Comment thread packages/ai-gemini/tests/parallel-function-response.test.ts Outdated
`GeminiTextAdapter.mergeConsecutiveSameRoleMessages` deduplicated
`functionResponse` parts by tool name. When Gemini returned two or more
parallel calls to the same tool in one turn, the second matching
response was filtered out, so the follow-up request 400'd:

  INVALID_ARGUMENT: Please ensure that the number of function response
  parts is equal to the number of function call parts of the function
  call turn.

Each `functionResponse` is already built with a unique `id` (the
originating `toolCallId`), so keying the dedup on `id` keeps parallel
calls distinct (different ids → both kept) while still collapsing a
genuine duplicate (same id → second one dropped). The pre-existing
comment already described the key as "tool call ID" — this brings the
implementation in line with that comment.

Fixes TanStack#894.
@feiiiiii5
feiiiiii5 force-pushed the fix/ai-gemini-parallel-function-response-dedup branch from f4119be to eb10a60 Compare July 19, 2026 05:06
@feiiiiii5

Copy link
Copy Markdown
Author

@coderabbitai thanks for the review. Both lint nitpicks addressed in eb10a60:

1. Array<string> instead of string[] — applied as suggested at functionResponseIds's return type. npx eslint packages/ai-gemini/tests/parallel-function-response.test.ts now reports 0 errors.

2. import/order violation — applied as suggested. import { GeminiTextAdapter } from '../src/adapters/text' now precedes the type imports from @tanstack/ai and @google/genai.

On moving the test to src/adapters/text.test.ts — respectfully skipping. The existing convention in this package is tests/*.test.ts (14 sibling test files: gemini-adapter.test.ts, audio-adapter.test.ts, image-adapter.test.ts, realtime-parse.test.ts, etc.), and packages/ai-bedrock/tests/ mirrors the same layout. Moving just this one file would split the test suite across two locations. Happy to revisit if a maintainer prefers colocating tests with source as a package-wide refactor.

Local verification on eb10a60:

  • npx eslint packages/ai-gemini/tests/parallel-function-response.test.ts → 0 errors, 0 warnings
  • pnpm --filter @tanstack/ai-gemini test:lib → 241/241 passing (including the 4 new cases)
  • pnpm --filter @tanstack/ai-gemini test:typestsc clean

(Side note: test:eslint script in package.json is scoped to eslint ./src, which is why these test-file lint errors weren't surfaced locally on the first push — that's a package-level script gap, not a fix for this PR.)

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@feiiiiii5 Thanks for addressing the lint findings and for the thorough verification.

Agreed on retaining packages/ai-gemini/tests/: following the existing package-wide test layout is preferable to introducing a one-off colocated test. I’m withdrawing that suggestion.

🐇 ✅


✏️ Learnings added
Learnt from: feiiiiii5
URL: https://github.com/TanStack/ai/pull/960

Timestamp: 2026-07-19T05:06:44.776Z
Learning: For the `tanstack/ai-gemini` package, tests conventionally live in `packages/ai-gemini/tests/` rather than alongside source files. This layout is shared by existing Gemini tests and mirrored by `packages/ai-bedrock/tests/`; do not recommend moving an individual test into `src/` unless the package-wide convention is intentionally being changed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ai-gemini: parallel calls to the same tool drop a functionResponse part, causing Gemini 400 (function response/call part count mismatch)

1 participant