Skip to content

fix: streaming tool call arguments silently drop '0' characters - #1005

Open
JuanmaMingot wants to merge 1 commit into
prism-php:mainfrom
JuanmaMingot:fix/streaming-tool-call-falsy-zero-string
Open

fix: streaming tool call arguments silently drop '0' characters#1005
JuanmaMingot wants to merge 1 commit into
prism-php:mainfrom
JuanmaMingot:fix/streaming-tool-call-falsy-zero-string

Conversation

@JuanmaMingot

Copy link
Copy Markdown

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.

Description

Breaking Changes

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) <noreply@anthropic.com>
wishborn added a commit to Particle-Academy/prism that referenced this pull request Aug 10, 2026
"0" is falsy in PHP, so `if ($content)` silently discards a message, delta or
payload whose entire text is the single character 0. Rector's
ExplicitBoolCompareRector (SetList::CODE_QUALITY) rewrote those checks into
the explicit `$content !== '' && $content !== '0'`, which is a faithful
translation — and is why the bug survived in plain sight.

A lone "0" is ordinary model output: a count, a numeric answer, a JSON
number, or one digit landing alone in a stream chunk.

Scoped to the call sites where a "0" can actually reach a user:

  message maps      Anthropic, Gemini, OpenAI, OpenRouter, Requesty
  stream deltas     Azure, DeepSeek, Qwen, XAI
  structured output Gemini, Vertex (a "0" body was reported as empty)
  file content      Media::fromLocalPath and rawContent used `?: ''`

Deliberately NOT swept: the same pattern in Anthropic's SSE line parsing,
Ollama's line parsing, the TTS voice name and ToolCall::arguments(). A
data line or a voice is never the single character 0, and ToolCall already
funnels through `is_array($decoded)`, so changing them would edit real
files across four more providers to fix nothing.

Two findings beyond the mechanical rewrite:

- HandlesStructuredJson returned json_decode() straight from a method
  declared `: array`. The "0" guard was accidentally shielding it, so any
  scalar JSON from a provider — "12", "\"text\"", "true" — raised a
  TypeError. Now guarded on the decoded shape.
- Anthropic's assistant map built its text block with a bare array_filter(),
  dropping 'text' => '0' while only meaning to drop a null cache_control.

Removing the '0' arm made three thinking-complete conditions provably
constant; PHPStan flagged them and they are gone, matching upstream prism-php#1005.

Rector's rule stays enabled: it is what made this visible. Suppressing it
would only hide the same bug behind `if ($content)`.

Covered by tests/Regression/FalsyZeroStringTest.php, verified to fail
against the unfixed source with these exact modes (including the TypeError).
wishborn added a commit to Particle-Academy/prism that referenced this pull request Aug 11, 2026
"0" is falsy in PHP, so `if ($content)` silently discards a message, delta or
payload whose entire text is the single character 0. Rector's
ExplicitBoolCompareRector (SetList::CODE_QUALITY) rewrote those checks into
the explicit `$content !== '' && $content !== '0'`, which is a faithful
translation — and is why the bug survived in plain sight.

A lone "0" is ordinary model output: a count, a numeric answer, a JSON
number, or one digit landing alone in a stream chunk.

Scoped to the call sites where a "0" can actually reach a user:

  message maps      Anthropic, Gemini, OpenAI, OpenRouter, Requesty
  stream deltas     Azure, DeepSeek, Qwen, XAI
  structured output Gemini, Vertex (a "0" body was reported as empty)
  file content      Media::fromLocalPath and rawContent used `?: ''`

Deliberately NOT swept: the same pattern in Anthropic's SSE line parsing,
Ollama's line parsing, the TTS voice name and ToolCall::arguments(). A
data line or a voice is never the single character 0, and ToolCall already
funnels through `is_array($decoded)`, so changing them would edit real
files across four more providers to fix nothing.

Two findings beyond the mechanical rewrite:

- HandlesStructuredJson returned json_decode() straight from a method
  declared `: array`. The "0" guard was accidentally shielding it, so any
  scalar JSON from a provider — "12", "\"text\"", "true" — raised a
  TypeError. Now guarded on the decoded shape.
- Anthropic's assistant map built its text block with a bare array_filter(),
  dropping 'text' => '0' while only meaning to drop a null cache_control.

Removing the '0' arm made three thinking-complete conditions provably
constant; PHPStan flagged them and they are gone, matching upstream prism-php#1005.

Rector's rule stays enabled: it is what made this visible. Suppressing it
would only hide the same bug behind `if ($content)`.

Covered by tests/Regression/FalsyZeroStringTest.php, verified to fail
against the unfixed source with these exact modes (including the TypeError).
wishborn added a commit to Particle-Academy/prism that referenced this pull request Aug 12, 2026
"0" is falsy in PHP, so `if ($content)` silently discards a message, delta or
payload whose entire text is the single character 0. Rector's
ExplicitBoolCompareRector (SetList::CODE_QUALITY) rewrote those checks into
the explicit `$content !== '' && $content !== '0'`, which is a faithful
translation — and is why the bug survived in plain sight.

A lone "0" is ordinary model output: a count, a numeric answer, a JSON
number, or one digit landing alone in a stream chunk.

Scoped to the call sites where a "0" can actually reach a user:

  message maps      Anthropic, Gemini, OpenAI, OpenRouter, Requesty
  stream deltas     Azure, DeepSeek, Qwen, XAI
  structured output Gemini, Vertex (a "0" body was reported as empty)
  file content      Media::fromLocalPath and rawContent used `?: ''`

Deliberately NOT swept: the same pattern in Anthropic's SSE line parsing,
Ollama's line parsing, the TTS voice name and ToolCall::arguments(). A
data line or a voice is never the single character 0, and ToolCall already
funnels through `is_array($decoded)`, so changing them would edit real
files across four more providers to fix nothing.

Two findings beyond the mechanical rewrite:

- HandlesStructuredJson returned json_decode() straight from a method
  declared `: array`. The "0" guard was accidentally shielding it, so any
  scalar JSON from a provider — "12", "\"text\"", "true" — raised a
  TypeError. Now guarded on the decoded shape.
- Anthropic's assistant map built its text block with a bare array_filter(),
  dropping 'text' => '0' while only meaning to drop a null cache_control.

Removing the '0' arm made three thinking-complete conditions provably
constant; PHPStan flagged them and they are gone, matching upstream prism-php#1005.

Rector's rule stays enabled: it is what made this visible. Suppressing it
would only hide the same bug behind `if ($content)`.

Covered by tests/Regression/FalsyZeroStringTest.php, verified to fail
against the unfixed source with these exact modes (including the TypeError).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant