Skip to content

Carrying the torch: 48 community PRs merged + fork fixes (v0.101.0-v0.109.0) - #1028

Open
wishborn wants to merge 195 commits into
prism-php:mainfrom
Particle-Academy:main
Open

Carrying the torch: 48 community PRs merged + fork fixes (v0.101.0-v0.109.0)#1028
wishborn wants to merge 195 commits into
prism-php:mainfrom
Particle-Academy:main

Conversation

@wishborn

@wishborn wishborn commented Jul 7, 2026

Copy link
Copy Markdown

Upstream has been quiet since March 2026 (v0.100.1), so particle-academy/prism — a drop-in fork, Prism\Prism namespace 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

Full release notes: https://github.com/Particle-Academy/prism/releases

🤖 Generated with Claude Code

cemarta7 and others added 30 commits November 24, 2025 22:18
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.
…h job management, result handling, and error mapping
… tool loop

## Context

When Anthropic's server-side tools (like `web_search`) are used alongside regular user-defined tools, the model can do both in a single response: perform a web search, write text with citations referencing the search results, and call a regular tool. Because a regular tool was called, Prism enters its multi-step tool loop. It executes the tool, then replays the entire conversation back to the API for the next turn.

The problem is that when Prism builds the replayed assistant message, it includes the text with citations but drops the `server_tool_use` and `web_search_tool_result` content blocks that the citations reference. The API validates that every citation points to an existing search result, finds none, and rejects the request with: `invalid_request_error - Could not find search result for citation index.`

This only triggers when the model performs a server-side tool call AND a regular tool call in the same response. If either happens alone, everything works fine.

## Changes

Both the Text and Stream handlers had the same gap in their tool loop replay logic.

**Text handler (`Text.php`):** Added `extractProviderToolContent()` that pulls `server_tool_use` and `*_tool_result` content blocks from the API response and stores them in `additionalContent` as `provider_tool_calls` and `provider_tool_results`, the same keys that `MessageMap::mapAssistantMessage()` already reads and serializes back to the API. This follows the existing pattern of `extractText()`, `extractCitations()`, and `extractThinking()`.

**Stream handler (`Stream.php`):** The stream state already tracked provider tool calls, provider tool results, and citations during streaming, but `handleToolCalls()` only included `thinking` and `thinking_signature` in the replayed `AssistantMessage`'s `additionalContent`. Now it also includes `citations`, `provider_tool_calls`, and `provider_tool_results`.
…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
wishborn and others added 10 commits July 7, 2026 01:01
…checker

composer-require-checker surfaced undeclared direct dependencies, now
declared: ext-mbstring, ext-openssl, psr/http-message, and
symfony/http-foundation (all already installed transitively via
laravel/framework, so this changes nothing for consumers). laravel/mcp
moves to suggest for the optional LaravelMcpTool integration; its
symbols plus the PHPUnit assertion used by the testing fakes are
whitelisted in composer-require-checker.json.

The custom ReorderMethodsRector was dev tooling shipping inside src/
(the source of phantom PhpParser/Rector symbols) - moved to dev/ under
a Prism\Dev namespace, autoloaded only in autoload-dev and
export-ignored from the dist package.

