laravel/ai: Use OpenRouter images endpoint for image generation
Contributed by pushpak1300
Part of the ai v1.2.1 release
Image generation through the OpenRouter gateway now targets OpenRouter's dedicated /images endpoint instead of chat/completions (PR #1116, fixes #1114). The previous implementation packaged every image request as a chat completion — a messages array plus modalities: ['image'] — which only chat models capable of emitting images could answer. Image-only families such as Flux responded with a 404. This release routes image work to the endpoint OpenRouter built for it, and it lands as fully backward compatible for anyone upgrading from v1.2.0.
What changed under the hood
The OpenRouter gateway's generateImage() method now POSTs to images with a flat payload: model, prompt, an optional aspect_ratio, an optional resolution, and input_references when source images are attached. The nested image_config object, the messages array, and the buildImageMessages() helper that produced it are all gone.
Response parsing was updated to match the new contract. Generated images are read from data[].b64_json with a media_type field — defaulting to image/png when a provider omits it — instead of being extracted from data URIs embedded in a chat message. Token usage still comes from usage.prompt_tokens and usage.completion_tokens, an empty data array still yields an empty images collection, and meta->model now reports the model identifier you requested, since the images endpoint doesn't echo it back.
Why it matters
Models like black-forest-labs/flux-3-image, bytedance-seed/seedream-4.5, and x-ai/grok-imagine-image-2.0 are image-only: they don't participate in chat/completions at all, so generating with them through OpenRouter previously failed outright. With the dedicated endpoint, every image model family OpenRouter routes to is reachable through the same Image API, so you can pick models on capability rather than on which transport they happen to support. It also removes a Gemini-family assumption that was baked into the old option names and docblocks.
Your application code doesn't change
Nothing about the public API moves. Sizes, quality tiers, and attachments behave exactly as before:
use Laravel\Ai\Image;
$response = Image::of('A donut sitting on a kitchen counter.')
->landscape()
->quality('high')
->generate(provider: 'openrouter', model: 'black-forest-labs/flux-3-image');
foreach ($response->images as $image) {
Storage::disk('public')->put('donut.png', base64_decode($image->image));
}
landscape() still maps to aspect_ratio: '3:2', and quality('high') still resolves to the 4K tier — it's just transmitted as resolution now. Editing from a reference image works too, with attachments sent as input_references instead of chat content parts:
$response = Image::of('Turn this into a watercolor painting.')
->attachments([new LocalImage(resource_path('images/red.png'))])
->generate(provider: 'openrouter', model: 'google/gemini-3.1-flash-image');
Provider options: same input, clearer precedence
If you pass OpenRouter-specific options via withProviderOptions(), the image_config key is still accepted. The gateway pulls it out and forwards its aspect_ratio and image_size values as the equivalent top-level aspect_ratio and resolution fields:
Image::of('A neon skyline at dusk')
->withProviderOptions([
'image_config' => ['aspect_ratio' => '21:9', 'image_size' => '2K'],
])
->generate(provider: 'openrouter', model: 'bytedance-seed/seedream-4.5');
One nuance worth knowing: when both are set, the fluent methods win. In the test suite, landscape() plus quality('high') alongside an image_config of '21:9' / '1K' produced aspect_ratio: '3:2' and resolution: '4K' on the wire.
Upgrade impact and verification status
For most applications there is nothing to do beyond pulling the release. Two edge cases deserve a quick look:
- If you have custom tests, proxies, or middleware asserting on the raw OpenRouter image request or response, update them: requests go to
https://openrouter.ai/api/v1/images, and responses usedata[].b64_jsonandmedia_type. - If you rely on
$response->meta->modelmatching a provider-echoed model string, note that it now reports the model you passed in.
Live coverage so far: basic and square generation pass for Gemini, GPT Image, Flux, Seedream, and Grok models through OpenRouter. The reference-image + resolution + landscape combination is wired into the integration suite but hasn't been run against the live API yet — worth a quick staging smoke test if image editing is on your critical path.
Takeaways
- OpenRouter image generation now uses the
/imagesendpoint, fixing 404s for image-only models like Flux. - The public
ImageAPI is unchanged; upgrading from v1.2.0 requires no code changes. quality()still maps to 1K/2K/4K, now sent asresolution;image_configprovider options are flattened to top-level fields.- Fluent size/quality methods take precedence over
image_configwhen both are set. - Verified live for basic and square generation across five model families; the reference-image landscape/resolution test is still pending.