Skip to content

Gemini streaming yields array instead of ToolCallResult object during tool calls DescriptionΒ #1107

@nsrosenqvist

Description

@nsrosenqvist

Disclaimer: I asked copilot to write up this description after investigating this issue.

Description

When using streaming with Gemini models and the model invokes a tool, the ResultConverter::convertStream() method yields $choices[0]->getContent() instead of $choices[0] directly. When the choice is a ToolCallResult, getContent() returns an array of ToolCall objects, which causes an "Array to string conversion" error in StreamResult::getContent() at line 54 where it attempts $streamedResult .= $value.

Steps to Reproduce

  1. Configure a Gemini model with tools/function calling enabled
  2. Enable streaming ('stream' => true)
  3. Send a prompt that triggers tool usage
  4. Observe the error when the stream processes the tool call response

Expected Behavior

The ToolCallResult object should be yielded directly (like other platform implementations do), allowing StreamResult::getContent() to properly detect it with if ($value instanceof ToolCallResult) and handle it accordingly.

Actual Behavior

Array to string conversion at vendor/symfony/ai-agent/src/Toolbox/StreamResult.php:54

The error occurs because convertStream() yields $choices[0]->getContent() which is an array when the choice is a ToolCallResult.

Root Cause

In ResultConverter.php, the convertStream() method:

private function convertStream(RawResultInterface $result): \Generator
{
    foreach ($result->getDataStream() as $data) {
        $choices = array_map($this->convertChoice(...), $data['candidates'] ?? []);
        
        // ...
        
        yield $choices[0]->getContent();  // <-- Bug: yields array for ToolCallResult
    }
}

Proposed Fix

Check if the choice is a ToolCallResult and yield the object itself:

private function convertStream(RawResultInterface $result): \Generator
{
    foreach ($result->getDataStream() as $data) {
        $choices = array_map($this->convertChoice(...), $data['candidates'] ?? []);
        
        // ...
        
        $choice = $choices[0];
        if ($choice instanceof ToolCallResult) {
            yield $choice;  // Yield the object, not its content
        } else {
            yield $choice->getContent();
        }
    }
}

This matches how other platform implementations (OpenAI, DeepSeek, Mistral, etc.) handle tool calls in streaming - they yield new ToolCallResult(...) directly.

Environment

  • symfony/ai-platform: dev-main (commit d7c8e4c9d3ebca8a670e06223067a8b58b5cb91d, 2025-12-05)
  • PHP: 8.4

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions