Skip to content

fix(gemini): keep streamed tool_call index/id stable across chunks#1159

Open
tbille wants to merge 2 commits into
mainfrom
fix/gemini-streaming-tool-call-index
Open

fix(gemini): keep streamed tool_call index/id stable across chunks#1159
tbille wants to merge 2 commits into
mainfrom
fix/gemini-streaming-tool-call-index

Conversation

@tbille

@tbille tbille commented Jul 1, 2026

Copy link
Copy Markdown
Member

Description

Fixes streamed tool call index/id for the Gemini provider.

_create_openai_chunk_from_google_chunk converts one Google GenerateContentResponse chunk at a time, but it was building ChoiceDeltaToolCall.index (and the generated id) from a list scoped to that single chunk. In a real stream, each chunk is converted independently, so:

  • index reset to 0/1/... for every chunk instead of tracking position across the whole assistant turn.
  • Repeated calls to the same function across different chunks (e.g. three separate read_file calls, as in the issue's example) got an identical id (hash(name) + position-within-chunk), since the per-chunk counter always restarted at 0.

Downstream consumers that merge streamed tool-call deltas by index/id can therefore overwrite or misassociate arguments between tool calls.

Fix

Thread a mutable tool_call_counter (a single-element list) through the stream, created once per _stream() call and passed into _convert_completion_chunk_response(..., tool_call_counter=...) on every chunk. This mirrors the tool_index_map pattern already used by the Bedrock provider (src/any_llm/providers/bedrock/utils.py / bedrock.py) to keep tool-call indices stable across a stream. The counter is optional (defaults to a fresh [0]) so existing single-chunk call sites/tests are unaffected.

PR Type

  • 🐛 Bug Fix

Relevant issues

Fixes #1148

Checklist

  • I understand the code I am submitting.
  • I have added unit tests that prove my fix/feature works
  • I have run this code locally and verified it fixes the issue.
  • New and existing tests pass locally
  • Documentation was updated where necessary
  • I have read and followed the contribution guidelines
  • AI Usage:
    • No AI was used.
    • AI was used for drafting/refactoring.
    • This is fully AI-generated.

AI Usage Information

  • AI Model used: Claude Sonnet 5

  • AI Developer Tool used: OpenCode

  • Any other info you'd like to share: Investigated the issue, designed and implemented the fix (following the existing tool_index_map pattern from the Bedrock provider), and added regression tests reproducing the reported scenario.

  • I am an AI Agent filling out this form (check box if true)

Summary by CodeRabbit

  • Bug Fixes

    • Improved streamed Gemini tool-call handling by keeping tool-call numbering and generated IDs stable across multi-chunk responses.
    • Fixed tool-call numbering to continue sequentially, including when later chunks contain parallel tool calls.
  • Tests

    • Added regression coverage to verify streamed tool-call index and ID stability across both sequential and parallel chunk scenarios.

@tbille tbille temporarily deployed to integration-tests July 1, 2026 07:37 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

A mutable tool_call_counter is threaded through Gemini streaming chunk conversion so tool-call index and id values stay consistent across streamed chunks. The provider wiring and chunk builder both use the shared counter, and regression tests cover the streaming path.

Changes

Gemini streaming tool-call indexing

Layer / File(s) Summary
Counter-based chunk conversion
src/any_llm/providers/gemini/utils.py
_create_openai_chunk_from_google_chunk accepts an optional tool_call_counter, defaults it to [0] when absent, and uses the shared counter for ChoiceDeltaToolCall.index and generated id values.
Streaming wiring
src/any_llm/providers/gemini/base.py
_acompletion initialises a shared tool_call_counter for streamed responses, and _convert_completion_chunk_response forwards that counter into the chunk builder for each chunk.
Regression tests
tests/unit/providers/test_gemini_provider.py
Adds a helper for single-tool-call mock chunks and regression tests covering sequential indices, multi-tool-call continuation, and end-to-end streamed provider behaviour.

Related Issues: #1148
Suggested Labels: bug, gemini, streaming
Suggested Reviewers: none identified from provided context

Sequence Diagram(s)

sequenceDiagram
  participant _acompletion as _acompletion
  participant _convert_completion_chunk_response as _convert_completion_chunk_response
  participant _create_openai_chunk_from_google_chunk as _create_openai_chunk_from_google_chunk

  _acompletion->>_acompletion: initialise tool_call_counter
  loop each streamed chunk
    _acompletion->>_convert_completion_chunk_response: convert chunk with counter
    _convert_completion_chunk_response->>_create_openai_chunk_from_google_chunk: forward tool_call_counter
    _create_openai_chunk_from_google_chunk->>_create_openai_chunk_from_google_chunk: assign index/id from counter
    _create_openai_chunk_from_google_chunk->>_create_openai_chunk_from_google_chunk: increment counter
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the Gemini streamed tool-call index/id stability fix.
Description check ✅ Passed The PR description follows the template and covers the change, type, linked issue, checklist, and AI usage details.
Linked Issues check ✅ Passed The code changes satisfy #1148 by keeping streamed Gemini tool-call indices and IDs stable across chunks.
Out of Scope Changes check ✅ Passed No unrelated changes are evident beyond the Gemini streaming fix and its regression tests.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gemini-streaming-tool-call-index

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.

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/any_llm/providers/gemini/base.py 86.79% <100.00%> (-6.37%) ⬇️
src/any_llm/providers/gemini/utils.py 82.80% <100.00%> (-3.98%) ⬇️

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/unit/providers/test_gemini_provider.py`:
- Around line 940-1012: Add an async provider-level streaming test that
exercises GoogleProvider._acompletion end-to-end, not just
_create_openai_chunk_from_google_chunk(). Mock generate_content_stream() and
verify multiple yielded chunks keep tool_call_counter state across the stream so
tool-call indices increment correctly and ids remain unique. Use the existing
GoogleProvider._acompletion and generate_content_stream symbols to catch any
counter reset or kwargs-forwarding regression in the new plumbing.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74f959ea-cc5e-4b9e-9221-4c031a11b4e6

📥 Commits

Reviewing files that changed from the base of the PR and between 02c9fc4 and ae4ecfb.

📒 Files selected for processing (3)
  • src/any_llm/providers/gemini/base.py
  • src/any_llm/providers/gemini/utils.py
  • tests/unit/providers/test_gemini_provider.py

Comment thread tests/unit/providers/test_gemini_provider.py
tbille added 2 commits July 1, 2026 10:22
Each chunk in a Gemini stream was previously converted independently
in _create_openai_chunk_from_google_chunk, so tool_calls_list was a
fresh list per chunk. This reset ChoiceDeltaToolCall.index to 0 for
every chunk instead of tracking position across the whole assistant
turn, and also caused repeated calls to the same function across
different chunks to receive an identical id (hash(name) + position
within the chunk).

Thread a mutable tool_call_counter through the stream (mirroring the
tool_index_map pattern already used by the bedrock provider) so index
and id stay unique and stable for the life of the stream.

Fixes #1148
Address CodeRabbit review comment and Codecov patch-coverage gap on
src/any_llm/providers/gemini/base.py by adding a test that exercises
GoogleProvider._acompletion with stream=True end-to-end (mocking
generate_content_stream), rather than only calling the pure
_create_openai_chunk_from_google_chunk helper directly. This verifies
tool_call_counter is correctly threaded through
_convert_completion_chunk_response's **kwargs across streamed chunks.
@tbille tbille force-pushed the fix/gemini-streaming-tool-call-index branch from ae4ecfb to f99c297 Compare July 1, 2026 08:25
@tbille tbille temporarily deployed to integration-tests July 1, 2026 08:25 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/unit/providers/test_gemini_provider.py`:
- Around line 1014-1016: The async helper `_async_iter_chunks` is annotated too
loosely with `Any`, which triggers Ruff ANN401 and weakens type safety. Update
its return type to a concrete async iterator type like `AsyncIterator[Mock]`,
and make sure the annotation matches the yielded `Mock` items in the helper.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 273fd1b1-8c73-4d7f-bc7c-ca595789a97b

📥 Commits

Reviewing files that changed from the base of the PR and between ae4ecfb and f99c297.

📒 Files selected for processing (3)
  • src/any_llm/providers/gemini/base.py
  • src/any_llm/providers/gemini/utils.py
  • tests/unit/providers/test_gemini_provider.py

Comment on lines +1014 to +1016
async def _async_iter_chunks(items: list[Mock]) -> Any:
for item in items:
yield item

Copy link
Copy Markdown

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

Tighten the async helper return type.

Line 1014 returns Any, which trips Ruff ANN401 and weakens type checking in this helper. Use a concrete async iterator type such as AsyncIterator[Mock] instead.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 1014-1014: Dynamically typed expressions (typing.Any) are disallowed in _async_iter_chunks

(ANN401)

🤖 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 `@tests/unit/providers/test_gemini_provider.py` around lines 1014 - 1016, The
async helper `_async_iter_chunks` is annotated too loosely with `Any`, which
triggers Ruff ANN401 and weakens type safety. Update its return type to a
concrete async iterator type like `AsyncIterator[Mock]`, and make sure the
annotation matches the yielded `Mock` items in the helper.

Source: Linters/SAST tools

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.

[BUG] Wrong ChoiceDeltaToolCall indexing for Gemini provider

1 participant