Skip to content

Systemic tool-call normalization bug: multiple providers crash with raw TypeError on missing or scalar arguments #1007

@ruttydm

Description

@ruttydm

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:

"1"

or

1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions