The headline is a silent data-loss bug affecting every provider, plus two capabilities that were blocked for no good reason.
"0" is no longer silently discarded
"0" is falsy in PHP, so if ($content) throws away a message, delta, or payload whose entire text is the single character 0. Rector's ExplicitBoolCompareRector had rewritten those checks into their literal equivalent — $content !== '' && $content !== '0' — which is a faithful translation, and is exactly why the bug survived in plain sight across ~40 call sites.
A lone "0" is ordinary model output: a count, a numeric answer, a JSON number, or one digit landing alone in a stream chunk.
Fixed in two shapes:
- Explicit comparisons — message maps (Anthropic, Gemini, OpenAI, OpenRouter, Requesty), stream deltas (Azure, DeepSeek, Qwen, xAI), structured output (Gemini, Vertex — a
0body was reported as empty), andMediafile content. - Bare
array_filter()— which drops every falsy value, so'content' => '0'vanished while only a nullcache_controlwas meant to. Replaced withPayload::compact()across 21 sites in 10 providers, keeping the behaviour the filter genuinely existed for (dropping emptytool_calls).
Important
The worst case was tool results. A tool returning a count of 0 had its result stripped from the payload entirely, so the model reasoned as if the tool had returned nothing.
Also fixed while in there: HandlesStructuredJson returned json_decode() straight from a method declared : array, so any scalar JSON from a provider — 12, "text", true — raised a TypeError. The '0' guard had been shielding it by accident.
Vertex can combine grounding with custom function tools
All three Vertex handlers refused provider tools alongside custom tools. That guard arrived with the very first Vertex commit as a placeholder — not in response to the API rejecting anything — and was never revisited, while the Gemini handlers it was copied from carry no such guard.
Google documents the combination as supported: "Gemini 3 models also support combining these built-in tools with custom tools (function calling)."
Removed, and the payload shape fixed in v0.111.1 is now reachable and covered by tests.
DeepSeek forwards provider options
All three DeepSeek handlers built their request body without ever reading providerOptions(), so ['thinking' => ['type' => 'disabled']] went nowhere — thinking mode could not be turned off at all.
They now merge provider options generically, so reasoning_effort, stop, the penalties, logprobs, and anything DeepSeek adds later all work. Options merge first, so explicit request settings always win: nothing can clobber model, messages, or switch stream off underneath the Stream handler.
Browser audio recordings keep their extension
Browsers report the container type, so an audio-only MediaRecorder clip arrives as video/webm or video/mp4 and was written out as audio.mp3. Now mapped correctly — including with codec parameters attached (audio/webm;codecs=opus), which the upstream fix did not cover and which is what real browser output actually looks like.
Also in this release
- OpenAI-compatible endpoints documented — pointing the OpenAI provider at vLLM, Azure, or a gateway, with the two things people get wrong: partial feature parity, and model-name-driven capability inference.
- Line endings normalised to LF, so a Windows clone stops reporting phantom modified files after a formatter run.
- Smaller package — 9.6M of committed docs build output and a dev lockfile no longer ship in the Composer dist. The docs markdown still ships.
Thanks
- @leobeal — prism-php/prism#1032, DeepSeek provider options
- @mrmorgan-i — prism-php/prism#1030, audio container mime types
- @saimaz — whose v0.111.1 payload fix made the Vertex change possible
- @cerebrixos — prism-php/prism#1022, for identifying the OpenAI-compatible docs gap
Contributions absorbed from upstream now earn XP on the contributor leaderboard.
Full Changelog: v0.111.1...v0.112.0