Introduction
Laravel is a natural place to run AI features: generate drafts, summarize tickets, classify support messages, or power an internal assistant. The hard part is not calling a model once—it is wiring the SDK so keys stay safe, requests do not block users, and model output is validated before you trust it.
This tutorial walks through a practical pattern for using an AI SDK inside a Laravel app.
What you will build
- An AI client bound in the service container
- A thin service for chat / text generation
- A queued Job for slow requests
- Basic validation and logging around model output
Prerequisites
- Laravel 10 or 11
- PHP 8.1+
- An API key from your AI provider
- Queue driver configured for production (
databaseorredis)
Step 1: Store secrets correctly
Never hardcode API keys. Put them in .env and map them through config/:
AI_API_KEY=
AI_BASE_URL=https://api.openai.com/v1
AI_MODEL=gpt-4.1-mini
AI_TIMEOUT=30
// config/services.php
'ai' => [
'key' => env('AI_API_KEY'),
'base_url' => env('AI_BASE_URL'),
'model' => env('AI_MODEL', 'gpt-4.1-mini'),
'timeout' => (int) env('AI_TIMEOUT', 30),
],
Step 2: Install and bind the SDK
Exact package names differ by provider. The important part is a single application service that wraps the vendor client:
composer require openai-php/laravel
php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"
namespace App\Services\AI;
use OpenAI\Client;
class TextGenerator
{
public function __construct(
private Client $client,
private string $model
) {}
public function complete(string $system, string $user): string
{
$response = $this->client->chat()->create([
'model' => $this->model,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
'temperature' => 0.3,
]);
return trim((string) ($response->choices[0]->message->content ?? ''));
}
}
// AppServiceProvider
$this->app->singleton(TextGenerator::class, function () {
return new TextGenerator(
client: \OpenAI::client(config('services.ai.key')),
model: config('services.ai.model'),
);
});
Adjust the client construction to match the SDK you chose. Keep one binding so Controllers never build clients ad hoc.
Step 3: Call it from a Controller (short requests only)
use App\Services\AI\TextGenerator;
use Illuminate\Http\Request;
public function summarize(Request $request, TextGenerator $ai)
{
$data = $request->validate([
'text' => ['required', 'string', 'max:8000'],
]);
$summary = $ai->complete(
'Summarize the text in 3 bullet points. No fluff.',
$data['text']
);
abort_if($summary === '', 502, 'Empty model response');
return response()->json(['summary' => $summary]);
}
Use synchronous calls only when latency is acceptable. Anything that may take several seconds should go to a Queue.
Step 4: Move slow work to a Job
php artisan make:job GenerateArticleDraft
namespace App\Jobs;
use App\Models\Post;
use App\Services\AI\TextGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateArticleDraft implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 90;
public function __construct(public Post $post) {}
public function handle(TextGenerator $ai): void
{
$draft = $ai->complete(
'Write a clear technical article draft in Markdown.',
'Topic: ' . $this->post->title
);
$this->post->update([
'content' => $draft,
'status' => 'draft',
]);
}
}
GenerateArticleDraft::dispatch($post);
Step 5: Validate model output
Treat LLM text as untrusted input:
- Reject empty responses
- Enforce max length before saving
- Strip or escape HTML if the output will be rendered
- For structured tasks, ask for JSON and decode with validation rules
$json = $ai->complete(
'Return JSON only with keys title and outline (array of strings).',
$topic
);
$data = json_decode($json, true);
abort_if(! is_array($data), 422, 'Invalid AI JSON');
$validated = validator($data, [
'title' => ['required', 'string', 'max:160'],
'outline' => ['required', 'array', 'min:3'],
'outline.*' => ['string', 'max:200'],
])->validate();
Best practices
- Keep prompts in classes or config—not scattered across Controllers
- Log model, latency, and failure reasons (never log full secrets)
- Add rate limiting on public AI endpoints
- Prefer draft-first flows for generated content
- Write feature tests with a fake client so CI does not call the live API
Common mistakes
- Putting
AI_API_KEYin the repository - Calling the model inside a web request that users must wait on
- Saving raw HTML from the model without sanitizing
- No timeout / retry strategy
- One giant “do everything” prompt instead of narrow tasks
Conclusion
Using an AI SDK in Laravel is mostly good engineering: config for secrets, a service boundary for the vendor client, queues for slow work, and validation before persistence. Once that skeleton exists, swapping models or providers becomes a small change instead of a rewrite.
FAQ
Which AI SDK should I pick?
Pick the official or well-maintained client for your provider, then wrap it. Your app should depend on TextGenerator, not on vendor classes everywhere.
Can I stream responses in Laravel?
Yes, many SDKs support streaming. Use it for chat UIs; keep queued Jobs for batch generation.
Do I need Redis?
Not to start. database queues work. Redis helps when AI traffic and retries grow.
