Skip to main content

Introduction

Laravel AI SDK provides a unified, expressive API for interacting with AI providers such as OpenAI, Anthropic, Gemini, and more. With the AI SDK you can build intelligent agents with tools and structured output, generate images, synthesize and transcribe audio, create vector embeddings, and much more — all using a consistent, Laravel-friendly interface.
Laravel AI SDK is an official package (laravel/ai) available in Laravel 13.
CapabilitySupported providers
TextOpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter
ImagesOpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter
TTSOpenAI, ElevenLabs, Gemini
STTOpenAI, ElevenLabs, Mistral, Gemini
EmbeddingsOpenAI, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter
RerankingCohere, Jina, VoyageAI
FilesOpenAI, Anthropic, Gemini, Azure

Installation

1

Install the package

composer require laravel/ai
2

Publish config and migrations

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
3

Run migrations

php artisan migrate
This creates the agent_conversations and agent_conversation_messages tables used to persist conversation history.

Configuration

Set your API keys in .env. Only add keys for providers you plan to use:
ANTHROPIC_API_KEY=
AZURE_OPENAI_API_KEY=
COHERE_API_KEY=
DEEPSEEK_API_KEY=
ELEVENLABS_API_KEY=
GEMINI_API_KEY=
GROQ_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
JINA_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=
The default models for each capability (text, images, audio, transcription, embeddings) may also be configured in config/ai.php. For example, to default to GPT-4o for text generation and dall-e-3 for image generation:
// config/ai.php
'models' => [
    'text'          => env('AI_TEXT_MODEL', 'gpt-4o'),
    'image'         => env('AI_IMAGE_MODEL', 'dall-e-3'),
    'audio'         => env('AI_AUDIO_MODEL', 'tts-1'),
    'transcription' => env('AI_TRANSCRIPTION_MODEL', 'whisper-1'),
    'embeddings'    => env('AI_EMBEDDINGS_MODEL', 'text-embedding-3-small'),
],

Custom base URLs

If you route requests through a proxy, configure a custom URL per provider in config/ai.php:
'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.

OpenAI-compatible providers

For APIs compatible with OpenAI, such as LM Studio, vLLM, Together, Fireworks, or a local gateway, configure a provider with the openai-compatible driver. The url is required. If you specify a key, it is sent as a bearer token.
'providers' => [
    'local' => [
        'driver' => 'openai-compatible',
        'url' => env('LOCAL_AI_URL'),
        'key' => env('LOCAL_AI_API_KEY'),
    ],
],
Use the configured provider name like any other provider:
agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model');
Configure a default text model to avoid specifying it for every request:
'local' => [
    'driver' => 'openai-compatible',
    'url' => env('LOCAL_AI_URL'),
    'key' => env('LOCAL_AI_API_KEY'),
    'models' => [
        'text' => [
            'default' => env('LOCAL_AI_MODEL'),
        ],
    ],
],
OpenAI-compatible providers support text generation, streaming, tools, structured output, and image attachments. If the endpoint requires additional request body fields, use provider options.

Lab enum

Use the Lab enum to reference providers without hardcoding strings:
use Laravel\Ai\Enums\Lab;

Lab::Anthropic;
Lab::OpenAI;
Lab::Gemini;

Agents

Agents are the fundamental building block of the Laravel AI SDK. Each agent is a PHP class that encapsulates a system prompt, conversation context, tools, and an optional structured output schema.

Creating an agent

php artisan make:agent SalesCoach
Add --structured for agents that return structured JSON output:
php artisan make:agent SalesCoach --structured
A full agent implementing all available interfaces:
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\History;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;

class SalesCoach implements Agent, Conversational, HasTools, HasStructuredOutput
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): Stringable|string
    {
        return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
    }

    public function messages(): iterable
    {
        return History::where('user_id', $this->user->id)
            ->latest()
            ->limit(50)
            ->get()
            ->reverse()
            ->map(function ($message) {
                return new Message($message->role, $message->content);
            })
            ->all();
    }

    public function tools(): iterable
    {
        return [new RetrievePreviousTranscripts];
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'feedback' => $schema->string()->required(),
            'score'    => $schema->integer()->min(1)->max(10)->required(),
        ];
    }
}

Prompting

Instantiate the agent and call prompt:
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');

return (string) $response;
Use make to resolve the agent from the service container:
$agent = SalesCoach::make(user: $user);
Override the provider, model, or timeout per request:
use Laravel\Ai\Enums\Lab;

$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: Lab::Anthropic,
    model:    'claude-haiku-4-5-20251001',
    timeout:  120,
);

Conversation context

Implement Conversational and define a messages() method to supply previous messages to the agent.

Manual history

use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Messages\Message;

class SalesCoach implements Agent, Conversational
{
    use Promptable;

    public function messages(): iterable
    {
        return History::where('user_id', $this->user->id)
            ->latest()->limit(50)->get()->reverse()
            ->map(fn ($message) => new Message($message->role, $message->content))
            ->all();
    }
}

Automatic DB storage with RemembersConversations

The RemembersConversations trait stores and retrieves history automatically using the published migrations:
use Laravel\Ai\Concerns\RemembersConversations;

class SalesCoach implements Agent, Conversational
{
    use Promptable, RemembersConversations;

    public function instructions(): string
    {
        return 'You are a sales coach...';
    }
}

// Start a new conversation
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$conversationId = $response->conversationId;

// Continue an existing conversation
$response = (new SalesCoach)
    ->continue($conversationId, as: $user)
    ->prompt('Tell me more about that.');

Structured output

Implement HasStructuredOutput and define a schema() method:
public function schema(JsonSchema $schema): array
{
    return [
        'feedback' => $schema->string()->required(),
        'score'    => $schema->integer()->min(1)->max(10)->required(),
    ];
}

$response = (new SalesCoach)->prompt('Analyze this...');

return $response['score'];

Nested objects

public function schema(JsonSchema $schema): array
{
    return [
        'score'    => $schema->integer()->required(),
        'metadata' => $schema->object(fn ($schema) => [
            'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
            'language'   => $schema->string()->required(),
        ])->required(),
    ];
}

Arrays of objects

public function schema(JsonSchema $schema): array
{
    return [
        'feedback' => $schema->array()->items(
            $schema->object(fn ($schema) => [
                'comment' => $schema->string()->required(),
                'score'   => $schema->integer()->required(),
            ])
        )->required(),
    ];
}

anyOf

If a value may match one of several schemas, use the anyOf method:
public function schema(JsonSchema $schema): array
{
    return [
        'content' => $schema->anyOf([
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['article'])->required(),
                'title' => $schema->string()->required(),
            ]),
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['image'])->required(),
                'url' => $schema->string()->required(),
            ]),
        ])->required(),
    ];
}

Attachments

Attach documents or images to a prompt:
use Laravel\Ai\Files;

$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [
        Files\Document::fromStorage('transcript.pdf'),
        Files\Document::fromPath('/home/laravel/transcript.md'),
        $request->file('transcript'),
    ]
);
For image attachments use Files\Image:
$response = (new ImageAnalyzer)->prompt('What is in this image?', attachments: [
    Files\Image::fromStorage('photo.jpg'),
    Files\Image::fromPath('/home/laravel/photo.jpg'),
    $request->file('photo'),
]);

Streaming

Return a stream from a route to deliver the response as Server-Sent Events (SSE):
Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze this sales transcript...');
});
Register a then callback to run after streaming completes:
use Laravel\Ai\Responses\StreamedAgentResponse;

Route::get('/coach', function () {
    return (new SalesCoach)
        ->stream('Analyze this sales transcript...')
        ->then(function (StreamedAgentResponse $response) {
            // $response->text, $response->events, $response->usage...
        });
});
Iterate the stream manually:
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    // handle each event
}

Vercel AI SDK protocol

Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze...')->usingVercelDataProtocol();
});

Broadcasting

Broadcast each streamed event over a Laravel channel:
use Illuminate\Broadcasting\Channel;

