-
-
Notifications
You must be signed in to change notification settings - Fork 249
Description
I ran into this issue with Gemini 3 and thoughtsignatures when making many tool calls. Claude implemented this fix, I tested it, it seems to work well.
Claude's description:
Problem
When using Gemini 3 with thinking enabled (thinkingLevel: 'medium') and streaming mode (asStream()), tool calling fails with HTTP 400:
Function call is missing a thought_signature in functionCall parts.
Root Cause
In streaming mode, when Gemini returns multiple tool calls, only the first tool call part contains a thoughtSignature. Subsequent tool calls arrive in separate streaming chunks without thoughtSignature.
However, when sending tool results back to Gemini, all functionCall parts must include their thoughtSignature per https://ai.google.dev/gemini-api/docs/thought-signatures.
Evidence from Logs
// First tool call - HAS thoughtSignature
{"part_keys":["functionCall","thoughtSignature"],"has_thoughtSignature":true}
// Second tool call - MISSING thoughtSignature
{"part_keys":["functionCall"],"has_thoughtSignature":false}
Affected File
src/Providers/Gemini/Handlers/Stream.php
The Fix
Store the first thoughtSignature and reuse it for subsequent tool calls that don't have their own:
class Stream
{
// Add property to store first thought signature
protected ?string $currentThoughtSignature = null;
public function handle(Request $request): Generator
{
$this->state->reset();
$this->currentThoughtSignature = null; // Reset for new request
// ...
}
protected function extractToolCalls(array $data, array $toolCalls): array
{
$parts = data_get($data, 'candidates.0.content.parts', []);
foreach ($parts as $index => $part) {
if (isset($part['functionCall'])) {
// Capture first thoughtSignature
if (isset($part['thoughtSignature'])) {
$this->currentThoughtSignature = $part['thoughtSignature'];
}
$toolCalls[$index]['id'] = EventID::generate('gm');
$toolCalls[$index]['name'] = data_get($part, 'functionCall.name');
$toolCalls[$index]['arguments'] = data_get($part, 'functionCall.args', []);
// Use part's signature if present, otherwise fall back to stored one
$toolCalls[$index]['reasoningId'] = $part['thoughtSignature']
?? $this->currentThoughtSignature;
}
}
return $toolCalls;
}
}
Why This Works
Per Google's docs on https://ai.google.dev/gemini-api/docs/thought-signatures:
"When there are parallel function calls, the first function call part returned by the model response will have a thought signature."
The signature represents the thinking context that led to the tool calls. All tool calls from the same thinking round share that context, so reusing the first signature is semantically correct.
Reproduction Steps
- Use Gemini 3 (gemini-3-flash-preview) with thinkingLevel: 'medium'
- Use streaming mode (asStream())
- Trigger a response that calls multiple tools
- Observe: first tool works, second fails with 400 error