Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Ai Laravel Package

backstage/laravel-ai

A Laravel package for adding AI capabilities to your app with a clean, expressive API. Configure providers, manage prompts, and build AI-powered features like chatbots and content generation using familiar Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. backstage/laravel-ai is published at 0 stars, 0 dependents, with the description literally reading "This is my package ai" — the default scaffold text. That combination means you should treat this as an unvetted internal-style package, not a battle-tested dependency. Before you write a line of application code, budget 30–45 minutes to read the source. There is no community knowledge base, no Stack Overflow corpus, and no third-party tutorials to fall back on.

Minimal install

composer require backstage/laravel-ai

Then check whether the package auto-discovers its service provider. If it does not (common in scaffolded packages that never had the extra.laravel block filled in), register it manually in bootstrap/providers.php (Laravel 11+) or config/app.php:

// bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,
    Backstage\LaravelAi\LaravelAiServiceProvider::class,
];

Where to look first, in this order:

  1. composer.json inside vendor/backstage/laravel-ai/ — the require block tells you which HTTP client and which Laravel versions are actually supported. If it pins laravel/framework: ^10.0 only, you know the package predates the 11/12 lifecycle and may break on php artisan about.
  2. src/ for the entry points: look for a Facade, a Manager (Laravel's driver-manager idiom), and any Contracts/ directory. Those three files tell you the entire public API surface in about five minutes.
  3. config/ — if a config file exists, run php artisan vendor:publish --provider="Backstage\LaravelAi\LaravelAiServiceProvider" --tag=config and read every key.
  4. README.md and CHANGELOG.md — the changelog is more trustworthy than the README for a 0-star package; the README is very likely still the scaffold.

First use case. Pick the smallest possible call and prove the wiring end-to-end before designing anything. Something like resolving the manager out of the container and making a single synchronous completion:

$ai = app(\Backstage\LaravelAi\Contracts\AiManager::class); // confirm the real FQCN in src/
$response = $ai->chat('Say hello in five words.');
dump($response); // dump, don't log — you want to see the raw shape

The goal of this first call is not to build a feature. It is to discover:

  • the return type (string? DTO? PSR-7 response? array with choices.0.message.content?)
  • whether exceptions are package-specific or bubble up as GuzzleHttp\Exception\*
  • whether the API key is read from config('laravel-ai.*') or env() directly (the latter is a red flag — it breaks config:cache)

Only after that single call works should you build a second one.

Implementation Patterns

Given the package's immaturity, the dominant implementation pattern is wrapping it, not calling it directly. Design your application so the package is reachable through exactly one or two of your own classes. That way, when the package stalls (see Gotchas) or you migrate to openai-php/client, prism-php/prism, or the official Laravel AI SDK, you change one file.

Pattern 1 — the adapter boundary

namespace App\Ai;

interface Completer
{
    public function complete(string $prompt, array $options = []): string;
}

final class PackageCompleter implements Completer
{
    public function __construct(private readonly \Backstage\LaravelAi\Contracts\AiManager $ai) {}

    public function complete(string $prompt, array $options = []): string
    {
        return (string) $this->ai->chat($prompt, $options);
    }
}

Bind it in a service provider, and inject Completer everywhere. Never let Backstage\LaravelAi\* appear in a controller or a Blade view.

Pattern 2 — feature classes, one per prompt

Treat prompts as first-class, versioned artefacts rather than string concatenation scattered through the codebase. One class per prompt, with the model name and temperature as class constants, and the prompt itself in a dedicated Blade or plain-text view:

final class SummariseTicket
{
    public const MODEL = 'gpt-4o-mini';
    public const PROMPT_VIEW = 'prompts.summarise-ticket';

    public function __construct(private readonly Completer $completer) {}

    public function __invoke(Ticket $ticket): string
    {
        return $this->completer->complete(
            view(self::PROMPT_VIEW, ['ticket' => $ticket])->render(),
            ['model' => self::MODEL, 'temperature' => 0.2],
        );
    }
}

This is the single highest-value pattern with an unproven package: when the model drifts or the package's option-passing turns out to be broken, you have one file to change instead of forty.

Pattern 3 — queue everything non-interactive

Never call the AI provider inside a web request unless the user is actively waiting for a chat reply. Summarisation, classification, tagging, embedding backfills — all of it goes on a job:

final class SummariseTicketJob implements ShouldQueue
{
    use Queueable, Batchable;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];
    public int $timeout = 60;

    public function __construct(public readonly int $ticketId) {}

    public function handle(SummariseTicket $summarise): void
    {
        $ticket = Ticket::findOrFail($this->ticketId);
        $ticket->update(['summary' => $summarise($ticket)]);
    }
}

Set $timeout lower than your queue worker's --timeout, and set $tries low — retrying a failed AI call three times is fine; retrying it ten times is how you burn through a monthly budget in an afternoon.

Pattern 4 — persist every call

You will need this the first time someone asks "why is the bill $400?" Create a migration-backed table before going to production, and write to it from the adapter (not from the feature classes, so it is impossible to forget):

