# 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.

- URL: https://laravel-vuejs.space/laravel-ai-sdk-vue
- Author: Anass Ez-zouaine (https://laravel-vuejs.space/author/anass-ez-zouaine)
- Published: 2026-10-11
- Topics: Laravel, VueJs, Inertia

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](https://laravel.com/docs/13.x/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:

`.env`
```ini
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.php`
```php
<?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.php`
```php
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.vue`
```vue
<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.php`
> ```php
> 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](https://laravel-vuejs.space/laravel-vue-in-2026). The form posts like the [CRUD guide](https://laravel-vuejs.space/laravel-inertia-vue-crud). The test style matches [Test a Laravel and Vue app](https://laravel-vuejs.space/test-laravel-vue).

## 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

**Q: 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.

**Q: 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.

**Q: How do I test Laravel AI SDK agents?**

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

**Q: 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

- [Stream an AI SDK reply without an Inertia visit](https://laravel-vuejs.space/laravel-ai-sdk-stream-vue)
- [Continue an AI SDK conversation you own](https://laravel-vuejs.space/laravel-ai-conversation-vue)
- [Return a structured score from the AI SDK](https://laravel-vuejs.space/laravel-ai-structured-output-vue)
- [Fake an AI agent in a Pest test](https://laravel-vuejs.space/pest-fake-ai-agent-vue)

## Sources

- [Laravel 13: AI SDK](https://laravel.com/docs/13.x/ai-sdk)
- [Inertia v3: Forms](https://inertiajs.com/docs/v3/the-basics/forms)
