Skip to content

v0.116.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 06:46
· 62 commits to main since this release

v0.116.0 — empty maps go out as objects, and quota stops riding on the content switch

Wire format: an empty map-typed field is now {} and not []. PHP has one
array type, so an empty map encoded as [] while a populated one encoded as
{} — the same field changing JSON type with its contents. Providers reject the
array form, which is why eight message maps carried a ?: (object) [] guard.

If you compare request bodies byte-for-byte, or store them, expect {} where
[] used to appear. Nothing else about the request changed.

A real defect went with it: a model sending {"filter":{}} got {"filter":[]}
sent back to the provider on the next turn. And four send sites had never had the
guard at all — Azure, OpenAI ChatCompletions and Qwen on tool-call arguments,
OpenAI ChatCompletions on tool schemas — so they were shipping [] already.
Fifteen duplicated guards are gone; the test that replaces them DISCOVERS the
provider maps from the filesystem, so the next provider is covered without
anyone remembering.

An unreadable rate-limit header no longer destroys the response it describes.
processRateLimits() runs on the SUCCESS path, after the model has answered and
the call has been billed, and built the reset instant with new Carbon($value) —
which raises on anything it cannot parse. An unparseable header therefore threw
straight out through asText(). The header is not necessarily the provider's
either: whatever proxy sits in front of the API can set it. Anthropic was the
only provider exposed; it now fails to a null reset, as OpenAI and Gemini
already did.

Rate-limit headers are matched case-insensitively. HTTP header names are
case-insensitive by specification, and Anthropic, OpenAI and Groq all matched
theirs exactly — so any proxy that title-cased headers made them report NO rate
limits, indistinguishable from a response that carried none.

Provider rate limits reach telemetry on a successful generation. They rode
on the response, which is passed to telemetry only when content capture is on —
so quota headroom was gated behind a privacy switch it has nothing to do with,
and appeared only on the 429 path, exactly when it had stopped being headroom.
GenerationCompleted and StepCompleted now carry them as their own
unconditional field, as Usage already did. The content gate is unchanged.

laravel/pint is pinned exactly, so the Formatting gate resolves the same
version for CI and for a developer.