The Laravel AI SDK (laravel/ai) is Laravel’s first-party, provider-agnostic way to talk to OpenAI, Anthropic, Gemini, and a dozen other AI providers without hand-rolling HTTP clients or gluing together community packages. This guide covers installation, configuration, agent fundamentals, model and provider selection, and the failover patterns you need before anything touches production.
It’s worth being explicit about scope up front, because there are two very different articles you could be reading right now. If you want a custom, hand-built provider-agnostic abstraction layered on top of raw provider clients, that’s a different problem with different trade-offs, and our production-ready architecture guide comparing OpenAI, Gemini, and Claude covers it in depth. This article is about the packaged, first-party approach: installing laravel/ai, configuring it correctly, and using it the way it ships. If you’re deciding between the two, stick around, since the closing section lays out that decision explicitly. For everyone else, this is the on-ramp for the AI Integration module.
What the Laravel AI SDK Actually Provides
The SDK’s core abstraction is the Agent: a dedicated PHP class that encapsulates instructions, conversation context, tools, and an output schema for a given AI interaction. You configure an agent once and prompt it repeatedly, and the SDK handles provider dispatch, request formatting, and response normalization underneath.
Provider coverage is broad, but it isn’t uniform: not every provider implements every capability, and checking the matrix before you commit an agent to a provider saves a rewrite later.
| Feature | Supported providers |
|---|---|
| Text generation | OpenAI, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible |
| Images | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter |
| Audio (TTS) | OpenAI, ElevenLabs, Gemini |
| Transcription (STT) | OpenAI, ElevenLabs, Mistral, Gemini |
| Embeddings | OpenAI, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter, OpenAI-compatible |
| Reranking | Cohere, Jina, VoyageAI |
Committing an agent to Anthropic and then discovering you also need that agent to generate images is a real scenario this table exists to prevent; Anthropic isn’t in the image-generation row, so that responsibility belongs to a different provider or a different agent entirely.
The important architectural point is that provider identity in the SDK isn’t a string you pass around and hope you spelled correctly. It’s the Laravel\Ai\Enums\Lab enum, referenced consistently through configuration, agent attributes, and runtime overrides. That single design decision is most of what makes provider-swapping in this SDK actually safe: the compiler catches a typo that a string-based config would silently swallow until a failed API call at 2am.
[Architect’s Note] If your application only ever needs one provider and you have no plausible reason to swap, you don’t strictly need the abstraction the SDK provides. But providers deprecate models on a schedule you don’t control, and vendor outages happen. Building on the abstraction from day one costs you almost nothing and buys you a failover path later without a rewrite.
Two things distinguish this from a driver you’d write yourself. First, testing is a first-class concern rather than an afterthought: every major surface, agents, images, audio, transcription, embeddings, reranking, files, and vector stores, ships with a corresponding fake() method and assertion helpers, so you can exercise the code paths around an AI call without an API key or network access in CI. Second, the SDK dispatches a consistent set of events, PromptingAgent, AgentPrompted, ToolInvoked, GeneratingEmbeddings, and others, which gives you a uniform place to hook in logging, cost tracking, or auditing regardless of which provider handled a given request. Community packages built around a single provider’s SDK rarely give you either of these for free; you build them yourself or go without.
It’s also worth naming what the SDK is not. It isn’t a replacement for understanding your provider’s actual model behavior, context window limits, or pricing structure, and it isn’t a guarantee that switching a config value from Anthropic to Gemini produces equivalent output quality for your specific prompts. The abstraction covers request mechanics, not model capability. Treat provider-swapping as an operational safety net and a convenience for testing, not as a substitute for evaluating whether a given provider is actually right for a given task.
Request Lifecycle: From Application Code to Provider API
Model selection and provider swapping happen entirely inside the resolution layer. Application code never changes, whether the request lands on config/ai.php‘s default, an agent’s Provider/Model attributes, or an explicit runtime override, and the amber path shows where failover reroutes a request without either side of the diagram knowing it happened.
Installing and Configuring the SDK
Installation is a three-step Composer and Artisan sequence, and skipping the third step is a real failure mode worth calling out explicitly.
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
The migration step creates two tables, agent_conversations and agent_conversation_messages, that back the SDK’s conversation persistence. If you skip this step because your first agent doesn’t obviously need conversation memory, you’ll hit a confusing failure the moment you add the RemembersConversations trait to a later agent.
[Production Pitfall] The migration is easy to treat as optional boilerplate during a quick proof of concept, then forget entirely once the feature reaches a staging or production environment with its own migration pipeline. Add it to your deployment checklist explicitly rather than assuming it travels with the package install.
Provider credentials live in config/ai.php or as environment variables:
ANTHROPIC_API_KEY= OPENAI_API_KEY= GEMINI_API_KEY= GROQ_API_KEY=
Default models for text, image, audio, transcription, and embedding generation are also configured per provider in config/ai.php, which is where the SDK’s provider-agnostic promise actually starts to pay off: you change a config value, not application code, to shift which vendor handles a given capability.
Treat these credentials the way you’d treat any other production secret. They belong in your environment configuration or a secrets manager, never committed alongside config/ai.php, and if your deployment pipeline already has a pattern for rotating API keys without a redeploy, apply it here too. AI provider keys tend to have generous default rate limits and correspondingly expensive misuse if leaked, which makes them a worse secret to expose than most.
[Edge Case Alert] If you’re configuring an
openai-compatibleprovider for embeddings specifically, the SDK has no way to discover the endpoint’s models on its own, since arbitrary endpoints don’t expose a models list the SDK understands. You must set a default embeddings model explicitly under that provider’smodels.embeddings.defaultkey, and if you need a fixed vector dimension for downstream storage, setdimensionstoo. Omittingdimensionssends the request without one and lets the model’s native dimension apply, which is fine until you’re storing vectors in a fixed-width database column that assumed otherwise.
Routing Through a Gateway or Proxy
If your infrastructure routes AI traffic through a proxy for centralized key management or rate limiting, such as LiteLLM or an Azure OpenAI Gateway, you can override the base URL per provider:
'providers' => [
'openai' => [
'driver' => 'openai',
'key' => env('OPENAI_API_KEY'),
'url' => env('OPENAI_URL'),
],
'anthropic' => [
'driver' => 'anthropic',
'key' => env('ANTHROPIC_API_KEY'),
'url' => env('ANTHROPIC_BASE_URL'),
],
],
Custom base URLs are supported for OpenAI, Anthropic, Gemini, Groq, Cohere, DeepSeek, xAI, and OpenRouter. For local or self-hosted inference, such as LM Studio or vLLM, an openai-compatible driver accepts an arbitrary url and optional bearer-token key, which is the cleanest path if part of your stack runs open-weight models on your own infrastructure alongside hosted providers.
Building Your First Agent
Agents are generated with an Artisan command and live in app/Ai/Agents:
php artisan make:agent SupportTriage
A minimal agent implements the Agent interface, uses the Promptable trait, and defines its instructions:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
use Stringable;
class SupportTriage implements Agent
{
use Promptable;
public function instructions(): Stringable|string
{
return 'You triage incoming support tickets by urgency and category.';
}
}
Prompting it is a single method call:
$response = (new SupportTriage)->prompt('Customer says checkout is throwing a 500 error.');
return (string) $response;
Every response also exposes the raw HTTP response from the underlying provider via a raw property, which is where you’ll find rate-limit headers and request IDs if you need them for logging or debugging. That property is null for streamed responses, for the Bedrock provider specifically, and for faked responses in tests.
If you need to prompt a model without the ceremony of a dedicated class, anonymous agents cover that case:
use function Laravel\Ai\agent;
$response = agent(
instructions: 'You are an expert at software development.',
)->prompt('Explain the difference between a job and a queued listener.');
For anything past a one-off prompt, dedicated agent classes are the better default. They’re independently testable, and they’re where structured output, tools, middleware, and conversation memory attach. This article deliberately stays at the agent-fundamentals level; if your use case needs the SDK’s tool-calling and orchestration layer, building a production Laravel AI agent with orchestration and state management picks up exactly where this leaves off. If you need enforced JSON output shape rather than free text, structured outputs with JSON schema validation is the dedicated reference.
Keeping Agents Testable From the Start
Because agents are ordinary PHP classes, faking them in tests doesn’t require mocking an HTTP client or stubbing a facade. Calling SupportTriage::fake() before your test body intercepts every prompt to that agent and lets you return a fixed response, a sequence of responses, or a closure that inspects the incoming prompt:
use App\Ai\Agents\SupportTriage;
SupportTriage::fake(['Urgent: payment processing failure.']);
$response = (new SupportTriage)->prompt('Checkout is returning a 500.');
SupportTriage::assertPrompted(fn ($prompt) => $prompt->contains('Checkout'));
This matters more than it looks like it should for a “getting started” concern. Teams that treat AI calls as inherently untestable end up with integration tests that hit a real provider, which is slow, costs money per test run, and is flaky in exactly the way CI pipelines punish hardest. Building agents as classes from the start, rather than inline closures scattered through controllers, is what makes this pattern available to you later without a refactor.
[Efficiency Gain]
preventStrayPrompts(), chained afterfake(), throws if any agent invocation in a test doesn’t have a corresponding fake response defined. It’s a small addition that catches the specific bug where a refactor accidentally routes a call through an agent your test forgot to fake, which otherwise fails silently or, worse, quietly hits a real provider during CI.
Selecting Providers and Models
The SDK gives you three separate mechanisms for controlling which provider and model handle a given request, and conflating them is a common source of confusion when a team has more than one developer touching agent code.
The first is the config/ai.php default: whatever model you’ve set as the default for a provider is used when nothing more specific is supplied. The second is a runtime override, passed directly to prompt():
$response = (new SupportTriage)->prompt(
'Customer says checkout is throwing a 500 error.',
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
);
The third is a set of PHP attributes applied to the agent class itself, which is the right choice when a specific agent should consistently use a specific provider and model regardless of caller:
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-5')]
class SupportTriage implements Agent
{
use Promptable;
// ...
}
| Mechanism | Scope | Best for |
|---|---|---|
config/ai.php default | Application-wide fallback | A sensible baseline when no agent specifies otherwise |
Runtime provider/model args | Single call | Caller-driven overrides, A/B testing, user-selected models |
| Class attributes | Per-agent | Agents with a fixed, intentional provider/model choice |
Runtime arguments passed to prompt() explicitly override an agent’s configured defaults, which is the behavior you want: a class attribute sets the agent’s normal operating mode, and a caller can still override it deliberately for a specific request, such as letting a user pick a model in a settings screen without needing a separate agent class per option.
Provider and model aren’t the only things worth setting on the class itself. MaxTokens, Temperature, TopP, Timeout, and MaxSteps (the ceiling on tool-calling iterations for agents that use tools) are all available as attributes, and setting them explicitly on agents that matter is part of what “production-ready” means in practice, rather than relying on whatever the provider’s own default happens to be:
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-5')]
#[MaxTokens(2048)]
#[Temperature(0.3)]
#[Timeout(45)]
class SupportTriage implements Agent
{
use Promptable;
// ...
}
A triage agent has no business running at a high temperature or waiting sixty seconds for a response; setting these deliberately, rather than accepting silent provider defaults, is a small habit that prevents a specific category of production surprise where an agent’s behavior shifts because a provider changed its own default. The default HTTP timeout for agent requests is 60 seconds when you don’t set Timeout explicitly, which is a reasonable general default but worth shortening for latency-sensitive endpoints like a synchronous triage call on an incoming webhook, where a caller waiting a full minute for a response is its own kind of production problem.
It’s worth knowing that the Provider attribute itself accepts either a single provider or an ordered array, meaning you can define an agent’s entire failover chain declaratively on the class rather than passing it at every call site. That’s the better choice for an agent whose failover behavior should be consistent regardless of caller, reserving the runtime provider argument for genuinely per-call overrides rather than duplicating the same failover list everywhere the agent gets prompted.
Two additional attributes, UseCheapestModel and UseSmartestModel, let an agent pick a provider’s cheapest or most capable text model automatically rather than naming one explicitly.
[Word to the Wise]
UseCheapestModelandUseSmartestModelare convenient, but the underlying model they resolve to can change between SDK releases as providers ship new models. That’s fine for a summarization agent where “good enough and cheap” is the actual requirement, but it’s the wrong choice for anything where you need stable, predictable output behavior and cost. If a swapped-in model changes tone, latency, or pricing in a way that matters to your application, name the model explicitly with theModelattribute instead.
Failover and Production Error Handling
Every AI provider has outages, rate limits, and the occasional bad day, and the SDK’s failover mechanism exists specifically for that reality rather than for routine load balancing. You supply an ordered list of providers, and the SDK falls through the list when a request to the current one fails in a specific, recoverable way:
use Laravel\Ai\Enums\Lab;
$response = (new SupportTriage)->prompt(
'Customer says checkout is throwing a 500 error.',
provider: [Lab::OpenAI, Lab::Anthropic],
);
Each provider in that list uses its own default model unless you key the array explicitly, since Lab enum cases can’t be used directly as PHP array keys:
$response = (new SupportTriage)->prompt(
'Customer says checkout is throwing a 500 error.',
provider: [
Lab::OpenAI->value => 'gpt-5.5',
Lab::Anthropic->value => 'claude-sonnet-5',
],
);
Failover is intentionally narrow in scope. It triggers only on FailoverableException and its subtypes: RateLimitedException, ProviderOverloadedException, and InsufficientCreditsException. A validation error or a malformed request will not trigger failover, and it shouldn’t, since retrying a request that’s wrong on a second provider just gets you the same failure twice.
[Production Pitfall] Teams sometimes assume failover is a general-purpose retry mechanism and wrap it around agents without any additional error handling. It isn’t. Wrap agent calls in a try/catch that handles non-failoverable exceptions explicitly, log them with enough context to debug after the fact, and decide deliberately what your application does when every provider in the failover chain is genuinely unavailable:
use Laravel\Ai\Exceptions\FailoverableException;
use Throwable;
try {
$response = (new SupportTriage)->prompt(
$ticketBody,
provider: [Lab::OpenAI, Lab::Anthropic],
);
} catch (FailoverableException $e) {
// Every provider in the chain failed or was unavailable.
Log::error('AI triage failed across all configured providers.', ['exception' => $e]);
} catch (Throwable $e) {
// A non-recoverable error: bad request, validation, auth, etc.
Log::error('AI triage request failed.', ['exception' => $e]);
}
If you’re tracking token usage or enforcing rate limits at the application layer on top of what the SDK and provider already do, that’s a separate governance concern from failover, and token tracking and rate limiting middleware covers building that layer in.
Choosing Between the SDK and a Custom Provider-Agnostic Layer
The decision here comes down to how much control you need over the request lifecycle versus how much of that control you’re willing to build and maintain yourself.
The first-party SDK is the right default when your application needs multi-provider support, agent-style interactions, and standard production concerns like failover and conversation memory, without needing to intercept or transform requests in ways the SDK doesn’t already expose a hook for. It’s actively maintained by the Laravel team, ships with test fakes for every major feature, and gets provider updates without you tracking a community package’s release cadence separately from Laravel’s own.
A custom provider-agnostic layer earns its cost when you need request-level behavior the SDK’s attributes and middleware don’t cover, when you’re integrating a provider entirely outside the SDK’s supported list, or when your organization has compliance requirements around request/response handling that need to live below the SDK’s abstraction rather than beside it. The production-ready architecture guide linked above walks through building that layer when it’s warranted.
There’s a middle ground worth naming honestly: Prism PHP, the community package the first-party SDK was built to supersede, is still a reasonable choice if you’re already deep into it on an existing codebase and the migration cost outweighs the benefit, or if you need a specific capability Prism exposes that the first-party SDK hasn’t caught up to yet. New projects don’t have that constraint, and starting on the first-party SDK avoids taking on a dependency that Laravel’s own roadmap has effectively deprioritized.
The maintenance cost of each option compounds differently over time, which is the part that’s easy to underweight during an initial build. A custom layer you write today is a custom layer your team maintains indefinitely, including every provider API change, every new model naming convention, and every edge case in streaming or tool-calling that the SDK’s maintainers have already had to solve. The first-party SDK shifts that maintenance burden onto the Laravel core team, in exchange for less control over exactly how requests are shaped. For the overwhelming majority of Laravel applications integrating AI features, that trade favors the packaged SDK. The exceptions are real, but they’re exceptions, not the default case.
If you already know which single provider you’re committing to and want the deepest possible coverage of that vendor’s specifics inside the SDK, the vendor-specific guides go further than this article does by design: Laravel OpenAI integration, Laravel Claude API integration, and integrating Gemini into the Laravel AI SDK.
Summary
The Laravel AI SDK’s real value isn’t any single feature, it’s that provider identity, model selection, and failover are all handled through one consistent, typed interface instead of three different bespoke solutions per vendor. Installation is a three-command sequence, but the migration step matters more than it looks like it should, since it’s the difference between conversation memory working and failing silently later. Model and provider selection has three distinct mechanisms with different scopes, and picking the wrong one for a given agent is an easy, avoidable mistake. Failover is narrow by design, which is a feature, not a limitation: it protects you from your provider’s bad day without masking your own bugs.
Where this sits in the broader architecture is straightforward. Use this SDK as your default for provider-agnostic AI features in a Laravel application. Reach for a custom abstraction only when you have a specific, articulable reason the packaged approach doesn’t cover, and reach for the vendor-specific guides when you’ve already committed to a single provider and want to go deeper on what it specifically offers.
None of this is a one-time decision you make once and forget. Provider pricing shifts, model capabilities improve unevenly across vendors, and a failover chain that made sense six months ago might be protecting against an outage pattern that provider no longer has. Revisit the choices this guide walks through, particularly which provider is primary and which models are pinned on your highest-traffic agents, on a cadence that matches how quickly the providers you depend on actually change, not on a fixed annual schedule that assumes stability the AI provider market doesn’t currently offer.
If you’ve integrated the SDK in production, I’d be interested in where the packaged abstraction held up and where you ended up dropping down to provider-specific code anyway. That gap, more than any feature list, is usually the most useful thing to know before you start.
Frequently Asked Questions
Does the Laravel AI SDK replace Prism PHP?
They solve overlapping problems, but the Laravel AI SDK is the first-party, framework-maintained option and is built on different internals with its own conventions for agents, tools, and testing. If you’re starting a new integration today, the first-party SDK is the more defensible long-term choice specifically because it ships and versions alongside Laravel itself.
Can I use the SDK with a provider it doesn’t natively support?
Yes, through the openai-compatible driver, which works with any endpoint that implements OpenAI’s API shape, including self-hosted options like LM Studio or vLLM. You’ll need to configure a default model explicitly, since arbitrary endpoints have no models the SDK can discover on its own.
Do I need the agent_conversations migration if I’m not using conversation memory yet?
Run it anyway. Skipping it works fine until the moment you or a teammate adds the RemembersConversations trait to any agent, at which point it fails in a way that’s harder to diagnose than the five minutes it takes to run the migration up front.
How do I control which model each provider uses in a failover chain?
Pass an associative array to the provider argument, keyed by each Lab enum case’s value rather than the enum case itself, since enum cases can’t be used directly as PHP array keys. Each key maps to the model string for that specific provider.
Does failover protect against a bad prompt or malformed request?
No, and it isn’t meant to. Failover only triggers on FailoverableException and its subtypes, which cover rate limits, provider overload, and insufficient credits. A validation error or bad request fails immediately on the current provider rather than retrying against the next one in the chain, since the problem isn’t the provider.
A software architect with 15+ years of experience in the PHP and Laravel ecosystem. Dewald created Origin Main to provide the engineering rigour required to integrate AI into professional, high-concurrency production systems. He writes for developers who care less about "getting it to work" and more about "getting it to last".

