AI features become easier to reuse when they live behind a stable HTTP contract. A Laravel endpoint can serve a web app, mobile client, CLI tool, or another backend without duplicating provider logic.
This guide shows how to build REST API with PHP using Laravel and the Laravel AI SDK. We will create an agent-backed chat endpoint with:
- OpenAI and Anthropic providers
- JSON responses
- Tool calling
- Server-Sent Events (SSE)
- Provider failover
- Rate limiting
- Queueing for long-running requests
-
Agent::fake()tests
Laravel gives you the routing, validation, queues, middleware, and testing tools. The AI SDK adds a consistent interface for providers, agents, tools, and streaming.
Install the Laravel AI SDK
Install the package with Composer:
composer require laravel/ai
Publish the SDK configuration and conversation migrations:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
The published config/ai.php file supports OpenAI, Anthropic, and several other providers. Add the credentials your application needs to .env:
OPENAI_API_KEY=your-openai-key
ANTHROPIC_API_KEY=your-anthropic-key
The SDK stores conversation data in agent_conversations and agent_conversation_messages. You only need those tables when your agent persists conversations.
Review the complete Laravel AI SDK documentation and the package source on GitHub.
Create an agent with PHP attributes
Agents keep instructions, tools, provider settings, and model configuration in one PHP class. Generate one with Artisan:
php artisan make:agent ChatAgent
Create app/Ai/Agents/ChatAgent.php:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
#[Provider(Lab::OpenAI)]
#[Model('gpt-4.1-mini')]
#[Temperature(0.3)]
#[MaxTokens(1200)]
#[MaxSteps(5)]
class ChatAgent implements Agent, HasTools
{
use Promptable;
public function instructions(): string
{
return <<<'PROMPT'
You are a concise support assistant.
Answer using the application's available tools when appropriate.
Do not invent order information.
If a tool does not provide an answer, say so clearly.
PROMPT;
}
public function tools(): iterable
{
return [
new GetOrderStatus,
];
}
}
The attributes make the defaults explicit:
-
Providerselects the primary provider. -
Modelselects the default model. -
Temperaturecontrols response variability. -
MaxTokenslimits generated output. -
MaxStepslimits tool-calling rounds.
A low temperature works well for support and data-oriented responses. Use a higher value for creative generation.

Add a tool for controlled actions
Tool calling lets the model request a specific server-side capability. Your application still owns the implementation and authorization rules.
Create the tool:
php artisan make:tool GetOrderStatus
Then define app/Ai/Tools/GetOrderStatus.php:
<?php
namespace App\Ai\Tools;
use App\Models\Order;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class GetOrderStatus implements Tool
{
public function description(): Stringable|string
{
return 'Look up the current status of an order by its public order number.';
}
public function schema(JsonSchema $schema): array
{
return [
'order_number' => $schema->string()->required(),
];
}
public function handle(Request $request): Stringable|string
{
$order = Order::query()
->where('number', $request['order_number'])
->first();
if (! $order) {
return 'No order was found with that number.';
}
return sprintf(
'Order %s is currently %s.',
$order->number,
$order->status,
);
}
}
Keep tools narrow and permission-aware. Do not expose a general database query tool. Validate tool arguments, scope records to the authenticated user, and require approval for destructive actions.
The SDK can also expose provider-managed tools such as WebSearch, or tools from Laravel MCP servers. See the tools section of the AI SDK documentation.
Build the JSON chat endpoint
Create a controller:
php artisan make:controller AiChatController
Add the endpoint logic:
<?php
namespace App\Http\Controllers;
use App\Ai\Agents\ChatAgent;
use Illuminate\Http\Request;
use Laravel\Ai\Enums\Lab;
class AiChatController extends Controller
{
public function __invoke(Request $request)
{
$validated = $request->validate([
'message' => ['required', 'string', 'max:8000'],
]);
$response = (new ChatAgent)->prompt(
$validated['message'],
provider: [
Lab::OpenAI->value => 'gpt-4.1-mini',
Lab::Anthropic->value => 'claude-sonnet-5',
],
);
return response()->json([
'data' => [
'message' => (string) $response,
'conversation_id' => $response->conversationId,
],
]);
}
}
Register the route in routes/api.php:
<?php
use App\Http\Controllers\AiChatController;
use Illuminate\Support\Facades\Route;
Route::post('/ai/chat', AiChatController::class)
->middleware(['auth:sanctum', 'throttle:ai']);
A request now looks like this:
curl -X POST https://example.com/api/ai/chat \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Where is order #10042?"}'
The endpoint returns a predictable JSON structure:
{
"data": {
"message": "Order #10042 is currently processing.",
"conversation_id": "conversation-id"
}
}
This separation matters. Clients only know about /api/ai/chat. They do not need to know which provider handled the request or whether the agent called a tool.
Stream responses with Server-Sent Events
Full responses work well for short prompts. Longer responses feel faster when the client receives chunks as they are generated.
Add a streaming method to the controller:
public function stream(Request $request)
{
$validated = $request->validate([
'message' => ['required', 'string', 'max:8000'],
]);
return (new ChatAgent)->stream(
$validated['message'],
provider: [
Lab::OpenAI->value => 'gpt-4.1-mini',
Lab::Anthropic->value => 'claude-sonnet-5',
],
);
}
Register a separate route:
Route::post('/ai/chat/stream', [AiChatController::class, 'stream'])
->middleware(['auth:sanctum', 'throttle:ai']);
The AI SDK returns a streamable response that Laravel can send directly as SSE. The client receives events while the model generates them.
A browser client can consume the stream with fetch and a readable stream:
const response = await fetch('/api/ai/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ message: 'Summarize our latest support policy.' }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(decoder.decode(value, { stream: true }));
}
The SDK also supports the Vercel AI SDK stream protocol:
return (new ChatAgent)
->stream($validated['message'])
->usingVercelDataProtocol();