$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    $event->broadcast(new Channel('channel-name'));
}
Or use broadcastOnQueue to broadcast asynchronously:
(new SalesCoach)->broadcastOnQueue(
    'Analyze this sales transcript...',
    new Channel('channel-name'),
);

Skipping oversized events

Some broadcasting platforms limit WebSocket messages to approximately 10 KB. Data-heavy stream events, such as large tool results, can exceed that limit and fail to broadcast. Use the WithoutBroadcasting attribute to exclude specific event types.
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Attributes\WithoutBroadcasting;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Laravel\Ai\Streaming\Events\ToolCall;
use Laravel\Ai\Streaming\Events\ToolResult;

#[WithoutBroadcasting(ToolCall::class, ToolResult::class)]
class SearchAgent implements Agent, HasTools
{
    use Promptable;

    // ...
}
Excluded events are not broadcast, but they are still stored in the agent_conversation_messages table. The frontend can retrieve the complete tool data after streaming finishes. This works for queued broadcasting with broadcastOnQueue and synchronous broadcasting with broadcast or broadcastNow.

Queueing

Run the agent in the background:
use Laravel\Ai\Responses\AgentResponse;

Route::post('/coach', function (Request $request) {
    (new SalesCoach)
        ->queue($request->input('transcript'))
        ->then(function (AgentResponse $response) { /* ... */ })
        ->catch(function (Throwable $e) { /* ... */ });

    return back();
});

Tools

Tools extend what an agent can do — query databases, call external APIs, perform calculations, etc.
php artisan make:tool RandomNumberGenerator
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class RandomNumberGenerator implements Tool
{
    public function description(): Stringable|string
    {
        return 'This tool may be used to generate cryptographically secure random numbers.';
    }

    public function handle(Request $request): Stringable|string
    {
        return (string) random_int($request['min'], $request['max']);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'min' => $schema->integer()->min(0)->required(),
            'max' => $schema->integer()->required(),
        ];
    }
}
Register tools in the agent’s tools method:
use Laravel\Ai\Contracts\HasTools;

class SalesCoach implements Agent, HasTools
{
    use Promptable;

    public function tools(): iterable
    {
        return [new RandomNumberGenerator];
    }
}

Similarity search tool

Use the built-in SimilaritySearch tool to query a vector-enabled Eloquent model:
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;

public function tools(): iterable
{
    return [
        SimilaritySearch::usingModel(Document::class, 'embedding'),
    ];
}
With additional options:
SimilaritySearch::usingModel(
    model:         Document::class,
    column:        'embedding',
    minSimilarity: 0.7,
    limit:         10,
    query:         fn ($query) => $query->where('published', true),
),
Using a custom closure:
new SimilaritySearch(using: function (string $query) {
    return Document::query()
        ->where('user_id', $this->user->id)
        ->whereVectorSimilarTo('embedding', $query)
        ->limit(10)
        ->get();
}),
Provide a custom description shown to the model:
SimilaritySearch::usingModel(Document::class, 'embedding')
    ->withDescription('Search the knowledge base for relevant articles.'),

File storage tools

The FileStorage tool factory gives an agent access to a Laravel filesystem disk. The all method returns tools for listing, reading, generating URLs for, writing, deleting, and copying files on the selected disk.
use Laravel\Ai\Tools\FileStorage;

public function tools(): iterable
{
    return FileStorage::all('local');
}
Use readOnly to provide read-only access:
return FileStorage::readOnly('local');
These methods return an Illuminate\Support\Collection, so you can further restrict the tools you expose:
use Laravel\Ai\Tools\Filesystem\DeleteFile;

return FileStorage::all('s3')
    ->reject(fn ($tool) => $tool instanceof DeleteFile);

MCP Tools

If your application uses Laravel MCP, you may give your agents tools exposed by Model Context Protocol servers. Using the Laravel MCP client, you may connect to a remote or local MCP server and pass its tools directly to your agent.
MCP tools require the Laravel MCP package to be installed in your application.
Because an MCP client’s tools method returns a collection, spread it into your agent’s tools array using the ... operator:
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;

