Skip to content

Guide

How to add AI to a Laravel SaaS without losing control of cost and data

By Adel Zemzemi ·

Your customers expect AI in the product, and the demo took an afternoon. Shipping it inside a real Laravel SaaS, with paying customers, is a different job. Laravel now has an official AI SDK that does most of the plumbing. Here is the order I work in on top of it, so the feature stays reliable and its cost stays inside what each customer pays.

  1. Start from the job, not from the model

    Write down the one task the feature does for your customer: extract the fields from an uploaded invoice, rewrite a paragraph, match a candidate to a job. One task, one input, one expected output.

    If you cannot describe the output precisely enough to validate it, the feature is not ready to build. "Make it smarter" is not a feature.

  2. Use the official Laravel AI SDK, one agent per feature

    Laravel now ships a first-party AI SDK, laravel/ai. It talks to OpenAI, Anthropic, Gemini, Mistral, Ollama and other providers through one API, so there is no reason to call a provider's HTTP API by hand or to write your own abstraction anymore.

    Each feature becomes an agent class: its instructions, and the exact shape of the answer you expect.

    composer require laravel/ai
    php artisan make:agent InvoiceExtractor --structured
    
    class InvoiceExtractor implements Agent, HasStructuredOutput
    {
        use Promptable;
    
        public function instructions(): string
        {
            return 'Extract the invoice fields. Use only figures printed on the document. '
                .'If a field is missing, return null for it.';
        }
    
        public function schema(JsonSchema $schema): array
        {
            return [
                'invoice_number' => $schema->string()->required(),
                'total' => $schema->number()->required(),
                'currency' => $schema->string()->required(),
            ];
        }
    }

    The SDK also ships fakes, so your test suite never calls a provider or pays for a token.

  3. Keep the provider swappable, with a fallback

    The provider is configuration, not code. You can set it per agent, change it per call, and pass a list so the SDK falls back to the next provider when the first one fails.

    $response = (new InvoiceExtractor)->prompt(
        $document->text,
        provider: [Lab::Anthropic, Lab::OpenAI],
    );

    A provider outage or a price change becomes a one-line decision instead of a migration.

  4. Decide what data leaves your system

    List, field by field, what the feature sends to the provider. Build the prompt from only those fields, never from a whole Eloquent model serialized to JSON.

    This list is what your customers and their lawyers will ask for. Keep it next to the feature, and update your data processing records when a provider is added.

  5. Count and cap usage per account

    Every response carries its token usage. An agent middleware is the natural place to refuse a call when the account has reached the limit of its plan, and to record what the call consumed.

    php artisan make:agent-middleware EnforceAiQuota
    
    class EnforceAiQuota
    {
        public function __construct(private Team $team) {}
    
        public function handle(PendingStep $step, Closure $next)
        {
            if ($this->team->aiUsageThisMonth() >= $this->team->plan->aiMonthlyLimit) {
                throw new AiQuotaExceeded($this->team);
            }
    
            return $next($step)->then(
                fn (StepResponse $response) => $this->team->recordAiUsage($response->usage)
            );
        }
    }

    Without this, one heavy customer can cost more in model calls than they pay you, and you only find out on the invoice.

  6. Keep model calls out of the web request

    A model call can take seconds and can fail. Queue it, show the user a pending state, and update the record when the answer arrives. The SDK queues a prompt for you and lets you handle both outcomes.

    (new InvoiceExtractor)
        ->queue($document->text)
        ->then(fn (AgentResponse $response) => $document->applyExtraction($response))
        ->catch(fn (Throwable $e) => $document->markExtractionFailed());

    The failure branch is where the user-facing message comes from. The rest of the app keeps working while the provider is down.

  7. Check the answer against your business rules

    Structured output guarantees the shape of the answer, not that it is right. Before saving, check it like any other untrusted input: totals that add up, a currency you support, a reference that exists in your database. An answer that fails is flagged for review, never written as is.

  8. Never let the model invent a number

    Models are good with language and unreliable with facts. Figures shown to a customer (prices, totals, dates, scores) come from your database or from the document itself, and the instructions say so explicitly.

    When a feature needs a figure the input does not contain, it should return nothing for it, not guess.

  9. Measure the cost per customer

    Store the input and output tokens of every call with the account, the feature and the model. One query then tells you what each customer costs per month, which feature is expensive, and whether your plan limits match your prices.

  10. Ship it behind a feature flag

    Turn the feature on for a few accounts first. Laravel Pennant gives you per-account flags without extra infrastructure, so you can watch cost, failures and answer quality before everyone has it.

    if (Feature::for($team)->active('ai-invoice-import')) {
        (new InvoiceExtractor)->queue($document->text);
    }