Skip to content

Support tool calling while streaming Gemini responses - #5

Merged
ActuallyTaylor merged 3 commits into
mainfrom
feature/gemini-streaming-tool-calling
Aug 3, 2026
Merged

Support tool calling while streaming Gemini responses#5
ActuallyTaylor merged 3 commits into
mainfrom
feature/gemini-streaming-tool-calling

Conversation

@ActuallyTaylor

Copy link
Copy Markdown
Collaborator

streamResponse handled only .text parts, so functionCall parts were silently dropped and tools never fired when streaming. respond already had a complete loop.

Streaming now runs a turn loop over a locally maintained contents array: it collects function calls alongside text, executes them through the shared resolveToolCalls, appends the model turn and a user turn carrying the functionResponse parts, and re-requests until a turn ends without calls. Tool activity reaches the transcript while the stream is running, with the tool calls entry ahead of its outputs, and a .stop decision finishes the stream without executing anything.

Two bugs fixed along the way, both found by running against the live API:

  • Thought signatures were dropped. Newer models attach an opaque thoughtSignature to each functionCall and require it echoed back verbatim; without it the second turn of every tool call failed with INVALID_ARGUMENT. It sits beside functionCall rather than inside it, so GeminiPart now reads it on decode, carries it on GeminiFunctionCall, and writes it back on encode.
  • A break inside a switch in the turn loop would have exited the switch rather than the loop, re-sending identical contents forever. The loop is now labeled.

The suite also moves off gemini-2.5-flash, which returns 404 "no longer available to new users" for keys not already using it.

Unlike Anthropic, continuation can't be gated on a stop reason — Gemini reports finishReason: "STOP" even on turns that request a call — so it gates on whether any functionCall part was seen.

Verified live: streamWithTools passes.

Based on #4; merge that first.

🤖 Generated with Claude Code

@ActuallyTaylor
ActuallyTaylor force-pushed the feature/gemini-streaming-tool-calling branch from 899d9fa to fa16ae2 Compare August 3, 2026 19:36
@ActuallyTaylor
ActuallyTaylor force-pushed the feature/gemini-streaming-tool-calling branch from fa16ae2 to 3777fa8 Compare August 3, 2026 19:43
Base automatically changed from feature/central-tool-resolution to main August 3, 2026 20:03
claude added 3 commits August 3, 2026 16:03
Gemini's streamResponse only handled .text parts, so .functionCall parts arriving over SSE were silently dropped and client tools never fired when streaming — the same prompt worked through respond but not through streamResponse.

Wrap the request in a turn loop that accumulates both assistant text and any function-call parts for a turn. When a turn ends with calls pending, execute them through the existing resolveFunctionCalls, append the model turn (text plus functionCall parts) and a single user turn carrying the functionResponse parts, then issue another request. The loop exits when the model finishes a turn without asking for a tool.

Transcript updates are live rather than deferred to the end: growStreamingTranscript as text arrives, and the .toolCalls entry is appended before the .toolOutput entries of the same turn so a Transcript-driven UI renders them in order. A .stop resolution appends the .toolCalls entry, finishes the continuation, and returns without executing.

Snapshot yielding (String versus structured partial-JSON handling) is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the Anthropic suite's streamWithTools: asserts that both a tool call and a tool output appear in the session transcript while the stream is still being consumed, not just after it completes, and that getWeather is present in the final transcript.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Newer Gemini models attach an opaque `thoughtSignature` to each `functionCall` part and require it back verbatim on the follow-up turn. The part decoder dropped it, so the second request of any tool-calling turn failed with `INVALID_ARGUMENT`: "Function call is missing a thought_signature in functionCall parts."

The signature sits beside `functionCall` rather than inside it, so `GeminiPart` now reads it during decode, carries it on `GeminiFunctionCall`, and writes it back out during encode. Pattern matches on `.functionCall(let call)` are unaffected.

Also moves the test suite off `gemini-2.5-flash`, which now returns 404 "no longer available to new users" for keys that were not already using it.

Found by running the tool tests against the live API.
@ActuallyTaylor
ActuallyTaylor force-pushed the feature/gemini-streaming-tool-calling branch from 3777fa8 to 3107112 Compare August 3, 2026 20:03

@ActuallyTaylor ActuallyTaylor left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Looks good, it is different than Anthropic in how it breaks the loop but it seems to match the Gemini api

@ActuallyTaylor
ActuallyTaylor merged commit 2115b3b into main Aug 3, 2026
7 checks passed
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.

2 participants