Schema::create('ai_calls', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->nullable()->constrained();
    $t->string('feature');          // 'summarise_ticket'
    $t->string('model');
    $t->unsignedInteger('prompt_tokens')->nullable();
    $t->unsignedInteger('completion_tokens')->nullable();
    $t->unsignedInteger('latency_ms');
    $t->boolean('succeeded');
    $t->text('error')->nullable();
    $t->timestamps();
});

If the package does not surface token counts in its response object, check whether it returns the raw decoded JSON alongside a convenience value — many scaffolded packages do, often as a second element of an array or a ->raw property. If it genuinely discards usage data, that is a strong argument for wrapping the HTTP layer yourself.

Pattern 5 — caching and idempotency

For deterministic, low-temperature prompts, hash the inputs and cache the output. This is the cheapest possible cost control and it also smooths over provider rate limits:

return Cache::remember(
    'ai:summary:'.hash('xxh128', $ticket->body),
    now()->addDays(30),
    fn () => $this->completer->complete(/* ... */),
);

Integration tips

  • Streaming. If the package exposes a streaming API, verify it works with your actual web server before building a chat UI. PHP-FPM + nginx buffering will silently break SSE unless you disable buffering; Octane behaves differently again. Test with curl -N first.
  • Function/tool calling. Confirm the package does not strip unknown keys from the options array. A scaffolded package that does Arr::only($options, ['model', 'temperature']) will make tools impossible, and you will discover this only after wiring a whole tool schema.
  • Local dev without spend. Stand up a fake at the container level rather than mocking inside tests, so php artisan tinker is also free:
// AppServiceProvider::register(), local env only
$this->app->bind(Completer::class, fn () => new class implements Completer {
    public function complete(string $prompt, array $options = []): string
    {
        return '[fake] '.Str::limit($prompt, 40);
    }
});

Gotchas and Tips

The package has no users. 0 stars, 0 dependents, and a placeholder description is not a neutral signal — it means nobody has hit the edge cases yet. Concretely, expect: no CHANGELOG, no UPGRADING.md, no tests you can trust, and a real chance the README's example code does not run against the published tag. Verify every example against vendor/ before adopting it as a pattern. If the code is in fact solid, great — but you cannot assume that, and you should not let it sit on a critical path without the adapter boundary described above.

Read the release date critically. Last release 2026-03-22 is in the future relative to any sane current date, which suggests either a mis-stamped Packagist entry or automated tagging. Either way, do not read "recent release" as "actively maintained." Check the commit history on the default branch and see whether the tag actually points at a commit with real work in it, or at the initial scaffold.

env() outside config is the classic scaffold bug. If src/ calls env('OPENAI_API_KEY') directly anywhere other than a config/ file, the package breaks the moment you run php artisan config:cache in production — and it will work perfectly locally, so you will not catch it in dev. Grep for it:

grep -rn "env(" vendor/backstage/laravel-ai/src/ vendor/backstage/laravel-ai/config/

Anything outside config/ is a bug you should patch via your own config file plus a small binding, not via env() in your app.

Silent exception swallowing. Scaffolded packages frequently wrap HTTP calls in try { } catch (\Throwable $e) { return null; }. A null return then propagates into your database as an empty string, and you find out from a user. Test the failure path explicitly: point the package at an invalid base URL and confirm you get an exception, not a null. If it swallows, throw your own exception from the adapter when the result is null.

No retry or rate-limit handling is likely. Laravel's HTTP client supports ->retry(3, 200, throw: false) natively, but the package probably calls Http::post(...) without it. If so, you get raw 429s whenever you scale up, and your jobs fail rather than back off. Either configure retries inside the adapter, or keep $tries/$backoff on the job and accept job-level retry semantics.

Cost blowups come from loops, not from any single call. The failure mode is not one expensive prompt; it is a foreach over 5,000 records in a command that somebody runs twice. Guard every batch with a hard cap (->take(100)) and a confirmation prompt in interactive commands, and put an absolute monthly ceiling in your ai_calls query — alarm on it, don't just report on it.

Testing. Use Http::fake() at the transport layer if the package respects the Laravel HTTP client (likely, given the ecosystem idiom) — that exercises the package's real parsing code, which is what you most want to test. Use your Completer fake for everything downstream. Do not write tests that assert on model output text; assert on structure and on what your application did with the result.

Extension points to look for. In a well-built Laravel package of this shape, the extension seams are: a manager class with an extend() method for custom drivers, a bound contract you can swap in the container, and publishable config. If those three exist, you can insulate yourself almost completely. If only the facade exists and it resolves a concrete class, your adapter boundary is doing all the work and you should consider pinning the exact version ("backstage/laravel-ai": "1.0.*") rather than floating, because a minor bump can rename a method with no changelog entry to warn you.

When to walk away. If, after the first-call test, you find any two of: no config file, no contract interface, no exception handling, and no tests — do not build on it. Swap in prism-php/prism or the official Laravel AI package, both of which have real user bases and support contracts. The migration cost from a thin adapter is an afternoon; the migration cost from a deep integration is a quarter.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core