/**
 * Get the tools available to the agent.
 *
 * @return Tool[]
 */
public function tools(): iterable
{
    return [
        ...Client::web('https://mcp.example.com')
            ->withToken($token)
            ->tools(),

        new RandomNumberGenerator,
    ];
}
The AI SDK automatically wraps each MCP tool so the agent can call it like any other tool. You may also use a named MCP client:
use Laravel\Mcp\Facades\Mcp;

public function tools(): iterable
{
    return [
        ...Mcp::client('github')->tools(),
    ];
}
Or connect to a local MCP server:
use Laravel\Mcp\Client;

public function tools(): iterable
{
    return [
        ...Client::local('php', ['artisan', 'mcp:start'])->tools(),
    ];
}
For more information on creating and authenticating MCP clients, including bearer tokens and OAuth, consult the MCP client documentation.

Provider tools

Provider tools are implemented natively by AI providers. Supported: Anthropic, OpenAI, Gemini, OpenRouter
use Laravel\Ai\Providers\Tools\WebSearch;

public function tools(): iterable
{
    return [new WebSearch];
}
With options:
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),
(new WebSearch)->location(city: 'New York', region: 'NY', country: 'US');

Web Fetch

Supported: Anthropic, Gemini
use Laravel\Ai\Providers\Tools\WebFetch;

public function tools(): iterable
{
    return [new WebFetch];
}

(new WebFetch)->max(3)->allow(['docs.laravel.com']),
Supported: OpenAI, Gemini
use Laravel\Ai\Providers\Tools\FileSearch;

public function tools(): iterable
{
    return [new FileSearch(stores: ['store_id'])];
}

// Multiple stores
new FileSearch(stores: ['store_1', 'store_2']);

// Filter by metadata
new FileSearch(stores: ['store_id'], where: ['author' => 'Taylor Otwell', 'year' => 2026]);

// Complex filter using a closure
use Laravel\Ai\Providers\Tools\FileSearchQuery;

new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) =>
    $query->where('author', 'Taylor Otwell')
          ->whereNot('status', 'draft')
          ->whereIn('category', ['news', 'updates'])
);

Subagents

You can return another agent from an agent’s tools() method. Registering an agent as a tool lets the parent delegate a specific task to the subagent and incorporate the result into its original response. This is useful when a general-purpose agent needs access to specialists with their own instructions, tools, model, and provider settings. For example, a customer support agent can delegate refund policy questions to a refund specialist:
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
    }

    public function tools(): iterable
    {
        return [new RefundsAgent];
    }
}
To customize how the subagent appears to its parent, implement CanActAsTool and define its tool name and description:
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a refunds specialist. Use order details and the refund policy to give concise eligibility guidance.';
    }

    public function name(): string
    {
        return 'refunds_specialist';
    }

    public function description(): string
    {
        return 'Determine whether an order is eligible for a refund and explain the next step.';
    }

    public function tools(): iterable
    {
        return [new LookupOrder];
    }
}
If a subagent does not implement CanActAsTool, Laravel derives the tool name from its class and generates a generic description. Each subagent invocation is isolated and does not inherit the parent’s conversation history.

Middleware

Agent middleware lets you inspect or modify prompts and responses before and after they are sent.
php artisan make:agent-middleware LogPrompts
Implement the middleware on the agent:
use App\Ai\Middleware\LogPrompts;
use Laravel\Ai\Contracts\HasMiddleware;

class SalesCoach implements Agent, HasMiddleware
{
    use Promptable;

    public function middleware(): array
    {
        return [new LogPrompts];
    }
}
A middleware class:
<?php

namespace App\Ai\Middleware;

use Closure;
use Laravel\Ai\Prompts\AgentPrompt;

class LogPrompts
{
    public function handle(AgentPrompt $prompt, Closure $next)
    {
        Log::info('Prompting agent', ['prompt' => $prompt->prompt]);

        return $next($prompt);
    }
}
Inspect the response after it is generated using then:
public function handle(AgentPrompt $prompt, Closure $next)
{
    return $next($prompt)->then(function (AgentResponse $response) {
        Log::info('Agent responded', ['text' => $response->text]);
    });
}

