Text generation gets the attention, but a lot of real product work is visual: read this receipt, moderate this upload, pull a serial number off a photo. Laravel AI SDK image input handles all of it through the same agent API you already use for text — attach an image, ask a question, and a vision-capable model answers. No separate HTTP client, no base64 juggling.
If you're new to the package, start with the complete guide to the Laravel AI SDK. This article assumes it's installed and picks up at the image attachment API.
Install and configure a vision-capable AI provider#
Vision is a property of the model, not the SDK. You need a provider and model that accept images — OpenAI, Anthropic, and Gemini all qualify. Install the SDK, publish its config and migrations, then make sure your default model is multimodal.
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
Add the provider key you intend to use to your .env:
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
Now create a dedicated agent. An agent is just a class that holds your instructions and gets prompted.
php artisan make:agent ImageAnalyzer
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
use Stringable;
class ImageAnalyzer implements Agent
{
use Promptable;
public function instructions(): Stringable|string
{
return 'You are a vision assistant. Describe the attached image factually and concisely. If you are unsure, say so.';
}
}
Attach an image to a Laravel AI SDK prompt#
Pass an attachments array to prompt(). The Laravel\Ai\Files\Image class gives you three source helpers, and the SDK normalises whatever you hand it into the format each provider expects.
use App\Ai\Agents\ImageAnalyzer;
use Laravel\Ai\Files;
$response = (new ImageAnalyzer)->prompt(
'What is in this image?',
attachments: [
Files\Image::fromStorage('uploads/receipt.jpg'), // from a filesystem disk
// Files\Image::fromPath('/var/www/storage/photo.jpg'), // from a local path
// Files\Image::fromUrl('https://example.com/photo.jpg'), // from a remote URL
],
);
return (string) $response;
fromStorage() reads from your default disk. Pass the disk: argument to target another one:
Files\Image::fromStorage('receipt.jpg', disk: 's3');
Ask the model a question about the image#
A string answer is fine for a chatbot, but most features want data you can act on. Implement HasStructuredOutput and define a schema, and the model is forced to return JSON that matches it. This is the same mechanism covered in Laravel AI SDK structured output, applied to a picture instead of text.
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
class ImageClassifier implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): Stringable|string
{
return 'Classify the attached image and summarise what it shows.';
}
/**
* @return array<string, mixed>
*/
public function schema(JsonSchema $schema): array
{
return [
'summary' => $schema->string()->required(),
'category' => $schema->string()
->enum(['receipt', 'screenshot', 'product-photo', 'document', 'other'])
->required(),
'confidence' => $schema->integer()->min(1)->max(10)->required(),
];
}
}
Prompt it the same way and read the result like an array:
$result = (new ImageClassifier)->prompt(
'Classify this image.',
attachments: [Files\Image::fromStorage('uploads/receipt.jpg')],
);
$result['category']; // "receipt"
$result['confidence']; // 9
Because the API is unified, switching providers is a per-call argument — the prompt and schema don't change. If OpenAI is rate-limiting you, send the same call to Gemini:
use Laravel\Ai\Enums\Lab;
(new ImageClassifier)->prompt(
'Classify this image.',
attachments: [Files\Image::fromStorage('uploads/receipt.jpg')],
provider: Lab::Gemini,
);
For automatic failover rather than a manual swap, see provider fallback and failover with the Laravel AI SDK.
Handle user-uploaded image input from a storage disk#
Real input comes from a form, not a hard-coded path. Validate the upload, store it on a disk, then attach it from there. Storing first — rather than attaching the raw UploadedFile — means the file survives into a queued job and can be retried without the original request.
<?php
namespace App\Http\Controllers;
use App\Ai\Agents\ImageClassifier;
use Illuminate\Http\Request;
use Laravel\Ai\Files;
class ImageAnalysisController extends Controller
{
public function __invoke(Request $request)
{
$request->validate([
'photo' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], // 5 MB
]);
// Store on a disk so the bytes outlive the request (and any queued retries).
$path = $request->file('photo')->store('uploads', 's3');
$result = (new ImageClassifier)->prompt(
'Classify this uploaded image.',
attachments: [Files\Image::fromStorage($path, disk: 's3')],
);
return response()->json([
'summary' => $result['summary'],
'category' => $result['category'],
'confidence' => $result['confidence'],
]);
}
}
If you'd rather skip storage entirely, you can attach the UploadedFile straight into the array with attachments: [$request->file('photo')] — but for anything that runs on a queue, store it first. When uploads are large or you want them to go straight to S3 without passing through PHP, pair this with Livewire direct-to-S3 temporary uploads.
Because this is just an agent, you can fake it in a feature test — no real API calls, no spend:
use App\Ai\Agents\ImageClassifier;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
it('classifies an uploaded image', function () {
Storage::fake('s3');
// Auto-generates a fake response matching the agent's schema — no provider call.
ImageClassifier::fake();
$this->post('/images/analyze', [
'photo' => UploadedFile::fake()->image('receipt.jpg'),
])->assertOk();
ImageClassifier::assertPrompted('Classify this uploaded image.');
});
Validate and guard image inputs before sending#
Validation isn't only about rejecting junk. Image input maps directly to tokens and spend, and every provider has hard ceilings. Guard three things before a request leaves your app: file type, file size, and resolution.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AnalyzeImageRequest extends FormRequest
{
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'photo' => [
'required',
'image',
'mimes:jpg,jpeg,png,webp', // formats every major vision provider accepts
'max:5120', // 5 MB — the lowest common ceiling (Anthropic)
'dimensions:max_width=8000,max_height=8000',
],
];
}
}
The size rule matters because the ceilings differ: OpenAI accepts up to 50 MB per payload, Gemini takes up to 20 MB inline before you need its File API, and Anthropic caps at 5 MB and 8000×8000 px per image. Validate to the lowest ceiling you might realistically hit and you stay safe across all three. If you want these rules to fire live as the user picks a file, rather than only on submit, wire them up with Precognition live form validation — the same Form Request runs in both places.
Gotchas with Laravel AI SDK image input#
A few things bite people the first time they wire this up.
- Vision is a model capability. Point the SDK at a text-only model and you'll get an error or a refusal, not an image read. Confirm
config/ai.php(or the per-callmodel:argument) resolves to a multimodal model. fromUrl()makes the provider fetch the URL. Private, authenticated, or short-lived signed URLs will fail on their end. PreferfromStorage()with a readable disk, or store the file with->put()first.- GIFs must be non-animated. Only a single frame is read, so don't expect motion or sequence to be understood.
- Token cost scales with resolution. A 12-megapixel phone photo costs far more than a 1024px-wide downscale that answers the same question. Resize aggressively before sending.
- Don't ship PII by accident. Receipts, IDs, and screenshots routinely contain personal data — check your provider's data-retention policy before sending user images to a third party.
Wrapping Up#
Image input is the same agent, the same prompt() call, plus an attachments array — the SDK handles the provider-specific encoding for you. Build a dedicated agent, return structured output so you get data instead of paragraphs, and validate size and format before you spend a token.
Two natural next steps: lock the model's responses to a strict shape with Laravel AI SDK structured output, and make the feature resilient with provider fallback and failover so one provider's outage doesn't take image analysis down. If those images eventually feed a retrieval system, see building a RAG pipeline with the Laravel AI SDK and pgvector.
FAQ#
How do I send an image to an LLM in Laravel?
Install the Laravel AI SDK, create an agent with php artisan make:agent, then call its prompt() method with an attachments array containing a Laravel\Ai\Files\Image instance. The SDK encodes the image for whichever provider you've configured and returns the model's answer, so you never touch the provider's HTTP API directly.
Which providers support vision in the Laravel AI SDK?
The image-capable providers are OpenAI, Anthropic, and Gemini. Because the SDK exposes a unified API, the same attachment code works across all three — you pick the provider in config/ai.php or per call with the provider: argument. Just make sure the specific model you target is a multimodal one, since vision is a model feature rather than an SDK one.
Can the Laravel AI SDK read text from an image (OCR)?
Yes. Vision models extract text well, so the SDK can read receipts, screenshots, and signage by attaching the image and asking for the text. For dense, multi-page scanned documents a dedicated OCR engine is still more reliable, but for most in-app cases the model's reading is good enough and can return structured data in a single step.
How do I attach a user-uploaded image to an AI prompt?
Validate the upload, store it on a disk with $request->file('photo')->store(...), then attach it using Files\Image::fromStorage($path, disk: ...). You can also pass the UploadedFile straight into the attachments array, but storing it first is safer for queued jobs and retries because the bytes outlive the original request.
What image formats does the Laravel AI SDK support?
That's set by the provider, not the SDK. JPEG, PNG, WebP, and non-animated GIF are accepted across OpenAI, Anthropic, and Gemini, so validating uploads to that set is safe. Watch the size ceilings too: roughly 5 MB for Anthropic, 20 MB inline for Gemini, and up to 50 MB for OpenAI.