Skip to content

v0.112.0 — content that is exactly "0", Vertex tool combinations, DeepSeek options

Latest

Choose a tag to compare

@wishborn wishborn released this 12 Aug 00:08
0f4e8ae

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 0 body was reported as empty), and Media file content.
  • Bare array_filter() — which drops every falsy value, so 'content' => '0' vanished while only a null cache_control was meant to. Replaced with Payload::compact() across 21 sites in 10 providers, keeping the behaviour the filter genuinely existed for (dropping empty tool_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

Contributions absorbed from upstream now earn XP on the contributor leaderboard.

Full Changelog: v0.111.1...v0.112.0