Repository navigation
Conversation
Add comprehensive Replicate provider implementation supporting all core features: text generation, streaming (SSE), structured output, embeddings, image generation, and audio (TTS/STT). Features: - Text generation with system prompts and conversation history - Real-time SSE streaming with automatic fallback to simulated streaming - Structured output with JSON schema validation - Image generation (FLUX, Stable Diffusion XL, etc.) - Text-to-Speech with multiple voices (Kokoro-82m) - Speech-to-Text with Whisper (WAV, MP3, FLAC, OGG, M4A) - Embeddings (single and batch, 768-dimensional vectors) Implementation: - Async prediction management with configurable polling - Sync mode (Prefer: wait header) for lower latency - Comprehensive error handling with typed exceptions - Full PHPStan level 8 compliance - 21 tests with 60 assertions (100% feature coverage) - 455 lines of comprehensive documentation Files changed: 58 files, 4,444+ lines added
…chronously This adds the ability to be able to send a request to a provider to create a transcript where the provider will give you an id and then send a webhook to you in the future when the job is done with that id. This is just supplying the interface that a provider can utilize in the future.
Add comprehensive support for Alibaba Cloud's Qwen models via the DashScope native API (/api/v1), covering text generation, streaming, structured output, embeddings, image generation, and image editing. Key features: - Text generation with multi-step tool calling - Multi-modal (VL) support with automatic endpoint routing - Streaming with DashScope SSE protocol and reasoning/thinking tokens - Structured output with both JSON Object and JSON Schema modes - Embeddings with configurable dimensions - Image generation (qwen-image-max/plus) and editing (qwen-image-edit) - Region-aware configuration (International, China, US deployments) - 52 tests with real API fixtures (176 assertions) Co-authored-by: Cursor <cursoragent@cursor.com>
StreamEndEvent.usage can be null when providers don't include usage data in their final stream chunk, causing a TypeError downstream. Add `?? new Usage(0, 0)` fallback to emitStreamEndEvent() in all providers missing it, matching the existing pattern in the OpenAI stream handler.
Add an `api_format` config option to the OpenAI driver that allows switching from the default `/responses` endpoint to `/chat/completions`. This enables using Prism with OpenAI-compatible backends like vLLM, LiteLLM, and LocalAI that only implement the chat/completions API. Set `OPENAI_API_FORMAT=chat_completions` in your env to use it. Only text, structured, and stream methods dispatch conditionally — other modalities (embeddings, images, moderation, TTS, STT) already use standard endpoints that work with compatible backends as-is.
…nfigured Providers that reject unknown parameters (e.g. Perplexity via LiteLLM) return HTTP 400 when `"tools": []` is sent. Return null instead so Arr::whereNotNull() filters it out entirely.
Providers with integrated search capabilities (e.g. Perplexity, You.com) return top-level `citations` and `search_results` fields in chat/completions responses. These were previously ignored. Add ChatCompletionsCitationsMapper to map these into Prism's existing Citation infrastructure, and extract them once per stream in the ChatCompletions stream handler. Citations are passed through on the StreamEndEvent, matching the existing pattern used by the Anthropic handler.
…ob management and result handling
…h job management, result handling, and error mapping
…-anthropic-and-openai
…lue for data retrieval
… provider methods for batch management
…delete, and metadata retrieval functionalities
…e batch job handling with inputFileId support
…xtRequest
- Use ?? [] on items to avoid passing null to buildAndUploadFile()
- Cast json_encode() result to string to satisfy non-empty-string return type
- Change clientRetry default from [] to [0] to satisfy array{0: int} type constraint
Made-with: Cursor
…s from OpenAI responses
…update tests for empty array responses
Introduce image generation support for the XAI provider: add Images handler (sends POST to images/generations, validates response, extracts GeneratedImage objects, and builds a Response via ResponseBuilder) and ImageRequestMap (maps Request to provider payload and forwards supported and additional provider options). Wire the new handler into XAI by adding an images() method. Add comprehensive tests for URL and base64 responses, provider-specific options, multiple images, usage, and meta handling.
Co-authored-by: machour <304450+machour@users.noreply.github.com> Agent-Logs-Url: https://github.com/machour/prism/sessions/17f7d150-e3c9-46a7-829b-e671f26cf794
Rector is the fifth gate in this repo and runs inside the Formatting job alongside Pint, so a Pint pass locally says nothing about it. It wanted a (string) cast on getPathname() and the four existence checks split into separate ifs. Mutation-checked again afterwards: all four defects still turn the test red.
docs/ ships inside this package, so a snippet that cannot run is shipped breakage. tests/Documentation/DocumentedApiTest.php already resolves every STATIC call on every `use Prism\...` import and found three live defects, but it cannot see an instance method, an argument name or a wrong receiver -- that needs type inference through a fluent chain. PHPStan does exactly that, and understands @method, Pest and Collection natively, where a bespoke matcher would need a hand-maintained allow-list to fake it. tools/doc-fences.php extracts the 467 fenced PHP blocks; phpstan-doc-fences.neon analyses them at level 2. LEVEL 2, NOT 0, AND THE CONTROL IS WHY. At level 0 PHPStan checks unknown methods only on $this, so a deliberately bogus call on a fluent chain was NOT caught -- I verified that with an injected control before trusting any result. Level 2 catches it: "Call to an undefined method Prism\Prism\Text\PendingRequest::totallyBogusMethod()", resolved through the facade's @method annotations. Without that control I would have reported 416 clean fences, which meant nothing. 60 of 467 fences are not standalone programs and never will be -- a config/prism.php array excerpt, a menu of four alternative constructors with no terminators, two `->using(...)` forms shown without a receiver, a labelled file that opens PHP after a comment. They are moved to fragments/ and EXCLUDED, because PHPStan stops reporting semantic findings entirely while any file fails to parse: one fragment left in the tree hid every finding in the other 407. The report partitions three ways, because unsorted output buries the signal: 157 undefined-variable findings (prose introduces the variable), 439 lost to per-fence isolation (a page declares `use` once and omits it later), and 60 worth reading. Among those 60, verified against source rather than taken from the tool: GeneratedAudio::getMimeType() does not exist on it or on Media; Text\PendingRequest has asStream() and not the stream() that providers/replicate.md calls; StreamEvent is an abstract readonly class with no $delta that three provider pages read; Document::fromBase64() is called with a $base64 named argument where the parameter is $document; four unknown parameters in a Document constructor call. Each would fail the moment a reader pasted it. Also fixed two bugs in my own extractor on the way, both of which looked like documentation defects: prepending <?php to fences that already open PHP, and reading the fence origin out of a comment in the file rather than a sidecar map.
The rule behind these is the Fancy agent's, and it sharpens one I had just sent
them. "Compare against a source, never a literal" is necessary and NOT
sufficient: a source-comparing guard with no floor under it degrades into a
literal the moment its source goes quiet. Nothing downstream of this tool can
tell an empty finding list from an empty input -- if a docs restructure dropped
extraction from 467 fences to 12, PHPStan would analyse 12, find nothing, and the
whole chain would report clean.
Two guards, failing for different reasons on purpose.
CONSERVATION, derived rather than asserted: every ```php fence in docs/ must end
up accounted for as analysable, fragment, or empty. Catches a fence the extractor
silently drops, and needs no magic number.
A FLOOR, deliberately a literal -- but one encoding a minimum plausible SCALE
rather than an answer. toStartWith('^4') went stale because it encoded the answer
and reality moved past it; this fires only if reality collapses, which is the one
case conservation cannot see, because zero declared equals zero accounted.
The conservation check failed on its first run, and it was the GUARD that was
wrong: it counted 464 against the extractor's 467 because it anchored ^ without
trimming while the extractor trims, so it missed three fences indented inside
list items. A conservation check is only as right as its own notion of the
denominator, which is worth having learned from the guard rather than from a
missed defect later.
Both mutation-tested: dropping indented fences in the extractor fires
conservation (467 declared, 464 accounted), and raising the floor above reality
fires the floor. A guard nobody has watched fail is a guard nobody should trust.
…own to 9
docs/ ships inside this package, so every one of these was shipped breakage. Each
was verified against src/ before being changed -- the tool reports, it does not
decide.
TWELVE Tool::as() calls were fatal. Sixteen pages imported Prism\Prism\Tool, the
concrete class, and then called Tool::as() statically; as() is an instance
method. Twenty pages already imported Prism\Prism\Facades\Tool correctly, which
forwards via __callStatic. Fourteen imports swapped across eight pages.
providers/vertex.md keeps the concrete import because it does (new Tool), and
core-concepts/tools-function-calling.md keeps it because it does
`class SearchTool extends Tool` -- my first filter matched "new Tool" against
"new ToolOutput" and would have broken both.
THIRTY-NINE deprecated calls. ->generate() maps to a different replacement per
builder, so each site was resolved from the nearest preceding Prism:: builder
rather than blanket-replaced: asText, asStructured, asEmbeddings. Images'
generate() is current and was left alone. fromPath() -> fromLocalPath() is
unambiguous and the deprecation names it.
NINE that would fatal or silently misbehave:
getMimeType() x3 -> mimeType(); getMimeType exists nowhere
->stream() -> asStream(); stream() does not exist
$response->responseMessages -> ->messages
Document::fromBase64(base64:) -> document:, the actual parameter name
Document::fromChunks(context:) -- removed; Document has no context concept
new Document(document:, dataFormat:, documentTitle:, documentContext:)
-> Document::fromText('', 'Test document');
none of those parameters exist on the class
echo $job->status -> ->name; echoing an enum is a TypeError, and line
208 of the same file already did it correctly
providers/replicate.md streaming read $chunk->text with NO type guard at all.
No stream event carries ->text. Replaced with the TextDelta pattern the other
provider pages already teach.
I introduced one regression and the tool caught it: renaming responseMessages: to
messages: in a Response constructor that already passed messages: produced a
duplicate argument. The correct repair was removal, not renaming.
NINE FINDINGS REMAIN AND NONE IS A DEFECT. Five read $event->delta behind an
`$event->type() === StreamEventType::TextDelta` guard, which is correct at
runtime; PHPStan cannot narrow a type from an enum comparison. Four are
doc-local or companion-package types that do not resolve in isolation. They are
deliberately left visible rather than filtered: an allow-list would hide the next
real finding of the same shape, and that is the failure this whole exercise is
about.
Gates: 2596 tests, PHPStan clean, Pint clean, Rector clean, 173 documentation
tests, and the docs site builds.
RELEASING.md already asked for this in prose -- "say what breaks, what it fixes,
and what a consumer has to do" -- and asking did not work. Of the twenty most
recent annotations across ten of these repositories, EIGHTEEN never used the
word "breaking" at all, including every one of prism's six latest releases.
prism-harness v0.13.0 shipped a mandatory migration, a changed scope-matching
rule and a raised Laravel floor under headings that described each change
accurately and labelled none of them breaking.
The annotation is the only changelog these packages have, so silence there is
not a neutral default -- it is the answer a consumer gets. tools/check-release-
notes.sh now requires an explicit declaration either way, the same way
publish.yml already refuses a lightweight tag and an empty annotation.
Two findings while testing it, both of which changed the design:
Prose does not count. Three real annotations contain the word "breaking" while
declaring nothing ("this avoids breaking downstream consumers"). A grep for the
word passes all three, so the check matches the structural form instead, and the
suite's sharpest negative control is exactly that shape.
git DELETES '#' lines from tag messages. Under the default --cleanup=strip they
are commentary, so `git tag -a -m '## Breaking changes ...'` publishes an
annotation with the heading silently removed -- which means the first form this
documented would have self-destructed. The cleanup-safe forms now come first,
the refusal message names the trap, and the suite asserts the stripping is still
real so that case cannot go vacuous.
Verified in both directions, not just the passing one: 23 cases including six
against real tag objects, and separately all 18 real annotations in the estate
are refused while the same annotations with a declaration appended are accepted.
A guard that refused everything would score identically on the first half alone.
--tag reads the annotation from the tag object rather than %(contents), which on
a lightweight tag yields the COMMIT message -- the same silent substitution that
has already built a prism release page from the wrong text three ways.
What this does NOT do: prevent a Composer publish. Packagist has the version the
moment the tag is pushed, so the guard withholds the GitHub release only. The
pre-tag run in RELEASING.md is the preventive control.
The checker now has a canonical home at Particle-Academy/.github under release-notes/, next to the third-party allowlist checker that every publish guard here already fetches. Twenty-eight repositories need this check, and the cheapest way to get it to them is also the one this ecosystem has measured the cost of: prism-parity found 38 of 111 hand-maintained copies stale. It is still VENDORED rather than fetched, for a reason the allowlist does not have: for a Composer package the pre-tag run is the only control that can prevent publication at all -- Packagist mirrors the version the moment the tag is pushed -- so the checker has to work from the tree being tagged, offline, and without piping a remote script into a shell. So the copy is diffed against the canonical file rather than trusted. That is the same argument prism-parity makes for the corpus copies it generates and then checks: an unchecked duplicate is one that has already drifted without saying so. The test file resolves the checker's filename instead of hardcoding it, so both files are byte-identical to the canonical pair and one diff covers both. Verified in both directions: identical copies pass, and a one-line perturbation of the vendored checker is caught.
prism-parity's remote audit reports this copy stale, and it is: the suite's
cases gained a `skip` map naming why the generic runner does not execute them
("Executed by prism:tests/CacheStabilityCorpusTest.php, not this generic
runner"), and this copy predates it.
Purely additive -- no value in any of the seven rows changed, and this test
reads `messages`, `expected.php` and `id` by key, so the new field is simply
carried. Nine tests still pass.
The fixture is a byte-for-byte artifact on purpose: the audit compares digests
rather than parsed values, because a copy that round-trips through a parser is
a copy that can lose a large integer or an empty map without saying so.
|
Quick update on where this stands. I just merged #1040, which brings in everything here up through your On top of that I pulled in a few things from later in the fork:
The Laravel floor and dropping Laravel 11 came along too. The package name and docs stay as While I was reviewing I found two bugs that are still in the fork, so you'll probably want these:
I'm leaving this PR open because I'm not done. Batch and Files, the new providers, human-in-the-loop, telemetry, and the usage fixes are all still on my list, and I expect to take a lot more of it. I'm going through those piece by piece, so it'll land as smaller PRs. This is going out as v0.101.0. Seriously, thank you for carrying this while I was away. |
StructuredModeResolver chose between a strict json_schema and legacy json_object from a fixed list of prefixes that stopped at gpt-5 and o3-mini. Against OpenAI's current roster (provider-watch, 2026-10-15), gpt-6-sol/luna/astra, gpt-6.1-sol, o1, o1-pro, o3, o3-pro and o4-mini matched nothing, so under the default Auto mode they silently got JSON mode, where the schema is not enforced. Nothing errored. A list goes stale in that unsafe direction with every release, so the families are now also recognised by OpenAI's naming: gpt-N for N >= 5 and the oN reasoning series. Names that only look like a family are unaffected, so custom models served through this provider under other names keep the JSON-mode default; that is why this does not default every unknown model to strict, which would break OpenAI-compatible servers. The handler test asserts the request body's text.format.type through Prism::structured(), not just the resolver's return value. Closes #83
Gemini's CONTINUATION finish reason means the per-request token limit was reached and generation is NOT complete. It had no case in FinishReasonMap and fell through to Other, so a caller checking for Length to detect a cut-off answer never saw it: a partial answer was presented as a finished one. python-genai 2.30/2.31 turn automatic continuation on by default, so servers are expected to send this in normal use. Vertex shares this map and is covered by the same change. Resuming with the continuation token is a separate capability. The test enters through Prism::text(), not the map. Closes #82
Every example and model table named a Gemini model Google no longer lists: gemini-1.5-flash (and -002), gemini-2.0-flash, gemini-2.0-flash-preview-image-generation, gemini-2.5-flash-preview, gemini-embedding-2-preview and text-embedding-004. A reader who copies the first example gets a failure on their first request. Replaced with ids on the current roster (provider-watch, 2026-10-15): gemini-2.5-flash, gemini-2.5-flash-image, gemini-embedding-2 and gemini-embedding-001. The Vertex model table no longer describes a retired model as the "latest generation", and points to Google's list. Closes #84
…anges fetchPublicUrlContent() relied on filter_var's private and reserved flags. On PHP 8.4 those admit, among others: 100.100.100.200 Alibaba Cloud's metadata endpoint (CGNAT) 64:ff9b::a9fe:a9fe NAT64 spelling of 169.254.169.254 2002:a9fe:a9fe:: 6to4 spelling of the same 198.18/15, 192.0.0/24, 224/4, fec0::/10, ff00::/8, Teredo, docs ranges The ports copied a list matching filter_var, so all three languages agreed and the media-fetch-guard corpus could not see it (G-89). The refused ranges are now an explicit list, identical in all three languages, with filter_var kept as a second layer. NAT64 and 6to4 addresses are judged by the IPv4 address they embed rather than refused outright, because a DNS64 resolver gives an IPv6-only host 64:ff9b:: addresses for public sites. Tests enter through fetchPublicUrlContent() with each spelling as a URL literal and as a DNS answer, plus public controls (including NAT64 and 6to4 of a public address) that must still fetch. 38 of the 40 refusal rows failed before this change.
prism-parity e01c1a4 adds url-0013..0017. The controls test now names all three controls by id, including url-0017: NAT64 of a public address must fetch, so a guard that refuses NAT64 outright cannot pass.
…taged the test but not the fixture)
…-90) fetchPublicUrlContent() validated every destination and then read the response body whole. The URL is by definition one the caller does not trust, so the size of its answer is not theirs to choose either: a public host could answer with gigabytes and take the worker's memory. The body is now read as a stream and refused with media_too_large the moment it passes $maxBytes, so an oversized answer is never held whole, and a declared Content-Length over the cap is refused before any body is read. The default, 32 MiB, is the largest inline document the providers accept, so a larger body could not have been sent to a model anyway. Tests enter through fetchPublicUrlContent(): one byte over the cap, an oversized declared length, a body exactly at the cap (the control), and the default. All four failed before this change.
Dependabot #36 (high): source-map-js before 1.2.2 allows an event-loop denial of service through indexed source-map sections. It reaches the docs SITE build only (vitepress via postcss and @vue/compiler-core), development scope, never the published package. Lock-only update, the same one the sibling repositories took. #37 (sprintf-js, medium) has no patched release; it is the same build-time scope and is triggered by a hostile format string, which the docs build never takes. Left until a fix is published.
Dependabot #38 (high, published 2026-10-19): @vue/server-renderer before 3.5.42 has an XSS through a missing CR in its attribute-name blacklist. It reaches the docs SITE build only (vitepress -> vue), development scope, never the published package. Lock-only: the vue family moves to 3.5.44 with its own dependencies (@babel/parser and types, postcss, nanoid, sourcemap-codec), all within existing ranges. Verified with npm ci and a full `vitepress build` of the docs site.
…e fork Brings the fork level with upstream after sixlive merged prism-php#1040 and the prism-php#1043 usage and stream standardisation. Upstream's design wins where the two overlap; the fork's own features are kept. BREAKING (usage semantics, from prism-php#1043): - promptTokens is now the UNCACHED input; cache reads and writes are reported only in their own fields. - completionTokens includes reasoning; thoughtTokens is a subset of it. - A field the provider did not report is null, not 0. - Every stream ends with one StepFinishEvent per step and exactly one StreamEndEvent. - OpenAI structured output uses strict json_schema for every model except the legacy list (gpt-3.5-turbo*, gpt-4, gpt-4-*, gpt-4o-2024-05-13). This replaces the fork's per-model allow-list. Kept from the fork: HITL tool approval, telemetry, cache-stability hints, Usage::cost, withReasoning, batch and files, Qwen and Requesty, the Perplexity Agent API endpoint, "0" prompts, the G-89/G-90 guards, and the particle-academy/prism package identity in composer.json. Docs use the upstream name and site. Integration fixes on top of the merge: - Streamed handlers advance the step-usage snapshot at every step finish, so a step that stops for approval reports only its own usage. New Groq test proves it (red without the advance: 40/12 instead of 30/7). - OpenRouter's billed cost is read by its MapsUsage concern; the three handler overrides are gone. - Z sends tool arguments through argumentsAsJson(). - Tests: cost key in usage expectations; tool input compared as wire JSON (now an object); an empty assistant turn is dropped; a Gemini image test uses local bytes (URLs are no longer fetched implicitly); Perplexity's Sonar-shaped Length test becomes the Agent API's incomplete-run equivalent.
Actions was off on this repository from 2026-10-07 until 2026-10-20, and pushes made in that window got no Tests, PHPStan, Formatting or Require Checker run. Those four ran only on push and pull_request, so the commit already on main could not be checked without making another one.
…ng of JSON A fancy-labs run failed only because Haiku sent an object-typed `schema` argument as a string of JSON, and the handler rejected it: "Parameter validation error: Type mismatch. Expected: [schema (ObjectSchema, required)]". The same brief passed on the other lanes. The owner's ruling (via fancy, 2026-10-21): accept it. When a parameter's schema is an ObjectSchema or ArraySchema and the value is a string, it is decoded as JSON and used only if it is exactly that JSON type -- an object, or a list. Invalid JSON or the wrong JSON type passes through unchanged and fails exactly as before. A string-typed parameter is never decoded, and neither is one whose handler declares `string`. Tests enter as CallsTools calls a tool, call_user_func_array on handle(...) with the model's named arguments.
…notice Upstream prism-php#1043 deleted Prism\Prism\Streaming\StreamCollector, and the fork's v0.132.0 shipped that deletion without declaring it among its breaking changes. prism-harness 0.13.0 builds every streamed transcript with it, and its range (>=0.123 <1.0) accepts 0.132, so a routine `composer update` left any app on the harness failing with 'Class "Prism\Prism\Streaming\StreamCollector" not found' (fancy-labs run 122, 2026-10-21). It is restored with the same constructor, collect() and callback signature, now built on StreamedMessages, so it follows the post-prism-php#1043 stream shape and keeps each step's replay metadata. Deprecated in favour of StreamedMessages::fromEvents(). Verified against the consumer, not only here: prism-harness's own suite (373 tests) passes against this tree through a path repository; with the class deleted, its three streaming tests fail with that same error. The class's original 13 tests are restored unchanged and pass; a new one enters exactly as the harness does, over a real multi-step tool stream. It was the only public class or constructor removed since v0.131.0.
… that relied on the implicit one The audio guide warned that URLs are not fetched automatically but named only fetchUrlContent(), the unguarded fetch, never fetchPublicUrlContent(), the one meant for a URL from user input or model output. And six examples still sent a URL to something that needs bytes (speech-to-text on OpenAI, Groq and Mistral, a Gemini video that is not YouTube or a File API URI), which now throws. Found while preparing the same change for upstream.
Upstream has been quiet since March 2026 (v0.100.1), so particle-academy/prism — a drop-in fork,
Prism\Prismnamespace unchanged — has been absorbing the open backlog and shipping releases (context: discussion #1027). This PR offers all of that work back upstream in one piece: 166 commits, nine releases (v0.101.0–v0.109.0), gated throughout by Pest + PHPStan + Pint/Rector.If maintainership resumes, merge wholesale or tell us how you'd like it split — we're happy to break it into reviewable chunks. Either way the fork remains active.
Community PRs from this repo merged into the fork (48)
v0.101.0 — correctness fixes (17): #952, #958, #964, #971, #977, #985, #986, #987, #989, #991, #996, #1001, #1004, #1009, #1012, #1013, #1024
v0.103.0 — provider correctness / API drift (16): #949, #954, #961, #965, #975, #976, #980, #992, #993, #995, #997, #1000, #1002, #1008, #1020, #1021
v0.104.0 — features (9): #951 (batches + files APIs), #960 (xAI images), #978 (Vertex AI provider, answers #795), #988 (fine-grained tool streaming), #998 (Anthropic adaptive thinking), #1003 (pause_turn/refusal), #1014 (Mistral FIM), #1018 (provider-agnostic withReasoning()), #1026 (Requesty provider)
v0.105.0 — features + providers (6): #757 (Replicate provider), #810 (async STT interface), #835 (Azure OpenAI provider), #898 (Qwen provider), #907 (OpenAI chat/completions api_format + streaming citations, answers #900), #920 (cost tracking in Usage)
Reimplemented rather than rebased: #932 (client-executed tools + human-in-the-loop approval, answers #921) — clean-room implementation across all providers including streaming; docs at https://ai.particle.academy/docs/core-concepts/human-in-the-loop
Adjudicated, not merged (rationale posted): #950 (duplicate of #977), #937 and #1005 (superseded by an escape-based control-character fix), #999 (superseded), #1025 (rejected — a composer-require-checker CI gate solves the underlying goal properly; analysis in Particle-Academy/prism#3)
Fork-original changes
laravel/framework ^12.61.1|^13.12.0).Tool::requiresApproval(bool|Closure)/Tool::clientExecuted(), deny-by-default resume from message history, streaming approval events — text/structured/stream on all 18 providers.promptTokens= non-cached input everywhere;cacheReadInputTokenspopulated wherever the provider exposes it. Fixed silent double counting in Gemini, Vertex, OpenRouter (v0.108.1) and Z.AI + Requesty streams (v0.109.0); added cache visibility for OpenAI chat/completions, Azure, Groq, Qwen, xAI.anthropic_betaprovider option.src/.Full release notes: https://github.com/Particle-Academy/prism/releases
🤖 Generated with Claude Code