Call the Laravel AI SDK from a Vue page

Install laravel/ai, prompt one agent from a controller, and show the reply on an Inertia Vue page. The provider key stays on the server.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/laravel-ai-sdk-vue.md
Add one Laravel AI SDK agent and show its reply on an existing Inertia Vue page.
Do not put the provider key in a VITE_ variable or call the provider from the browser.
Do not leave the prompt route public.
Do not pass $request->all() to prompt().
Show the diff. Do not commit.
On this page · 9 sections
  1. Install the SDK
  2. One agent, one job
  3. Show it on the Vue page
  4. Fake the agent in a test
  5. What to leave out
  6. Recap
  7. FAQ
  8. Next steps
  9. Sources

You want an AI-drafted reply on a page you already have, without leaking the provider key or building a chat app. The Laravel AI SDK does it in one agent class, one controller action and one Inertia form, and Pest can fake it.

TL;DR

  • composer require laravel/ai, publish, set one provider key in .env.
  • make:agent DraftReply; prompt it from an authenticated controller with only the validated note.
  • Redirect back with (string) $draft; the Vue page renders it as a prop.
  • DraftReply::fake([...]) + assertPrompted() in tests; no real provider calls.

What you'll learn

  • How to install the Laravel AI SDK and set a provider key
  • How to write a small agent and prompt it from a controller
  • How to show the reply on an Inertia page and fake it in Pest

Who it's for: Laravel + Vue developers adding a first AI feature

Applies to: Laravel 13 · laravel/ai · Inertia 3 · Vue 3 · Pest

People searching “Laravel AI SDK” and “Laravel AI Vue” want a reply on a page they already have. The Laravel AI SDK is the official package for that. It talks to OpenAI, Anthropic, Gemini, and the other supported providers through one PHP API. The Vue page only sends the note and renders the text that comes back.

Gotcha: the key never reaches Vue

Do not call the provider from the browser. A VITE_ variable is shipped to every visitor. Put the key in .env and prompt from a controller.

Install the SDK#

shell
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate

Publishing writes config/ai.php and the conversation migrations. The migrate step creates agent_conversations and agent_conversation_messages. You need those tables only when an agent stores a thread. A one-shot reply does not.

Set one provider key. The SDK reads it from the environment:

.envini
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GEMINI_API_KEY=

Leave the keys you do not use empty. Do not copy a key into resources/js.

One agent, one job#

php artisan make:agent DraftReply creates app/Ai/Agents/DraftReply.php. Keep the first agent small. This one rewrites a note. It does not browse, store a thread, or pick a model per request.

app/Ai/Agents/DraftReply.phpphp
<?php

namespace App\Ai\Agents;

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

class DraftReply implements Agent
{
    use Promptable;

    public function instructions(): string
    {
        return 'Rewrite the note as one short reply. Do not add facts that are not in the note.';
    }
}

Prompt it from a controller that already belongs to the page. Validate the note. Cast the response to a string, which the docs do for a text agent. Send the visitor back to the form with that string in the session.

app/Http/Controllers/ReplyController.phpphp
public function create(): Response
{
    return Inertia::render('replies/Create', [
        'draft' => session('draft'),
    ]);
}

public function store(Request $request): RedirectResponse
{
    $validated = $request->validate([
        'note' => ['required', 'string', 'max:2000'],
    ]);

    $draft = (new DraftReply)->prompt($validated['note']);

    return to_route('replies.create')->with('draft', (string) $draft);
}

Gotcha: an open prompt endpoint is an open bill

Put both routes behind auth. An open prompt endpoint is an open bill. Do not pass $request->all() into prompt(). The agent should see the note, not the rest of the request.

prompt() accepts a provider and a model when you need to override the default:

php
use Laravel\Ai\Enums\Lab;

$draft = (new DraftReply)->prompt(
    $validated['note'],
    provider: Lab::Anthropic, 
    timeout: 120,
);

Show it on the Vue page#

The page is a normal Inertia form. useForm posts the note. The draft arrives as a prop after the redirect. Validation errors land on form.errors.note because the controller used $request->validate().

resources/js/pages/replies/Create.vuevue
<script setup>
import { useForm } from "@inertiajs/vue3";

defineProps({
  draft: { type: String, default: "" },
});

const form = useForm({ note: "" });

function submit() {
  form.post("/replies");
}
</script>

<template>
  <form @submit.prevent="submit">
    <textarea v-model="form.note" name="note" maxlength="2000" />
    <p v-if="form.errors.note">{{ form.errors.note }}</p>
    <button type="submit" :disabled="form.processing">Draft a reply</button>
    <p v-if="draft">{{ draft }}</p>
  </form>
</template>

Heads up: redirect first, stream later

Do not install a second HTTP client for this form. Inertia already posts it. A streamed reply is a different endpoint: stream() can be returned from a route and Laravel sends server-sent events. That is not an Inertia visit. Ship the redirect first.

Fake the agent in a test#

Run it without spending a token

DraftReply::fake() stops the test from calling a provider. Pass the replies in order. assertPrompted checks the note that crossed the wire.

tests/Feature/DraftReplyTest.phpphp
use App\Ai\Agents\DraftReply;
use App\Models\User;

it('shows the drafted reply', function () {
    DraftReply::fake(['The invoice is three days late.']);

    $this->actingAs(User::factory()->create())
        ->post('/replies', ['note' => 'Invoice late by 3 days'])
        ->assertRedirect()
        ->assertSessionHas('draft', 'The invoice is three days late.');

    DraftReply::assertPrompted('Invoice late by 3 days');
});

php artisan test --filter=DraftReply passes with no provider key set.

A closure receives the AgentPrompt when the reply depends on the note. preventStrayPrompts() fails the test if something prompts an agent you did not fake.

What to leave out#

Gotcha: continue() does not check ownership

Do not store a conversation until a person needs the previous turn. RemembersConversations writes the thread, and forUser($user) starts one. continue($id, as: $user) resumes it. Before you continue an id that came from the browser, check conversationBelongsTo() on the conversation store. The docs say continue does not check ownership.

Do not add tools, structured output, or a second provider on the first page. A structured agent implements HasStructuredOutput and a schema() method, then the response is read like an array. That is the next change, after this page returns a string.

The stack choice for the page itself is still the one in Laravel and Vue in 2026. The form posts like the CRUD guide. The test style matches Test a Laravel and Vue app.

Recap#

  • Key in .env, agent in PHP, reply as a prop.
  • Validate and authenticate before you prompt.
  • Pass only the validated note to prompt().
  • Fake every agent in tests; add tools, threads and streams later.

FAQ#

Can I call OpenAI or Anthropic directly from Vue?

No. Any VITE_ variable ships to every visitor. Keep the key in .env and prompt from a Laravel controller.

Do I need the conversation tables for a one-shot reply?

No. agent_conversations and agent_conversation_messages are only used when an agent stores a thread.

How do I test Laravel AI SDK agents?

Call DraftReply::fake([...]) with the replies, then assertPrompted(); preventStrayPrompts() fails on unfaked agents.

Can I stream the reply into the Vue page?

Yes, but stream() belongs on its own route as server-sent events, not on an Inertia form action.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.