alexbaldo/laravel-ai
A Laravel package that simplifies adding AI features to your app, providing an easy integration layer for AI providers and models with configurable support for common AI workflows.
This is a pre-1.0 package (v0.12.x), so the first step is simply pinning and upgrading:
composer update alexbaldo/laravel-ai
There is no migration or config change in v0.12.3 — it is an additive feature release. Your existing image-generation calls keep working unchanged, because the new moderation argument is optional and the package deliberately imposes no default of its own.
Where to look first, in this order:
Laravel\Ai\Providers\Tools\ImageGeneration — the tool object. It now has a ?string $moderation constructor argument alongside quality.PendingImageGeneration — the fluent entry point most day-to-day code will touch. Look for quality() and the new sibling moderation().OpenAiProvider::imageGenerationToolOptions() — where the value is actually serialised into the image_generation tool payload.GeneratesImagesViaResponses — the gateway that threads the value end to end on the Responses API path.First use case: you are building an agent/Responses API call that uses OpenAI's hosted image_generation tool, and you need to relax or tighten OpenAI-side moderation for a specific generation without switching to the classic Images API path:
use Laravel\Ai\Providers\Tools\ImageGeneration;
$tool = new ImageGeneration(quality: 'high', moderation: 'low');
or, on the pending builder (mirroring the v0.12.2 quality flow):
$image = $pending->quality('high')
->moderation('low')
->generate();
If you never call moderation(), nothing changes: the argument is omitted from the payload and OpenAI applies its own default, which it echoes back as 'auto'.
Mirror the quality pattern. v0.12.3 is intentionally a clone of v0.12.2's quality plumbing. Anything you already do for quality — passing it from a config value, exposing it as an enum/cast on a model, asserting it in tests — you can now do identically for moderation. Treat them as a pair in your own abstractions:
final class ImageRequest
{
public function __construct(
public string $prompt,
public ?string $quality = null,
public ?string $moderation = null,
) {}
}
// somewhere in a service
$pending = $builder->quality($request->quality);
if ($request->moderation !== null) {
$pending = $pending->moderation($request->moderation);
}
Keep the null check. The package only includes the key when a value is given (array_filter pattern, same as quality), and it is worth preserving that "absent means let the API decide" semantic in your own layer rather than coercing to 'auto' yourself.
Thread it through your own domain, not on the call site. The value travels a long chain — PendingImageGeneration::moderation() → ImageProvider::image() → ImageGateway::generateImage() → GeneratesImagesViaResponses::generateImageViaResponses() → the image_generation tool. That means you can inject it at the builder level, at the provider call, or at the gateway call. For day-to-day app code, stay at the builder level; only reach lower if you are implementing a custom gateway or provider.
Tool-based usage (agents / Responses API). When you assemble a tool list declaratively, the constructor form is the natural fit:
$tools = [
new ImageGeneration(moderation: 'low'),
];
$response = $provider->respond($prompt, tools: $tools);
This is the main reason the feature exists: the gap noted in ADR-0007 / GI-Q8 was that the Responses API tool path had no way to express moderation, while the classic Images API path already handled it internally.
Be explicit about provider portability. Every non-OpenAI-Responses gateway (Gemini, OpenRouter, AzureOpenAi, Xai, Bedrock, Anthropic, plus the fake gateway) accepts the parameter for interface compatibility and silently ignores it. That is by design and unchanged. So passing moderation is always safe, but it is only meaningful on one path. If moderation behaviour matters to your product, feature-flag or assert on the provider rather than assuming uniformity.
Testing. The fake gateway accepts and ignores moderation, so a fake-gateway assertion tells you your app passed the value, not that the provider honoured it. Pair a fake-gateway test for the chain with a payload-level test (HTTP client fake or a spied OpenAiProvider::imageGenerationToolOptions()) to prove the key actually lands in the image_generation tool array:
$options = $provider->imageGenerationToolOptions(/* ... */);
$this->assertSame('low', $options['moderation']);
…and a negative test that it is absent when you pass null — that absence is the contract.
Extension point. imageGenerationToolOptions() is the seam for adding further tool options. If you subclass or decorate the provider, follow the same conditional array_filter inclusion pattern so you do not accidentally start sending defaults the package intentionally omits.
Two OpenAI paths, two different behaviours. This is the single most confusing thing in v0.12.3. The Responses API image_generation tool now honours your moderation (or omits it). The classic Images API path (sendImageGenerationRequest() / sendImageEditRequest()) is untouched and keeps forcing moderation: 'low' for gpt-image* models on its own, ignoring your parameter. So swapping drivers to "fix" a moderation issue can silently change behaviour in either direction.
'low' means less filtering, not more. It is easy to read "low moderation" as "tightly moderated". OpenAI's moderation setting is the opposite: low relaxes the filter. On the classic path the package is pinning a permissive value for you; do not treat that as a safety guarantee, and do not assume the Responses path gives you one either.
Absent is not 'auto'. The package never sends 'auto' itself — it sends nothing and OpenAI defaults, echoing back 'auto'. If you read the value back or log it, do not expect your builder state to say 'auto' just because the API responded that way. Your null and the API's 'auto' are different facts.
array_filter without a callback drops all falsy values. The inclusion logic follows the quality pattern, so null is dropped (intended). But if you extend imageGenerationToolOptions() or add your own optional tool keys in the same style, remember '', '0', 0, false and [] would also vanish. For moderation ('low'/'auto') this is fine; for future numeric or boolean options it is a footgun.
Silent no-ops across providers. Passing moderation to Gemini, Anthropic, Bedrock, Xai, OpenRouter or Azure will not throw, warn, or log. If a user reports "moderation isn't working", the first question is which gateway is actually resolving, not whether the value was passed.
Debugging the payload. When a value does not seem to reach OpenAI, dump the tool array rather than the builder: imageGenerationToolOptions() is the last place the package controls before the request leaves. Compare a null call (key absent) with a 'low' call (key present) to confirm you are on the Responses gateway at all — if the tool array never appears, you are on the classic Images API path or a non-OpenAI driver.
Version churn. v0.12.2 added quality, v0.12.3 added moderation, both as optional constructor parameters with no defaults. Pre-1.0 releases in this line are additive and behaviour-preserving, but the constructor signature on ImageGeneration is now positional-sensitive — if you instantiate it positionally rather than with named arguments, prefer named arguments to stay upgrade-safe.
Related work. The feature references ADR-0007 / GI-Q8. If you are deciding where to invest integration effort, read that ADR alongside the changelog — it explains the intended split between the Responses tool path and the classic Images API path, which the release notes only hint at.
How can I help you explore Laravel packages today?