-
-
Notifications
You must be signed in to change notification settings - Fork 147
Description
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
- Configure a Gemini model with tools/function calling enabled
- Enable streaming (
'stream' => true) - Send a prompt that triggers tool usage
- 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