laravel/ai v1.2.0: OpenAI Decisions classification, sturdier structured output, and new Anthropic defaults
Version 1.2.0 of laravel/ai is here, and the headline is classification. OpenAI joins TypeSafe and OpenRouter as a classification provider through its new Decisions API, and classifications can now evaluate image attachments alongside the state. Structured output also gets sturdier, with fixes for Bedrock models that reject forced tool choice and for provider options that accidentally stripped the JSON schema from requests. Rounding things out, the Anthropic model defaults move up a generation, and the gateway stops sending sampling parameters to Claude models that no longer accept them.
Classification comes to OpenAI with the Decisions API
OpenAI now implements the classification provider contract, calling the new POST /v1/decisions endpoint with the same predicate, choice, and score questions you already use with TypeSafe and OpenRouter. Answers come back as the familiar Boolean, Choice, and Score types, and the default model is gpt-6-luna, configurable under models.classification.
The bigger addition is attachments. Pass images as a second argument to Classification::of() and they travel with the state:
Classification::of('Inspect the product in this photo.', [Image::fromPath($photo)])
->question('damaged', new Boolean('Does the product have visible damage?'))
->classify(provider: 'openai');
A few practical details worth knowing:
- OpenAI only accepts inline base64 images, so remote URLs and stored files are downloaded and sent inline as data URLs.
- The allow-list is JPEG, PNG, GIF, and WebP, following OpenAI's vision rules; anything else — a PDF upload, a provider-hosted image, an HTML response posing as a PNG — is rejected before any request goes out.
- The Decisions response carries no usage data yet, so token counts read zero until OpenAI starts reporting them.
- Attachments fail loudly instead of failing over: a classification with images aimed at TypeSafe or OpenRouter throws rather than silently degrading.
Because classification is experimental, its contracts may change in a minor release, and this one does: ClassificationProvider::classify() and ClassificationGateway::classify() now accept a trailing array $attachments = []. If you maintain a custom classification provider or gateway, add the parameter — there's an upgrade note covering it.
Structured output that survives picky models and provider options
Two fixes in this release make structured output behave outside the happy path.
The first (#1104, @cwilsn's first contribution — welcome!) targets Bedrock. Newer Claude models on Bedrock Converse, including claude-sonnet-5-5 and claude-opus-5-5, reject a forced tool choice — which is precisely how the gateway used to extract structured output, so requests simply failed. The gateway now recognises those models and switches strategy: the JSON schema is injected as a system instruction demanding a bare JSON answer, and the structured result is parsed from the model's text instead of a synthetic tool call. Ordinary tools stay registered with automatic selection, so an agent can still work through tool calls across steps and finish with schema-valid JSON.
The second (#1105) bites when you combine native structured output with provider options. On Anthropic, returning output_config from providerOptions() replaced the entire output_config the gateway builds, dropping the format that carries the JSON schema — so the model answered in free text. output_config is now merged one level deep: your effort and thinking settings coexist with the schema, and an explicit format you pass yourself still wins. OpenAI receives the same treatment for its text key, so setting text.verbosity no longer clobbers text.format.
Anthropic: new defaults and well-behaved sampling parameters
Claude 4.7 and later return a 400 for non-default temperature and top_p, which broke agents combining #[UseCheapestModel] with a #[Temperature] attribute. The gateway now sends sampling parameters only to Claude 3.x and 4.0–4.6 models. On newer models, #[Temperature] and #[TopP] are quietly ignored instead of failing the request. One related behaviour change: when an agent sets both parameters, only temperature is sent.
The defaults also move up a generation. The cheapest model is now claude-haiku-5-5 (previously claude-haiku-4-5-20251001), and the smartest is claude-opus-5-5 (previously claude-fable-5-1). Pin your own versions via the models.text config as usual.
Other changes in v1.2.0
- The upgrade guide gained a 1.2 section (#1108) documenting the classification signature change and noting that classification is experimental, which is why a signature break ships in a minor release.
- Thanks to @cwilsn for the first contribution, and to @kachelle and @pushpak1300 for the rest of this release.
- The full changelog lists every pull request between v1.1.0 and v1.2.0.
Takeaways
- OpenAI is now a classification provider via
POST /v1/decisions, andClassification::of()accepts image attachments that are sent inline. - Custom classification providers and gateways must add the trailing
array $attachments = []parameter toclassify(). - Structured output works again on Bedrock models that reject forced tool choice, and provider options no longer strip the JSON schema on Anthropic or OpenAI.
- Anthropic defaults are now
claude-haiku-5-5(cheapest) andclaude-opus-5-5(smartest);temperatureandtop_ponly reach Claude 3.x through 4.6.