Skip to content

Prism Gemini 3 Streaming thoughtSignature Bug Report #830

@petervandijck

Description

@petervandijck

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

  1. Use Gemini 3 (gemini-3-flash-preview) with thinkingLevel: 'medium'
  2. Use streaming mode (asStream())
  3. Trigger a response that calls multiple tools
  4. Observe: first tool works, second fails with 400 error

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions