I dug further after filing #1006 and found this is not an OpenRouter-only bug. It looks like prism-php/prism has a broader tool-call normalization issue across multiple providers and both text + stream paths.
Summary
Multiple provider-specific mappers/handlers pass nullable or scalar-decoded tool arguments directly into Prism\Prism\ValueObjects\ToolCall, whose constructor requires array|string for $arguments and string for $name.
That means malformed, partial, or edge-case provider payloads can escape as raw PHP TypeErrors instead of being handled as provider/parsing errors.
Why this matters
Tool-call payloads are model-generated. Missing args, partial args, or syntactically valid scalar JSON are all realistic failure modes.
Right now, those cases can hard-crash Prism instead of degrading gracefully.
Confirmed affected paths
Static / non-stream mapping
Providers/OpenRouter/Maps/ToolCallMap.php
Providers/OpenAI/Maps/ToolCallMap.php
Providers/Gemini/Maps/ToolCallMap.php
Providers/DeepSeek/Maps/ToolCallMap.php
Text handlers
Providers/Groq/Handlers/Text.php
Providers/Mistral/Handlers/Text.php
Providers/XAI/Handlers/Text.php
Providers/Ollama/Handlers/Text.php
Stream handlers
Providers/Groq/Handlers/Stream.php
Providers/OpenRouter/Handlers/Stream.php
Providers/Mistral/Handlers/Stream.php
Providers/XAI/Handlers/Stream.php
Providers/Ollama/Handlers/Stream.php
Two failure classes
1. Missing / null arguments
Several code paths do some variation of:
new ToolCall(
id: data_get($toolCall, 'id'),
name: data_get($toolCall, 'name') or data_get($toolCall, 'function.name'),
arguments: data_get($toolCall, 'arguments') or data_get($toolCall, 'function.arguments'),
)
If the provider payload omits arguments, data_get(...) returns null and Prism throws a raw TypeError.
2. Scalar JSON in stream handlers
Some stream handlers attempt to decode JSON first, which is good, but they do not verify the decoded type before constructing ToolCall.
For example, valid JSON like:
or
can become a scalar (string or int depending on handling path), and int(1) then gets passed into ToolCall, causing another raw TypeError.
Minimal repros
A. Missing args in static mapper
Prism\Prism\Providers\OpenAI\Maps\ToolCallMap::map([
['id' => '1', 'name' => 'demo'],
]);
Observed result:
TypeError: Prism\Prism\ValueObjects\ToolCall::__construct(): Argument #3 ($arguments) must be of type array|string, null given
B. Missing args in provider-specific mapper
Prism\Prism\Providers\Gemini\Maps\ToolCallMap::map([
['functionCall' => ['name' => 'demo']],
]);
Observed result:
TypeError: Prism\Prism\ValueObjects\ToolCall::__construct(): Argument #3 ($arguments) must be of type array|string, null given
C. Scalar JSON in stream path
Using a small subclass around the protected stream mapper:
$client = Http::baseUrl('https://example.com');
$handler = new class($client) extends \Prism\Prism\Providers\OpenRouter\Handlers\Stream {
public function expose(array $payload) { return $this->mapToolCalls($payload); }
};
$handler->expose([
['id' => '1', 'function' => ['name' => 'demo', 'arguments' => '1']],
]);
Observed result:
TypeError: Prism\Prism\ValueObjects\ToolCall::__construct(): Argument #3 ($arguments) must be of type array|string, int given
I confirmed the same scalar-JSON crash pattern in Groq and Mistral stream handlers too.
Expected
All provider/tool-call parsing paths should normalize arguments defensively before constructing ToolCall, for example:
- missing args =>
[] or raw string fallback
- decoded scalar JSON => preserve as raw string or wrap in a stable structure
- malformed JSON => provider/parsing exception or raw-string fallback, but not a PHP
TypeError
- missing tool name => handled provider/parsing error, not a constructor crash
Actual
A malformed or edge-case provider payload can throw a raw TypeError from inside Prism internals.
Existing related issue
If useful, I can turn this into a PR that introduces a shared normalization helper and applies it consistently across the affected providers.
I dug further after filing #1006 and found this is not an OpenRouter-only bug. It looks like
prism-php/prismhas a broader tool-call normalization issue across multiple providers and both text + stream paths.Summary
Multiple provider-specific mappers/handlers pass nullable or scalar-decoded tool arguments directly into
Prism\Prism\ValueObjects\ToolCall, whose constructor requiresarray|stringfor$argumentsandstringfor$name.That means malformed, partial, or edge-case provider payloads can escape as raw PHP
TypeErrors instead of being handled as provider/parsing errors.Why this matters
Tool-call payloads are model-generated. Missing args, partial args, or syntactically valid scalar JSON are all realistic failure modes.
Right now, those cases can hard-crash Prism instead of degrading gracefully.
Confirmed affected paths
Static / non-stream mapping
Providers/OpenRouter/Maps/ToolCallMap.phpProviders/OpenAI/Maps/ToolCallMap.phpProviders/Gemini/Maps/ToolCallMap.phpProviders/DeepSeek/Maps/ToolCallMap.phpText handlers
Providers/Groq/Handlers/Text.phpProviders/Mistral/Handlers/Text.phpProviders/XAI/Handlers/Text.phpProviders/Ollama/Handlers/Text.phpStream handlers
Providers/Groq/Handlers/Stream.phpProviders/OpenRouter/Handlers/Stream.phpProviders/Mistral/Handlers/Stream.phpProviders/XAI/Handlers/Stream.phpProviders/Ollama/Handlers/Stream.phpTwo failure classes
1. Missing / null arguments
Several code paths do some variation of:
If the provider payload omits
arguments,data_get(...)returnsnulland Prism throws a rawTypeError.2. Scalar JSON in stream handlers
Some stream handlers attempt to decode JSON first, which is good, but they do not verify the decoded type before constructing
ToolCall.For example, valid JSON like:
"1"or
1can become a scalar (
stringorintdepending on handling path), andint(1)then gets passed intoToolCall, causing another rawTypeError.Minimal repros
A. Missing args in static mapper
Observed result:
B. Missing args in provider-specific mapper
Observed result:
C. Scalar JSON in stream path
Using a small subclass around the protected stream mapper:
Observed result:
I confirmed the same scalar-JSON crash pattern in Groq and Mistral stream handlers too.
Expected
All provider/tool-call parsing paths should normalize arguments defensively before constructing
ToolCall, for example:[]or raw string fallbackTypeErrorActual
A malformed or edge-case provider payload can throw a raw
TypeErrorfrom inside Prism internals.Existing related issue
If useful, I can turn this into a PR that introduces a shared normalization helper and applies it consistently across the affected providers.