Anonymous agents

Create a one-off agent inline with the agent() helper:
use function Laravel\Ai\{agent};

$response = agent(
    instructions: 'You are an expert at software development.',
    messages:     [],
    tools:        [],
)->prompt('Tell me about Laravel');
With structured output:
use Illuminate\Contracts\JsonSchema\JsonSchema;

$response = agent(
    schema: fn (JsonSchema $schema) => ['number' => $schema->integer()->required()],
)->prompt('Generate a random number less than 100');

Agent configuration via PHP attributes

Use PHP attributes to configure an agent’s default provider, model, and behaviour:
<?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\Attributes\Timeout;
use Laravel\Ai\Attributes\TopP;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[MaxSteps(10)]
#[MaxTokens(4096)]
#[Temperature(0.7)]
#[Timeout(120)]
#[TopP(0.9)]
class SalesCoach implements Agent
{
    use Promptable;
}
Two convenience attributes automatically select the cheapest or most capable model available:
use Laravel\Ai\Attributes\UseCheapestModel;
use Laravel\Ai\Attributes\UseSmartestModel;

#[UseCheapestModel]
class SimpleSummarizer implements Agent { use Promptable; }

#[UseSmartestModel]
class ComplexReasoner implements Agent { use Promptable; }

Provider options

Pass provider-specific options by implementing HasProviderOptions:
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasProviderOptions;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

class SalesCoach implements Agent, HasProviderOptions
{
    use Promptable;

    public function providerOptions(Lab|string $provider): array
    {
        return match ($provider) {
            Lab::OpenAI => [
                'reasoning'         => ['effort' => 'low'],
                'frequency_penalty' => 0.5,
                'presence_penalty'  => 0.3,
            ],
            Lab::Anthropic => [
                'thinking' => ['budget_tokens' => 1024],
            ],
            default => [],
        };
    }
}

Image generation

Generate images with the Image class. Supported providers: OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter.
use Laravel\Ai\Image;

$image = Image::of('A donut sitting on the kitchen counter')->generate();

$rawContent = (string) $image;
Specify quality and aspect ratio:
$image = Image::of('A donut sitting on the kitchen counter')
    ->quality('high')
    ->landscape()
    ->timeout(120)
    ->generate();
Generate from a reference image:
use Laravel\Ai\Files;

$image = Image::of('Update this photo to be in the style of an impressionist painting.')
    ->attachments([Files\Image::fromStorage('photo.jpg')])
    ->landscape()
    ->generate();

Storing generated images

$path = $image->store();
$path = $image->storeAs('image.jpg');
$path = $image->storePublicly();
$path = $image->storePubliclyAs('image.jpg');

Queuing image generation

use Laravel\Ai\Responses\ImageResponse;

Image::of('A donut sitting on the kitchen counter')
    ->portrait()
    ->queue()
    ->then(function (ImageResponse $image) {
        $path = $image->store();
    });

Audio (TTS)

Generate speech from text with the Audio class. Supported providers: OpenAI, ElevenLabs, Gemini.
use Laravel\Ai\Audio;

$audio = Audio::of('I love coding with Laravel.')->generate();

$rawContent = (string) $audio;
Select a voice:
$audio = Audio::of('I love coding with Laravel.')->female()->generate();
$audio = Audio::of('I love coding with Laravel.')->voice('voice-id-or-name')->generate();
$audio = Audio::of('I love coding with Laravel.')->female()->instructions('Said like a pirate')->generate();

Storing generated audio

$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');

Queuing audio generation

use Laravel\Ai\Responses\AudioResponse;

Audio::of('I love coding with Laravel.')
    ->queue()
    ->then(function (AudioResponse $audio) {
        $path = $audio->store();
    });

Transcription (STT)

Transcribe audio files with the Transcription class. Supported providers: OpenAI, ElevenLabs, Mistral, Gemini.
use Laravel\Ai\Transcription;

$transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate();
$transcript = Transcription::fromStorage('audio.mp3')->generate();
$transcript = Transcription::fromUpload($request->file('audio'))->generate();

