From c8345f8ed4fe84a446c8564857316b1b6f06b122 Mon Sep 17 00:00:00 2001 From: Juanma Mingot Date: Fri, 10 Apr 2026 14:42:58 +0200 Subject: [PATCH] fix: streaming tool call arguments silently drop '0' characters In PHP, the string '0' is falsy. When tool call arguments are streamed token-by-token and a chunk contains just "0" (common in UUIDs/IDs), the truthy check `if ($arguments = data_get(...))` evaluates to false and the chunk is silently discarded. This causes IDs like "BZG7TxXIBU0P-6nP" to arrive as "BZG7TxXIBUP-6nP" (missing the 0), breaking tool execution. Fix: replace truthy assignment checks with explicit null checks (`$x !== null`), consistent with how the Groq and OpenRouter providers already handle this correctly. Also remove `!== '0'` guards on content/reasoning delta checks and simplify the now-redundant `$delta === ''` conditions that become always-true after the fix. Affected providers: DeepSeek, XAI, Ollama. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Providers/DeepSeek/Handlers/Stream.php | 15 +++++++++------ src/Providers/Ollama/Handlers/Stream.php | 7 ++++--- src/Providers/XAI/Handlers/Stream.php | 13 ++++++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Providers/DeepSeek/Handlers/Stream.php b/src/Providers/DeepSeek/Handlers/Stream.php index 8ae26e57b..36701c4e3 100644 --- a/src/Providers/DeepSeek/Handlers/Stream.php +++ b/src/Providers/DeepSeek/Handlers/Stream.php @@ -137,7 +137,7 @@ protected function processStream(Response $response, Request $request, int $dept } $reasoningDelta = $this->extractReasoningDelta($data); - if ($reasoningDelta !== '' && $reasoningDelta !== '0') { + if ($reasoningDelta !== '') { if ($this->state->shouldEmitThinkingStart()) { $this->state->withReasoningId(EventID::generate())->markThinkingStarted(); @@ -158,7 +158,7 @@ protected function processStream(Response $response, Request $request, int $dept continue; } - if ($this->state->hasThinkingStarted() && $reasoningDelta === '') { + if ($this->state->hasThinkingStarted()) { yield new ThinkingCompleteEvent( id: EventID::generate(), timestamp: time(), @@ -167,7 +167,7 @@ protected function processStream(Response $response, Request $request, int $dept } $content = $this->extractContentDelta($data); - if ($content !== '' && $content !== '0') { + if ($content !== '') { if ($this->state->shouldEmitTextStart()) { $this->state->markTextStarted(); @@ -300,15 +300,18 @@ protected function extractToolCalls(array $data, array $toolCalls): array ]; } - if ($id = data_get($deltaToolCall, 'id')) { + $id = data_get($deltaToolCall, 'id'); + if ($id !== null) { $toolCalls[$index]['id'] = $id; } - if ($name = data_get($deltaToolCall, 'function.name')) { + $name = data_get($deltaToolCall, 'function.name'); + if ($name !== null) { $toolCalls[$index]['name'] = $name; } - if ($arguments = data_get($deltaToolCall, 'function.arguments')) { + $arguments = data_get($deltaToolCall, 'function.arguments'); + if ($arguments !== null) { $toolCalls[$index]['arguments'] .= $arguments; } } diff --git a/src/Providers/Ollama/Handlers/Stream.php b/src/Providers/Ollama/Handlers/Stream.php index b5544c111..79e1eab6c 100644 --- a/src/Providers/Ollama/Handlers/Stream.php +++ b/src/Providers/Ollama/Handlers/Stream.php @@ -253,14 +253,15 @@ protected function parseNextDataLine(StreamInterface $stream): ?array protected function extractToolCalls(array $data, array $toolCalls): array { foreach (data_get($data, 'message.tool_calls', []) as $index => $toolCall) { - if ($name = data_get($toolCall, 'function.name')) { + $name = data_get($toolCall, 'function.name'); + if ($name !== null) { $toolCalls[$index]['name'] = $name; $toolCalls[$index]['arguments'] = ''; $toolCalls[$index]['id'] = data_get($toolCall, 'id'); } - if ($arguments = data_get($toolCall, 'function.arguments')) { - + $arguments = data_get($toolCall, 'function.arguments'); + if ($arguments !== null) { $argumentValue = is_array($arguments) ? json_encode($arguments) : $arguments; $toolCalls[$index]['arguments'] .= $argumentValue; } diff --git a/src/Providers/XAI/Handlers/Stream.php b/src/Providers/XAI/Handlers/Stream.php index 1edac9f2a..101acb870 100644 --- a/src/Providers/XAI/Handlers/Stream.php +++ b/src/Providers/XAI/Handlers/Stream.php @@ -111,7 +111,7 @@ protected function processStream(Response $response, Request $request, int $dept $thinkingContent = $this->extractThinking($data, $request); - if ($thinkingContent !== '' && $thinkingContent !== '0') { + if ($thinkingContent !== '') { if ($this->state->shouldEmitThinkingStart()) { $this->state ->withReasoningId(EventID::generate()) @@ -134,7 +134,7 @@ protected function processStream(Response $response, Request $request, int $dept continue; } - if ($this->state->hasThinkingStarted() && $thinkingContent === '') { + if ($this->state->hasThinkingStarted()) { yield new ThinkingCompleteEvent( id: EventID::generate(), timestamp: time(), @@ -291,15 +291,18 @@ protected function extractToolCalls(array $data, array $toolCalls): array ]; } - if ($id = data_get($deltaToolCall, 'id')) { + $id = data_get($deltaToolCall, 'id'); + if ($id !== null) { $toolCalls[$index]['id'] = $id; } - if ($name = data_get($deltaToolCall, 'function.name')) { + $name = data_get($deltaToolCall, 'function.name'); + if ($name !== null) { $toolCalls[$index]['name'] = $name; } - if ($arguments = data_get($deltaToolCall, 'function.arguments')) { + $arguments = data_get($deltaToolCall, 'function.arguments'); + if ($arguments !== null) { $toolCalls[$index]['arguments'] .= $arguments; } }