AI chat feels slow when users wait for a complete response before seeing anything. Streaming changes that experience by sending generated content as soon as it becomes available.
Laravel’s AI SDK gives you a consistent API for streaming responses from providers such as OpenAI and Anthropic. Laravel handles the server-side stream as Server-Sent Events (SSE), while the optional Vercel AI SDK protocol makes the same endpoint compatible with modern chat interfaces.
This tutorial builds a practical chat endpoint with:
- Laravel AI SDK installation and provider configuration
- A dedicated agent created with
make:agent - Persisted conversations with
RemembersConversations - User-scoped conversations using
forUserandcontinue - Automatic SSE responses from a Laravel route
- Post-stream processing with
then() - Vercel AI SDK UI compatibility
- Provider failover between OpenAI and Anthropic
Why stream AI responses?
A non-streaming request keeps the browser waiting until the model finishes generating the entire answer. That can mean several seconds of blank space, even when the model has already produced useful text.
Streaming reduces perceived latency. Users see the first words quickly, then watch the answer develop. They can begin reading before generation finishes, which is especially useful for longer explanations, code generation, and support conversations.
Streaming also gives your interface clearer states. The browser can show an active response, disable duplicate submissions, offer cancellation, and display errors without hiding the entire interaction behind a loading spinner.
SSE is a good fit for this pattern. It uses a standard HTTP response and sends a sequence of events from the server to the browser. Laravel’s AI SDK handles the SSE response formatting for you.
Install the Laravel AI SDK:
Start by installing the package with Composer:
composer require laravel/ai
Publish the SDK configuration and migration files:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
The migrations create the tables Laravel uses for persisted agent conversations and messages. Run them before adding RemembersConversations to an agent.
Laravel is a productive PHP web framework because common application concerns stay close to your code. The AI SDK follows that same approach. You configure providers once, then work with agents through familiar PHP classes.
Configure OpenAI and Anthropic:
Add your provider keys to .env:
OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
The published config/ai.php file contains the provider configuration. A minimal setup looks like this:
'providers' => [
'openai' => [
'driver' => 'openai',
'key' => env('OPENAI_API_KEY'),
],
'anthropic' => [
'driver' => 'anthropic',
'key' => env('ANTHROPIC_API_KEY'),
],
],
You can configure default text models in the same file. Use the AI SDK documentation for the current provider and model options.
Provider credentials should remain server-side. Your browser should call your Laravel endpoint, never the provider API directly.
Create a conversational agent:
Generate an agent with Artisan:
php artisan make:agent SupportAgent
Laravel places the generated class in app/Ai/Agents. Update it with the Agent and Conversational contracts, the Promptable trait, and RemembersConversations:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Promptable;
class SupportAgent implements Agent, Conversational
{
use Promptable, RemembersConversations;
public function instructions(): string
{
return <<<'INSTRUCTIONS'
You are a helpful customer support assistant.
Give concise, practical answers.
Ask one clarifying question when the user's request lacks important context.
Never invent account, order, or billing details.
INSTRUCTIONS;
}
}
The instructions() method defines the agent’s behavior. Keep those instructions stable and specific. User input should arrive through prompt(), rather than being embedded in the instructions.
RemembersConversations automatically stores and reloads user and assistant messages. Do not define your own messages() method when using this trait. A custom method would override the trait and prevent the persisted history from loading.

Scope conversations to users:
A new conversation can be associated with an authenticated user through forUser():
use App\Ai\Agents\SupportAgent;
$response = (new SupportAgent)
->forUser($user)
->prompt('I need help updating my billing details.');
$conversationId = $response->conversationId;
The response includes a conversationId. Store that identifier if your application exposes multiple conversations per user.
To continue a specific conversation, use continue():
$response = (new SupportAgent)
->continue($conversationId, as: $user)
->prompt('Can you explain the second step?');
You can also continue the user’s latest conversation:
$response = (new SupportAgent)
->continueLastConversation($user)
->prompt('Please pick up where we stopped.');
The continue() method does not authorize ownership automatically. Always check that the authenticated user can access the requested conversation before continuing it. Use a policy or a database query to enforce that boundary.
This approach keeps conversation state in your application. You do not need to send the complete message history from the browser on every request. That reduces client complexity and gives your server control over memory limits, authorization, and retention.
Stream responses as SSE:
The AI SDK’s stream() method returns a streamable agent response. Returning it directly from a route automatically sends an SSE response to the client:
use App\Ai\Agents\SupportAgent;
use Illuminate\Support\Facades\Route;
Route::get('/support', function () {
return (new SupportAgent)
->stream('Help the user with their support request.');
});
Laravel sets the response to text/event-stream and sends events as the provider generates content.
For a real chat endpoint, accept the message through a POST request. The following example supports both new and existing conversations:
<?php
use App\Ai\Agents\SupportAgent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
Route::post('/api/chat', function (Request $request) {
$validated = $request->validate([
'message' => ['required', 'string', 'max:10000'],
'conversation_id' => ['nullable', 'integer'],
]);
$user = $request->user();
if ($validated['conversation_id']) {
$conversation = $user->conversations()
->findOrFail($validated['conversation_id']);
Gate::authorize('view', $conversation);
$agent = (new SupportAgent)
->continue($conversation->id, as: $user);
} else {
$agent = (new SupportAgent)
->forUser($user);
}
return $agent->stream($validated['message']);
})->middleware('auth');
This is a standard way to build a REST API with PHP. Laravel validates the request, authorizes the conversation, invokes the agent, and returns a streaming HTTP response.