return (string) $transcript;
Enable speaker diarization:
$transcript = Transcription::fromStorage('audio.mp3')->diarize()->generate();

Queuing transcription

use Laravel\Ai\Responses\TranscriptionResponse;

Transcription::fromStorage('audio.mp3')
    ->queue()
    ->then(function (TranscriptionResponse $transcript) {
        // ...
    });

Embeddings

Generate embeddings using the Embeddings class or the Str macro.
use Illuminate\Support\Str;

$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();
use Laravel\Ai\Embeddings;

$response = Embeddings::for([
    'Napa Valley has great wine.',
    'Laravel is a PHP framework.',
])->generate();

$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]
Specify dimensions and model:
$response = Embeddings::for(['Napa Valley has great wine.'])
    ->dimensions(1536)
    ->generate(Lab::OpenAI, 'text-embedding-3-small');

Querying embeddings (pgvector)

Add the vector column in your migration:
Schema::ensureVectorExtensionExists();

Schema::create('documents', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->vector('embedding', dimensions: 1536);
    $table->timestamps();
});

// Optional HNSW index for performance
$table->vector('embedding', dimensions: 1536)->index();
Cast the column to an array on the model:
protected function casts(): array
{
    return ['embedding' => 'array'];
}
Query by vector similarity:
// Using a pre-computed embedding vector
$documents = Document::query()
    ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
    ->limit(10)
    ->get();

// Pass a string — it is automatically embedded
$documents = Document::query()
    ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
    ->limit(10)
    ->get();
Lower-level query methods:
$documents = Document::query()
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();

Caching embeddings

Enable caching globally in config/ai.php:
'caching' => [
    'embeddings' => [
        'cache' => true,
        'store' => env('CACHE_STORE', 'database'),
    ],
],
Or enable per request:
$response = Embeddings::for(['Napa Valley has great wine.'])->cache()->generate();
$response = Embeddings::for(['Napa Valley has great wine.'])->cache(seconds: 3600)->generate();

// Via the Str macro
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);

Reranking

Rerank a list of documents by relevance to a query. Supported providers: Cohere, Jina, VoyageAI.
use Laravel\Ai\Reranking;

$response = Reranking::of([
    'Django is a Python web framework.',
    'Laravel is a PHP web application framework.',
    'React is a JavaScript library for building user interfaces.',
])->rerank('PHP frameworks');

$response->first()->document; // "Laravel is a PHP web application framework."
$response->first()->score;    // 0.95
$response->first()->index;    // 1

$response = Reranking::of($documents)->limit(5)->rerank('search query');

Reranking Eloquent collections

Rerank by a single field:
$posts = Post::all()->rerank('body', 'Laravel tutorials');
Rerank by multiple fields (concatenated as JSON):
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');
Using a closure:
$reranked = $posts->rerank(
    fn ($post) => $post->title.': '.$post->body,
    'Laravel tutorials'
);
With additional options:
$reranked = $posts->rerank(
    by:       'content',
    query:    'Laravel tutorials',
    limit:    10,
    provider: Lab::Cohere,
);

Files

Upload files to AI providers for use in subsequent prompts or vector stores.
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;

$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();
$response = Document::fromUrl('https://example.com/document.pdf')->put();
$response = Image::fromUrl('https://example.com/photo.jpg')->put();

return $response->id;
Upload raw content or a form upload:
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
$stored = Document::fromUpload($request->file('document'))->put();
Reference a previously stored file in a prompt:
$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [Files\Document::fromId('file-id')]
);
Retrieve file metadata:
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();
Delete a stored file:
Document::fromId('file-id')->delete();
Specify the provider explicitly:
$response = Document::fromPath('/home/laravel/document.pdf')->put(provider: Lab::Anthropic);

Provider-specific options

Use withProviderOptions to pass provider-specific upload options, such as OpenAI’s file purpose:
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/knowledge.txt')
    ->withProviderOptions(['purpose' => 'assistants'])
    ->put();
Pass a closure to configure different options for each provider:
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/training.jsonl')
    ->withProviderOptions(fn (Lab|string $provider) => match ($provider) {
        Lab::OpenAI => ['purpose' => 'fine-tune'],
        default => [],
    })
    ->put();

Vector stores

Manage vector stores for file-search tools.
use Laravel\Ai\Stores;

// Create a store
$store = Stores::create('Knowledge Base');
$store = Stores::create(
    name:                 'Knowledge Base',
    description:          'Documentation.',
    expiresWhenIdleFor:   days(30),
);

return $store->id;

// Retrieve a store
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;

// Delete a store
Stores::delete('store_id');
$store->delete();

Adding files to a store

$store = Stores::get('store_id');

$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
$document = $store->add(Document::fromPath('/path/to/document.pdf'));
$document = $store->add(Document::fromStorage('manual.pdf'));
$document = $store->add($request->file('document'));

$document->id;
$document->fileId;
With metadata:
$store->add(
    Document::fromPath('/path/to/document.pdf'),
    metadata: [
        'author'     => 'Taylor Otwell',
        'department' => 'Engineering',
        'year'       => 2026,
    ]
);
Remove a file from a store:
$store->remove('file_id');
$store->remove('file_abc123', deleteFile: true);

Failover

Pass an array of providers to automatically fall back when one is unavailable:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Image;

$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: [Lab::OpenAI, Lab::Anthropic],
);

$image = Image::of('A donut sitting on the kitchen counter')
    ->generate(provider: [Lab::Gemini, Lab::xAI]);

Testing

The AI SDK provides fake implementations for every capability so you can test without real API calls.

Agents

use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;

SalesCoach::fake();
SalesCoach::fake(['First response', 'Second response']);
SalesCoach::fake(function (AgentPrompt $prompt) {
    return 'Response for: '.$prompt->prompt;
});

SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertPrompted(function (AgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();
Queued prompt assertions:
use Laravel\Ai\QueuedAgentPrompt;

SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();
Prevent real prompts from being sent during tests:
SalesCoach::fake()->preventStrayPrompts();
For agents that return structured output, pass an array as a fake response:
SalesCoach::fake([
    ['score' => 87],
]);
If you call fake() for a structured-output agent without explicitly providing fake data, Laravel automatically generates data that matches the agent’s schema.
For anonymous agents:
use Laravel\Ai\AnonymousAgent;

AnonymousAgent::fake(['Test response']);

Images

use Laravel\Ai\Image;
use Laravel\Ai\Prompts\ImagePrompt;

Image::fake();
Image::fake([base64_encode($firstImage), base64_encode($secondImage)]);
Image::fake(function (ImagePrompt $prompt) {
    return base64_encode('...');
});

Image::assertGenerated(function (ImagePrompt $prompt) {
    return $prompt->contains('sunset') && $prompt->isLandscape();
});
Image::assertNotGenerated('Missing prompt');
Image::assertNothingGenerated();

Image::assertQueued(fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset'));
Image::assertNotQueued('Missing prompt');
Image::assertNothingQueued();

Image::fake()->preventStrayImages();

Audio

use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;

Audio::fake();
Audio::fake([base64_encode($firstAudio), base64_encode($secondAudio)]);
Audio::fake(function (AudioPrompt $prompt) {
    return base64_encode('...');
});

Audio::assertGenerated(function (AudioPrompt $prompt) {
    return $prompt->contains('Hello') && $prompt->isFemale();
});
Audio::assertNotGenerated('Missing prompt');
Audio::assertNothingGenerated();

Audio::assertQueued(fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello'));
Audio::assertNotQueued('Missing prompt');
Audio::assertNothingQueued();

Audio::fake()->preventStrayAudio();

Transcriptions

use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;

Transcription::fake();
Transcription::fake(['First transcription text.', 'Second transcription text.']);
Transcription::fake(function (TranscriptionPrompt $prompt) {
    return 'Transcribed text...';
});

Transcription::assertGenerated(function (TranscriptionPrompt $prompt) {
    return $prompt->language === 'en' && $prompt->isDiarized();
});
Transcription::assertNotGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingGenerated();

Transcription::assertQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized());
Transcription::assertNotQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingQueued();

Transcription::fake()->preventStrayTranscriptions();

Embeddings

use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;

Embeddings::fake();
Embeddings::fake([[$firstEmbeddingVector], [$secondEmbeddingVector]]);
Embeddings::fake(function (EmbeddingsPrompt $prompt) {
    return array_map(
        fn () => Embeddings::fakeEmbedding($prompt->dimensions),
        $prompt->inputs
    );
});

Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->dimensions === 1536;
});
Embeddings::assertNotGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingGenerated();

Embeddings::assertQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel'));
Embeddings::assertNotQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingQueued();

Embeddings::fake()->preventStrayEmbeddings();

Reranking

use Laravel\Ai\Reranking;
use Laravel\Ai\Prompts\RerankingPrompt;
use Laravel\Ai\Responses\Data\RankedDocument;

Reranking::fake();
Reranking::fake([[
    new RankedDocument(index: 0, document: 'First', score: 0.95),
    new RankedDocument(index: 1, document: 'Second', score: 0.80),
]]);

Reranking::assertReranked(function (RerankingPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->limit === 5;
});
Reranking::assertNotReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Django'));
Reranking::assertNothingReranked();

Files

use Laravel\Ai\Files;
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;

Files::fake();

Document::fromString('Hello, Laravel!', mimeType: 'text/plain')->as('hello.txt')->put();

Files::assertStored(fn (StorableFile $file) =>
    (string) $file === 'Hello, Laravel!' && $file->mimeType() === 'text/plain'
);
Files::assertNotStored(fn (StorableFile $file) => (string) $file === 'Hello, World!');
Files::assertNothingStored();

Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();

Vector stores

use Laravel\Ai\Stores;

Stores::fake(); // Also fakes file operations

$store = Stores::create('Knowledge Base');

Stores::assertCreated('Knowledge Base');
Stores::assertCreated(fn (string $name, ?string $description) => $name === 'Knowledge Base');
Stores::assertNotCreated('Other Store');
Stores::assertNothingCreated();

Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();
File operations on a store:
$store = Stores::get('store_id');

$store->add('added_id');
$store->remove('removed_id');

$store->assertAdded('added_id');
$store->assertRemoved('removed_id');
$store->assertNotAdded('other_file_id');
$store->assertNotRemoved('other_file_id');

// Assert by file content
$store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt'));
$store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt');
$store->assertAdded(fn (StorableFile $file) => $file->content() === 'Hello, World!');
Always call fake() in tests to prevent real API calls, unexpected charges, and rate-limit errors.

Events

The Laravel AI SDK dispatches the following events. You can listen for them using standard Laravel event listeners.
EventFired when
PromptingAgentBefore an agent prompt is sent
AgentPromptedAfter an agent prompt completes
StreamingAgentBefore a streaming agent response starts
AgentStreamedAfter a streaming agent response completes
InvokingToolBefore an agent tool is invoked
ToolInvokedAfter an agent tool is invoked
EventFired when
GeneratingImageBefore image generation starts
ImageGeneratedAfter an image is generated
GeneratingAudioBefore audio generation starts
AudioGeneratedAfter audio is generated
GeneratingTranscriptionBefore transcription starts
TranscriptionGeneratedAfter transcription completes
EventFired when
GeneratingEmbeddingsBefore embeddings generation starts
EmbeddingsGeneratedAfter embeddings are generated
RerankingBefore reranking starts
RerankedAfter reranking completes
EventFired when
StoringFileBefore a file is uploaded to a provider
FileStoredAfter a file is uploaded to a provider
FileDeletedAfter a stored file is deleted
CreatingStoreBefore a vector store is created
StoreCreatedAfter a vector store is created
AddingFileToStoreBefore a file is added to a vector store
FileAddedToStoreAfter a file is added to a vector store
RemovingFileFromStoreBefore a file is removed from a vector store
FileRemovedFromStoreAfter a file is removed from a vector store
Last modified on July 13, 2026