Structured output
When the answer has to be JSON, ask the provider to guarantee it rather than asking the model nicely.
$response = $client->chat( messages: $messages, model: 'gpt-4.1-mini', schema: [ 'name' => 'classification', 'schema' => [ 'type' => 'object', 'properties' => [ 'category' => ['type' => 'string', 'enum' => ['invoice', 'contract', 'other']], 'confidence' => ['type' => 'number'], ], 'required' => ['category', 'confidence'], 'additionalProperties' => false, ], ], ); $data = json_decode($response->content, true);
Why not just ask for it
The pattern this replaces is everywhere, and it is begging:
$prompt = 'Return ONLY the raw JSON, no markdown, no ```'; $clean = preg_replace('/^```json|```$/', '', trim($response->content)); $data = json_decode($clean, true);
It works until it does not. The day the content contains a quote the model does not escape, json_decode returns null and the user gets a generic error with no cause. In the application this came from that happened with measurement tables in official gazettes, where a value like 3'0" broke the JSON, and asking for it in plain text made it worse: the model read that as a request for verbatim reproduction and refused.
A schema removes the class of problem rather than the last instance of it.
The enum is the point
strict makes an enum in the schema binding, which is what you actually wanted every time you set temperature: 0.1 on a classifier. Temperature reduces variance. A schema guarantees the shape, and an enum guarantees the value is one of yours.
The two providers do it differently
OpenAI has response_format: json_schema with strict: true, which is the direct answer.
Anthropic has no equivalent. Its documented way to guarantee a shape is to declare one tool whose input schema is that shape, and force the model to call it; the arguments come back already validated.
That has a consequence worth stating plainly: on Anthropic, a schema and a tool loop are mutually exclusive. The model is being forced into one call, so it cannot reach for anything else. This is not a limitation of the package so much as what "guarantee me a shape" means on that provider, and it is usually fine, because a caller asking for an answer in a shape is not asking for a conversation.
The client hides the difference. Decoding a schema-backed reply reads $response->content either way.
Building the schema
The tool base class already builds one from Illuminate\JsonSchema, and you can use the same thing here:
use Illuminate\JsonSchema\JsonSchema; use Illuminate\JsonSchema\Serializer; Serializer::serialize(JsonSchema::object([ 'category' => JsonSchema::string()->enum(['invoice', 'contract', 'other']), ]));
One caveat if you build it that way for OpenAI: strict mode requires additionalProperties: false, and the framework's serialiser does not emit it. Add it yourself, or write the array by hand for schemas that need strict mode.