New Require Checker CI workflow runs the tool on every push/PR. This is
the proper guard for the goal upstream prism-php#1025 aimed at (catching use of
undeclared symbols) without splitting laravel/framework into
illuminate/* packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…penAI verbosity

Three bug fixes from the upstream issue tracker plus one found in review:

- DeepSeek and Qwen streaming handlers sent `stream: true` in the request
  body but never set the Guzzle `stream` transport option, so the full
  response body was buffered before the first event yielded — matching the
  other 14 providers now (upstream prism-php#990; Qwen found in review).
- OpenRouter ToolCallMap eagerly ran json_decode() and passed the result to
  the ToolCall constructor, so malformed model JSON produced a raw TypeError
  (json_decode returns null) instead of a handled error. The map now stores
  the raw argument string; ToolCall::arguments() decodes lazily and wraps a
  decode failure in PrismException::malformedToolCallArguments(), which the
  tool-execution loop already converts into a tool result the model sees.
  The loop's error path is hardened to not re-throw while building that
  result (upstream prism-php#1006).

Also adds OpenAI text_verbosity support to the structured (Responses) path
and both chat/completions paths — it was previously only wired on the
Responses text path (upstream prism-php#1015).

Tests: OpenRouter ToolCallMap mapping + lazy-decode behavior, verbosity
pass-through for structured + chat/completions, updated the ToolCall value
object test to assert the handled PrismException. 1,891 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stency

Per upstream prism-php#1017: promptTokens is normalized to exclude cached tokens
everywhere, but completionTokens is inclusive of reasoning tokens on
OpenAI/Anthropic/OpenAI-compatible providers and exclusive on Gemini/Vertex.
Documents the difference rather than changing billing-relevant numbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s-vertex

feat(vertex): added support for multi region endpoints
Introduce the Tier-1 telemetry substrate for observability (upstream prism-php#935):
a provider-agnostic set of plain Laravel events emitted across the generation
lifecycle, decoupled from the existing ShouldBroadcast streaming events so
telemetry never forces websocket delivery.

- TelemetryContext: correlation VO carrying a stable traceId plus explicit
  stepIndex/toolIndex ordinals, so a consumer can rebuild the span tree
  deterministically without ambient context surviving the tool loop.
- ContextStack: container-bound ambient stack; tolerates out-of-order removal
  so an abandoned streaming generator cannot corrupt later calls.
- Telemetry: static emission helper; a complete no-op when disabled.
- Events: GenerationStarted/Completed/Failed, StepCompleted, ToolInvoked.
- config: prism.telemetry.{enabled,capture_content}, both off by default
  (capture_content gates prompt/completion/tool-arg payloads — PII).
- Bind ContextStack as a singleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion paths

Wire the neutral telemetry events into the provider-agnostic chokepoints so
instrumentation lives in one place per path, touching no provider handler.

- asText / asStructured / asEmbeddings / generate (images): start on entry,
  completed on success (with per-step StepCompleted for text/structured),
  failed on RequestException, context popped in finally.
- asStream: wrap the provider generator via Telemetry::instrumentStream, which
  emits StepCompleted per StepFinishEvent and GenerationCompleted on
  StreamEndEvent while passing every event through untouched.
- CallsTools: executeToolCall now measures and returns its own duration;
  ToolInvoked is dispatched from the in-process parent (executeToolsWithConcurrency
  and the approval-resume path) so listeners never fire in a forked child.
- Tests: disabled no-op, started/completed, content-capture gating, per-step
  events, failure, streaming pass-through, and tool-invocation ordinals.

Full suite green (1909 passed), PHPStan level 8 clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security review found the capture_content privacy control was applied to
GenerationStarted, StepCompleted, and GenerationCompleted but NOT to
ToolInvoked, so tool arguments and tool results (which can carry user PII)
were placed into the event payload even when capture_content was disabled —
contradicting the documented opt-out and leaking content to whatever sink the
operator wired.

Fix: ToolInvoked now carries always-present scalar `toolName`/`toolCallId`
(safe span metadata) while the content-bearing `toolCall`/`toolResult` are
nullable and populated only when capture_content is enabled — matching the
nullable-content pattern of the other three events. Added a regression test
asserting content is withheld by default and present only when enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ceof)

The security-fix commit ran Pint but not Rector; the Formatting CI job runs
`rector` + `git diff --exit-code`, which flagged the `=== null` assertions.
FlipTypeControlToUseExclusiveTypeRector rewrites them to
`! $e->toolCall instanceof ToolCall`, matching the repo's enforced style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Anthropic streaming and Groq/Mistral/OpenAI audio handlers call
json_validate(), a PHP 8.3 built-in, while composer.json declares "php": "^8.2"
and the test matrix runs 8.2. Without a polyfill this fatals on real 8.2 in
those paths, and composer-require-checker (run on 8.2) flagged json_validate as
an unknown symbol — a red check on every push since the checker was added.

Add symfony/polyfill-php83 to require: it defines json_validate() on 8.2,
fixing the latent runtime bug and satisfying the require-checker while keeping
8.2 support (vs. a whitelist entry, which would only silence the checker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wishborn and others added 12 commits July 14, 2026 11:21
fix: polyfill json_validate() for PHP 8.2 support
docs: point README to the ai.particle.academy docs site
The recursive tool loop runs step N's tools before step N is recorded, so
ToolInvoked carried no step ordinal and a consumer could not nest a tool
span under its step. Track a per-generation step cursor on the ContextStack,
advance it once per executed tool batch, and stamp it onto every ToolInvoked.
No-op when telemetry is disabled.
…s + bounded content capture

Adopts upstream issue prism-php#935: neutral Laravel telemetry events across the generation lifecycle (context/stack, step/tool ordinals, user/session ids), bounded opt-in content capture, and a step cursor so tool events are tagged with their owning step. Off by default and a complete no-op when disabled. Validated end-to-end (real multi-step tool generation -> OpenTelemetry bridge -> Phoenix) and security-reviewed (PASS WITH WARNINGS).
Pairs with the "main protection" branch ruleset: outside contributions now
need a pull request with an approving review from a code owner before they
can land on main.
The Gemini API specifies `tools` as `Tool[]`, but four handlers built it in ways
that break that contract once more than one kind of tool is in play.

**Mixed keys → JSON object.** Gemini/Text, Gemini/Stream and Vertex/Text map
provider tools into a numerically keyed list and then assign
`$tools['function_declarations'] = …` on top. The result is a mixed-key array,
which `json_encode` emits as an object:

    "tools": {"0": {"google_search": {}}, "function_declarations": [...]}

Gemini and Vertex both reject that, so combining Google Search grounding with
any custom tool fails outright.

**Silent overwrite.** Vertex/Structured reassigns `$tools` instead of appending,
so provider tools vanish whenever custom tools are present.

**Mutual exclusion.** Vertex/Stream chained `elseif`, so custom tools were only
ever sent when there were neither provider tools nor `searchGrounding` — the
combination was impossible rather than merely malformed.

All four now append a separate Tool entry, matching Gemini/Structured, which
already had it right and serves as the reference:

    "tools": [{"google_search": {}}, {"function_declarations": [...]}]

Provider-tool precedence in Vertex/Stream (explicit provider tools over the
legacy `searchGrounding` option) is preserved; only the custom-tool branch
becomes additive.

Adds a regression test asserting `array_is_list($data['tools'])` plus both
entries. Verified it fails on the current code and passes with the fix; the 169
Gemini + Vertex tests and Pint stay green.
Extends the fix with runnable coverage for the previously untested paths:
Gemini/Text custom-tools-only, Gemini/Stream grounding + custom tools, and
Vertex/Text custom-tools-only — each asserting `tools` serializes as a JSON
array (array_is_list), the shape Gemini and Vertex require.
Covers enabling telemetry, the config block, the five lifecycle events and
their payloads, listening, TelemetryContext, user/session metadata via
withTelemetryMetadata(), the content-capture PII bounds, and exporting to
OpenTelemetry / Arize Phoenix via prism-opentelemetry. Adds it under Advanced.
Resolves the open high-severity Dependabot alert for postcss
(GHSA-r28c-9q8g-f849, path traversal via sourceMappingURL) plus two
further high advisories npm audit surfaced in the same tree:
brace-expansion (GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895) and js-yaml
(GHSA-5p4m-2wfm-xmqj).

All three are transitive dev dependencies of the VitePress docs site and
were already permitted by the existing semver ranges — the lockfile was
simply stale. Refreshed with `npm audit fix --package-lock-only`, so no
override pins and no package.json changes. `npm audit` now reports zero
vulnerabilities.
Two repository-hygiene fixes found during the v0.111.1 release audit.

**Line endings.** There was no eol rule, so on Windows (core.autocrlf=true)
every checkout writes CRLF while Pint writes LF. `git status` then reports
30+ files as modified with empty `git diff`s, and `git add -A` will stage
that noise into an unrelated commit. `* text=auto eol=lf` makes the working
tree match what the tools produce.

Renormalising touches exactly five files. Every fixture is stored with LF
except tests/Fixtures/gemini/stream-with-tools*.json, committed with CRLF
and the anomaly — including the .sse stream fixtures, whose CRLF I first
took for stored bytes when it was only a checkout artefact. Those five are
now LF like the rest; the Gemini suite (146 tests) passes unchanged, so
nothing depended on it.

**docs.** /docs was the only non-runtime path not export-ignored, so 9.6M
of committed .vitepress/dist build output and a dev package-lock.json ship
inside every Composer install — dead weight in vendor/, and a lockfile that
makes dependency scanners flag consumers for advisories in our docs
toolchain.

The directory itself must keep shipping: the docs site renders markdown AND
parses the VitePress sidebar out of vendor/particle-academy/prism/docs, and
installs with --prefer-dist. Excluding /docs wholesale would have taken that
site down. So only what nothing reads at runtime is excluded, verified
against a real `git archive`: 46 markdown files and .vitepress/config.mts
still present, dist output and lockfile gone.

1923 passed, 11 skipped.
"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).
Closes #14.

All three Vertex handlers refused provider tools alongside custom tools:

  throw new PrismException('Use of provider tools with custom tools is
      not currently supported by Vertex.');

Investigated rather than assumed, because #12 left its Structured and
Stream fixes unreachable behind these guards.

The guards are not evidence of an API restriction. They arrived with the
very first Vertex support commit (d81de06, Feb 2026) as a conservative
placeholder, not in response to Vertex rejecting anything, and were never
revisited. The Vertex handlers are otherwise near-copies of the Gemini
ones, which carry no such guard — so the same package already permits on
Gemini exactly what it forbade on Vertex, against the same API shape.

Google documents the combination as supported: "Gemini 3 models also
support combining these built-in tools with custom tools (function
calling)". That is precisely the case reported in #12 — Gemini 3 on Vertex
with Google Search grounding plus custom function tools.

Removing all four guards (the three provider-tool ones plus Stream's
separate searchGrounding one, whose message claimed Prism did not support
the combination at all — it does, on Gemini). #12 already fixed the
payload these paths emit, so the tools array serialises as a proper
Tool[]; the code was correct and simply unreachable.

Tests assert the combined payload is a list of two entries, google_search
then function_declarations, and that the call no longer throws.

Not verified against the live Vertex API — that needs Google Cloud
credentials I do not have. If Vertex does reject some model/tool
combination, the caller now gets Google's own error, which is more
accurate than a blanket Prism exception that contradicts Google's docs.

Pint · PHPStan clean · 1925 passed, 11 skipped.
Upstream PR by @mrmorgan-i. Browsers report the CONTAINER type, so an
audio-only MediaRecorder clip arrives as video/webm or video/mp4 and was
being written out as audio.mp3 — the wrong extension on every browser
recording, which providers then reject or mis-transcode.

Applied upstream's mapping (video/mp4, video/ogg, video/webm) plus one gap
it does not cover: MediaRecorder attaches codec parameters, so the very
mime types this fix targets arrive as "audio/webm;codecs=opus" and fell
straight through to the mp3 default anyway. The mime type is now reduced to
its bare type and lowercased before matching, so the fix works on real
browser output rather than only on the canonical strings.
Inspired by prism-php#1022 by @cerebrixos, rewritten rather than
absorbed.

The gap that PR identified is real: `url` appears in the config block but
nothing on the page says it can point somewhere other than OpenAI, so the
single most common deployment question — "can I put this behind vLLM / a
gateway / Azure" — is unanswered.

The upstream PR answers it with one vendor's product as the worked example,
including their hostname, their env var name, and a paragraph of their
positioning. Merging that would put a supplier advertisement in our
provider docs and date the page to that supplier. So this documents the
capability instead: a neutral placeholder URL, self-hosted runtimes and
gateways named as categories, and no vendor endorsed.

Also adds what a user actually gets wrong here and the upstream text omits:
"OpenAI-compatible" rarely covers the whole surface, so an unsupported
feature fails at the endpoint rather than in Prism; and model names drive
capability inference, so a llama model behind the OpenAI provider is not
treated as structured-output capable. Closes with a pointer to the dedicated
providers, which map their APIs' real quirks.
…#16)

Follow-up to the falsy-"0" fix. The message maps built their payloads with a
bare `array_filter()`, which drops EVERY falsy value. That is correct for a
null cache_control or an empty tool_calls array — and wrong for
`'content' => '0'`, which is falsy in PHP but is ordinary model output.

The recurring shape, present in eight providers:

    array_filter([
        'role' => 'assistant',
        'content' => $message->content,   // '0' silently removed
        'tool_calls' => $toolCalls,       // [] correctly removed
    ])

Because the same array mixes a content value with a collection that SHOULD
vanish when empty, neither a plain `array_filter` nor `Arr::whereNotNull`
is right. Added Providers\Support\Payload::compact(), which keeps the
original intent — no null, no empty string, no empty array, no false — and
keeps scalar zero in string and int form.

Converted 21 content-bearing sites across Anthropic, Azure, Groq, Mistral,
Ollama, OpenAI (chat completions), OpenRouter, Qwen, Requesty and XAI,
covering assistant content, user text, system prompts, tool-result content
and citation text. The Anthropic site fixed with Arr::whereNotNull in the
previous commit now uses the same helper, so there is one mechanism.

Deliberately NOT converted: the remaining array_filter calls carry file ids,
model names, provider options, schema fragments and tool names/descriptions
— no message content — so changing them would alter provider payloads for
no benefit. One two-argument call in Anthropic's CitationsMapper already had
an explicit null-only callback and was correct; a first pass converted it by
mistake and PHPStan caught it.

Tests: tests/Regression/PayloadCompactTest.php, 20 cases. Verified against
the unfixed source — the 10 "keeps 0" cases fail, and the 8 "still omits an
empty tool_calls array" guards pass, confirming the strip behaviour the
array_filter existed for is unchanged.

Pint · PHPStan clean · 1950 passed, 11 skipped.

Known, not addressed: ElevenLabs' speech-to-text call array_filters boolean
provider options, so `diarize: false` is dropped. Same falsy class, but a
boolean rather than "0", and dropping it may match the API default. Needs a
maintainer decision rather than a blind change.
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.