What Is the Laravel AI SDK: A Practical Guide to Laravel's Native AI SDK
You spin up a new Laravel project in 2026 and the question is no longer "which package do I use to talk to OpenAI." It's a different one: which of the official Laravel things do I install — and what happened to that wrapper you wrote in app/Services/OpenAiService.php and never looked at again.
The Laravel AI SDK is the first-party package that answers the first half of that question: it's the AI SDK that now ships with the framework. Its job is to put AI inside your app, in the flow your end user actually touches. One composer require laravel/ai, one agent class, and you have text generation, tool calling, structured output, streaming, embeddings and vector search talking to OpenAI, Anthropic, Gemini, Groq or Ollama through the same API.
In this guide we go from zero to a running agent: what the SDK is, what it isn't, installation, a first agent with a tool, structured output, embeddings with pgvector, testing with fakes, and where it still hurts. Code that runs, not pseudo-code.
TL;DR
- What it is: Laravel's official AI SDK (
laravel/ai), with a unified API across multiple providers and the concept of an agent as a PHP class. - Stack/Models: PHP 8.3+, Laravel 12 or 13, OpenAI, Anthropic, Gemini, Groq, xAI, ElevenLabs, Cohere, Ollama and any OpenAI-compatible endpoint.
- Cost/Access: MIT package, free. You pay for the tokens of whichever provider you pick.
- Useful link: official AI SDK documentation and the laravel/ai repository.
The context: why the Laravel AI SDK matters
Before February 2026, putting artificial intelligence into a Laravel project was an architecture decision each team made on its own. Either you used Prism, or you wrote your own HTTP client, or you called the OpenAI API straight from the controller and prayed. Every project solved rate limiting, retries, streaming and tool schemas its own way. The result is what anyone who has audited a PHP project with AI has already seen: three different ways of calling an LLM in the same repository.
The official SDK was announced on February 5, 2026 and went stable alongside Laravel 13. The motivation, in Taylor Otwell's own words:
"It's such an important part of the dev workflow at this point. It felt like we needed some sort of first-party opinion on interacting with AI providers. Just like we have opinions on sending email or queuing jobs."
That's exactly it. Laravel has always had an opinion on email, queues, cache and storage. Now it has an opinion on LLMs. And a framework opinion, in Laravel, means three things: a folder convention, an Artisan command to generate the boilerplate, and a fake to test with.
The numbers say it stuck. The package is at v0.10.3, with more than 5.5 million downloads on Packagist and an MIT license. It's the difference between using AI and building with AI, and it's that second kind of work — architecture, evaluation, cost, context — that we break down live every week in the Clã Beer and Code, with code running on screen. It's paid, it's a subscription, and it's the environment this post describes in practice.
Prerequisites
Checklist before you run a single line:
- [ ] PHP 8.3 or higher (the package's
composer.jsonrequires^8.3). - [ ] Laravel 12.x or 13.x — the
illuminate/*components are pinned to^12.0|^13.0. - [ ] An API key for at least one provider (
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,GROQ_API_KEY). - [ ] PostgreSQL with the
pgvectorextension, if you're going to use vector search. For everything else, any database works.
If you've never touched embeddings and pgvector, read the RAG guide for backend devs first — the vector search part of this post assumes you already have the concepts down.
How to use AI in Laravel: from composer require to your first agent
Step 1: install and publish
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
vendor:publish creates config/ai.php and two migrations: agent_conversations and agent_conversation_messages. That's where the SDK stores conversation history when you turn memory on. If your use case is one-shot (summarize text, classify, extract data), you never touch those tables.
In .env, the provider key:
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
Step 2: the shortest possible prompt
Before the class, you can use the helper function for a quick test in Tinker:
use function Laravel\Ai\agent;
$response = agent(
instructions: 'Você é um assistente técnico. Responda em português, direto.',
)->prompt('Explique o que é idempotência em uma API REST.');
echo (string) $response;
Notice the (string) $response. The response object is Stringable, but it carries more: ->text, ->usage (tokens spent) and the generation events. Logging usage from day one is what saves you from the surprise on the invoice at the end of the month.
Step 3: the agent as a class (this is where the value lives)
The helper is for playing around. In production, an agent is a class:
php artisan make:agent SuporteTecnico
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
use Stringable;
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-5')]
#[MaxSteps(8)]
class SuporteTecnico implements Agent, HasTools
{
use Promptable;
public function __construct(public User $user) {}
public function instructions(): Stringable|string
{
return <<<TXT
Você é o suporte técnico da loja. Responda com base nos pedidos
do cliente. Se não achar o dado, diga que não achou.
Nunca invente número de pedido.
TXT;
}
public function tools(): iterable
{
return [
new BuscarPedido($this->user),
];
}
}
Three things here that are worth the price of admission:
- PHP attributes replace scattered config.
#[Provider],#[Model],#[MaxSteps],#[Temperature],#[Timeout]. Switching from Anthropic to OpenAI means editing one line, not refactoring a service. #[UseCheapestModel]and#[UseSmartestModel]exist. You declare the cost intent instead of hardcoding the model name, and the SDK resolves it. An agent that only classifies intent doesn't need the expensive model.- Plain dependency injection. The agent is a PHP class with a constructor.
SuporteTecnico::make(user: $user)resolves through the container.
Calling it is one line:
$resposta = (new SuporteTecnico($user))->prompt('Cadê meu pedido 8812?');
Step 4: tools, which is where the agent becomes useful
php artisan make:tool BuscarPedido
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class BuscarPedido implements Tool
{
public function __construct(protected User $user) {}
public function description(): Stringable|string
{
return 'Busca um pedido do cliente autenticado pelo número.';
}
public function schema(JsonSchema $schema): array
{
return [
'numero' => $schema->integer()->required(),
];
}
public function handle(Request $request): Stringable|string
{
$pedido = $this->user->orders()->find($request['numero']);
return $pedido
? "Pedido {$pedido->id}: status {$pedido->status}, entrega prevista {$pedido->eta}."
: 'Pedido não encontrado para este cliente.';
}
}
The security detail almost every tutorial skips: the tool receives $user in the constructor and filters through $this->user->orders(). The model asks for "order 8812" and the Eloquent scope guarantees it only sees what belongs to that customer. A tool doesn't validate permissions on its own. If you write Order::find($request['numero']), you've just built a conversational IDOR.
The SDK also ships ready-made tools: SimilaritySearch (vector search against your database), FileStorage (disk reads, with a readOnly variant), and the provider tools WebSearch, WebFetch and FileSearch.
use Laravel\Ai\Providers\Tools\WebSearch;
(new WebSearch)->max(5)->allow(['laravel.com']);
Step 5: structured output, so you can stop regexing LLM responses
If the result is going into the database, you don't want free text. You want JSON with a contract:
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\HasStructuredOutput;
class ClassificadorDeTicket implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): Stringable|string
{
return 'Classifique o ticket de suporte recebido.';
}
public function schema(JsonSchema $schema): array
{
return [
'categoria' => $schema->string()
->enum(['entrega', 'pagamento', 'produto', 'outro'])
->required(),
'urgencia' => $schema->integer()->min(1)->max(5)->required(),
'resumo' => $schema->string()->required(),
];
}
}
$r = (new ClassificadorDeTicket)->prompt($ticket->body);
Ticket::find($ticket->id)->update([
'categoria' => $r['categoria'],
'urgencia' => $r['urgencia'],
]);
JsonSchema is the same contract Laravel uses elsewhere in the framework, and enum() locks down the vocabulary. Without it you'll get "Entrega", "entregas" and "problema de entrega" in the same column.
Step 6: streaming, queues and embeddings
Streaming is just returning the agent straight from the route:
Route::get('/suporte', fn () => (new SuporteTecnico(auth()->user()))
->stream('Cadê meu pedido 8812?'));
There's support for the Vercel AI SDK protocol via ->usingVercelDataProtocol(), which saves you the translation layer if your frontend already consumes that format.
For slow tasks, swap prompt() for queue() and the generation becomes a job:
(new RelatorioMensal)
->queue('Analise os dados de julho.')
->then(fn ($response) => $report->update(['body' => $response->text]))
->catch(fn ($e) => Log::error('Falhou', ['e' => $e->getMessage()]));
And embeddings got Laravel-style syntactic sugar:
use Illuminate\Support\Str;
use Laravel\Ai\Embeddings;
$vetor = Str::of('Napa Valley tem ótimos vinhos.')->toEmbeddings();
$resposta = Embeddings::for($textos)
->dimensions(1536)
->cache(seconds: 3600)
->generate();
In the migration, vector() is now a native column type:
Schema::ensureVectorExtensionExists();
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->vector('embedding', dimensions: 1536)->index();
});
And the similarity query is plain Eloquent:
$docs = Document::query()
->whereVectorSimilarTo('embedding', 'melhores vinícolas de Napa', minSimilarity: 0.4)
->limit(10)
->get();
Notice: you passed the string. The SDK generates the query embedding and does the comparison. That's the kind of thing that used to cost you about 40 lines of raw SQL with <=> and array casts.
Step 7: testing without burning tokens
The real reason the SDK exists as a layer on top of an HTTP client is the fakes:
use Laravel\Ai\Fakes\AgentFake;
it('classifica ticket de entrega', function () {
AgentFake::expect(ClassificadorDeTicket::class)
->toReceivePrompt('Meu pedido não chegou')
->andRespond(['categoria' => 'entrega', 'urgencia' => 4, 'resumo' => '...']);
$ticket = Ticket::factory()->create(['body' => 'Meu pedido não chegou']);
expect(classify($ticket)->categoria)->toBe('entrega');
});
There are fakes for agents, images, audio, transcription and embeddings. Deterministic suite, zero cost, CI with no API key. If your homegrown wrapper doesn't have this, here's where the migration pays for itself.
A tutorial shows you the way — in the Clã you build alongside us. A live class every week, real AI Engineering projects, next to people already in production.
Join the ClãAI SDK, Boost or MCP: which is which
The most common confusion for anyone just arriving. There are three official packages and none of them replaces the others:
| Package | Who uses it | What it does |
|---|---|---|
| Laravel AI SDK | your application | adds AI features that your end user consumes |
| Laravel Boost | you, the dev | gives the coding agent context and up-to-date docs so it writes idiomatic Laravel |
| Laravel MCP | external AI clients | exposes your app for ChatGPT, Claude or Cursor to call |
The practical rule of thumb: if the feature shows up on your customer's screen, it's the AI SDK. If it only improves what Claude Code generates in your editor, it's Boost. If an outside agent needs to execute an action in your system, it's MCP.
Limitations and things to watch out for
Where you're going to get burned:
It's still 0.x. The current version is v0.10.3. There's no stable semver guarantee on a zero major, and the package is shipping releases frequently. Pin the version in composer.json and read the CHANGELOG before bumping a minor in production.
Context bloat from too many tools. The warning comes from Taylor himself: "You shouldn't have like 50, 60, 70 tools probably exposed to the LLM because you get what's called context bloat. You also have to send all of the tool definitions, what they do, and you have to send that on every message." Every tool's definition goes out with every message. An agent with 40 tools burns context before the user has typed anything. That's why tools are declared per agent and not globally: scoping is a feature, not a limitation.
Migrating from Prism isn't automatic. The relationship between the two, in Taylor's analogy, is that of Query Builder and Eloquent: the AI SDK is an abstraction layer on top of the same kind of problem, with agent classes, attributes and testing. If you already run Prism in production, look at the real diff of a migration from Prism to the official SDK before deciding.
Prompt injection is still your problem. The SDK doesn't sanitize anything. If the tool writes to the database, deletes files or fires off email, treat it like a public endpoint: validate arguments, scope by user, and consider the human approval API (Approvable + Decisions), which lets you hold the tool's execution until someone approves it.
Cost is invisible until it isn't. $response->usage exists. Use it. An agent with MaxSteps(10) can make ten round trips to the model in a single prompt() call, and that bill doesn't show up anywhere if you don't log it.
Quick FAQ
Do I need Laravel 13?
No. The package accepts illuminate/* at ^12.0|^13.0, so Laravel 12 works. The hard requirement is PHP 8.3 or higher.
Can I use a local model, without sending data out?
Yes. The SDK supports Ollama for local inference, and you can point at any OpenAI-compatible endpoint by overriding the base URL in config/ai.php. OpenRouter works too, if the idea is a single key for several providers.
Does it replace Prism? For a new project, yes — that's the official recommendation. For an existing project, it comes down to migration cost. Prism keeps working and won't blow up because you didn't migrate.
How do I keep the user from waiting 40 seconds for a response?
Two ways out: stream() to return token by token over SSE, or queue() to push it onto the queue and notify later via broadcast. If you're using queues for this, it's worth designing the SLA properly — we covered that in the post on queues in Laravel in 2026.
What to do with this
The Laravel AI SDK doesn't teach you AI engineering. It gets the boring part out of the way: HTTP client, retries, tool schemas, JSON parsing, test mocks. What's left is the work that was always hard — deciding what context the model gets, how you measure whether the answer is any good, how much it costs per request, and what happens when the provider goes down at three in the morning.
Start small. One agent, one tool, structured output, a test with a fake. Run it in production on a low-risk feature (summarization, classification, extraction) and watch usage for a week before you hand a write tool to anything.
The ecosystem's next step is already sketched out: native human-in-the-loop, sub-agents as tools and agent middleware. In other words, Laravel is treating agents with the same rigor it treats HTTP requests — pipeline, middleware, tests. That's a lot more interesting than any chatbot demo.
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.
There is no shortage of content. What is missing is someone to untangle it: what matters now is how to implement it the right way. In the Clã you get that live, every week, with people who have already filtered out the noise.
Join the Clã