Run work after streaming finishes:
Use then() when you need to perform work after the complete response has reached the client. The callback receives a StreamedAgentResponse.
use Laravel\Ai\Responses\StreamedAgentResponse;
use Illuminate\Support\Facades\Log;
return $agent
->stream($validated['message'])
->then(function (StreamedAgentResponse $response) use ($user) {
Log::info('AI response completed', [
'user_id' => $user->id,
'text_length' => strlen($response->text),
'usage' => $response->usage,
]);
});
This callback is useful for logging, analytics, usage tracking, or triggering follow-up work. Do not use it for data the user must see during generation. The callback runs after the stream completes.
Your frontend can consume the response with a standard streaming fetch() request. If you use a framework-specific chat client, select the transport that matches the response protocol.
Add Vercel AI SDK compatibility:
The Vercel AI SDK defines a data stream protocol for chat interfaces such as useChat. It uses SSE formatting with structured parts for message starts, text deltas, tool calls, errors, and completion events.
Laravel can format the agent stream for this protocol:
use Laravel\Ai\Enums\Lab;
return $agent
->stream($validated['message'])
->usingVercelDataProtocol();
A complete provider-compatible route might look like this:
use App\Ai\Agents\SupportAgent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laravel\Ai\Enums\Lab;
Route::post('/api/chat', function (Request $request) {
$validated = $request->validate([
'message' => ['required', 'string', 'max:10000'],
'conversation_id' => ['nullable', 'integer'],
]);
$user = $request->user();
$agent = isset($validated['conversation_id'])
? (new SupportAgent)->continue(
$validated['conversation_id'],
as: $user,
)
: (new SupportAgent)->forUser($user);
return $agent
->stream(
$validated['message'],
provider: [Lab::OpenAI, Lab::Anthropic],
)
->usingVercelDataProtocol();
})->middleware('auth');
The Vercel protocol adds the x-vercel-ai-ui-message-stream: v1 header. Text arrives as structured text-delta events, followed by completion events. That lets Vercel AI SDK UI components render the response without a custom SSE parser.
Read the Vercel AI SDK stream protocol documentation when your frontend needs tool calls, reasoning parts, custom data, or other structured events.
Add provider failover:
Provider outages and rate limits are normal operational concerns. The AI SDK accepts an ordered provider array:
use Laravel\Ai\Enums\Lab;
$response = (new SupportAgent)
->forUser($user)
->prompt(
'Summarize the customer issue.',
provider: [Lab::OpenAI, Lab::Anthropic],
);
The same provider option can be used with a streamed request, as shown in the route above. Laravel tries OpenAI first, then Anthropic when a failoverable exception occurs.
Failover handles conditions such as rate limits, provider overload, unavailability, and insufficient credits. It does not hide ordinary application errors, invalid requests, or malformed prompts.
Keep provider models and capabilities in mind when designing a fallback chain. If your agent depends on a provider-specific tool, structured output feature, or context limit, confirm that the fallback supports the same behavior.
Production considerations:
Streaming works best when every layer preserves the response stream. Check your reverse proxy, web server, platform timeout, and application middleware for buffering or short request limits.
Log provider failures and stream completion events. Laravel’s ecosystem of PHP developer tools also includes Nightwatch for monitoring application errors, performance, and logs.
Start with a small agent and a focused instruction set. Then add tools, attachments, structured output, or retrieval as the chat experience becomes more useful.
Streaming does not make the model generate faster. It makes waiting visible, useful, and easier to manage. With Laravel AI SDK, SSE and Vercel AI SDK compatibility fit into the same agent workflow, so you can focus on the conversation instead of hand-building the transport layer.
If you build this pattern, we’d love to hear how you shape the frontend experience.