Configure provider failover
AI providers can rate-limit requests or experience temporary outages. The SDK supports provider failover without custom retry logic.
The controller above passes a provider-to-model map:
provider: [
Lab::OpenAI->value => 'gpt-4.1-mini',
Lab::Anthropic->value => 'claude-sonnet-5',
],
The SDK tries OpenAI first. It moves to Anthropic when a failover-eligible exception occurs, such as a rate limit, provider overload, or insufficient credits.
Failover does not hide ordinary application errors. Invalid requests and validation failures should still fail immediately.
You can observe provider changes with the AgentFailedOver event. Log the event with a request identifier, agent name, provider, and model. Avoid logging full prompts when they may contain personal or sensitive data.
Add rate limiting
AI endpoints need their own limits because each request can create provider costs. Define a named limiter in AppServiceProvider or a dedicated service provider:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('ai', function (Request $request) {
return Limit::perMinute(20)->by(
$request->user()?->id ?: $request->ip()
);
});
}
The route already uses throttle:ai. You can adjust the limit by user plan, tenant, or endpoint.
Read Laravel’s middleware documentation for more options. In production, use a shared cache store so limits work across multiple application servers.
Queue long-running work
Do not hold an HTTP request open for document analysis, batch processing, or multi-step agents. Queue those operations instead.
The AI SDK provides a queue method:
use Illuminate\Http\Request;
use Laravel\Ai\Responses\AgentResponse;
use Throwable;
public function queue(Request $request)
{
$validated = $request->validate([
'message' => ['required', 'string', 'max:20000'],
]);
(new ChatAgent)
->queue($validated['message'])
->then(function (AgentResponse $response) {
// Persist the response or dispatch a completion event.
})
->catch(function (Throwable $exception) {
report($exception);
});
return response()->json([
'status' => 'queued',
], 202);
}
For a production API, persist an ai_requests record before dispatching the work. Return its public identifier to the client:
{
"data": {
"id": "ai_request_123",
"status": "queued"
}
}
Add a status endpoint so clients can poll safely:
Route::get('/ai/requests/{aiRequest}', AiRequestStatusController::class);
Use Laravel Horizon when your application needs queue visibility and worker management. Store provider responses, usage, failures, and completion timestamps with the request record.

Test the endpoint without provider calls
The AI SDK includes fakes for agents. This keeps feature tests fast and prevents accidental provider charges.
<?php
namespace Tests\Feature;
use App\Ai\Agents\ChatAgent;
use Tests\TestCase;
class AiChatTest extends TestCase
{
public function test_chat_returns_a_json_response(): void
{
ChatAgent::fake(['Order #10042 is processing.']);
$response = $this->postJson('/api/ai/chat', [
'message' => 'Where is order #10042?',
]);
$response
->assertOk()
->assertJsonPath(
'data.message',
'Order #10042 is processing.'
);
ChatAgent::assertPrompted('Where is order #10042?');
}
}
You can also provide multiple fake responses or a closure. Use preventStrayPrompts() when you want unexpected model calls to fail the test.
Run the test suite with:
php artisan test
Laravel’s testing and mocking documentation covers the broader testing workflow.
Production checklist
Before shipping the endpoint, confirm that you have:
- Authentication and authorization in place.
- Request validation and maximum input sizes.
- Per-user or per-tenant rate limits.
- Provider failover for transient failures.
- Timeouts and queue handling for long jobs.
- Structured logs without sensitive prompt content.
- Request IDs for tracing.
- Tests using
ChatAgent::fake(). - Tool argument validation and authorization.
- A versioned API contract.
Laravel gives PHP developers a practical path from a single agent class to a production API. The same application can handle HTTP requests, SSE streams, queues, provider failover, and tests with the framework conventions you already use.