spaanproductions/laravel-ai-usage
A Laravel package for tracking AI model usage, token consumption, and costs across your application. It helps log requests from AI providers, monitor spending, and gain visibility into usage through a simple, configurable integration.
The package records AI/LLM usage (tokens, cost, model, and related metadata) in an ai_usage_records table and stores budget thresholds/breaches in an ai_usage_budget_alerts. Day-to-day, it is the ledger you query when you need to answer "what did this feature/user/tenant cost us this month?"
Minimal setup:
composer require <vendor>/ai-usage (check the README for the exact vendor name).php artisan vendor:publish --tag=ai-usage-migrations (plus whatever config tag the package exposes).php artisan migrate.Where to look first:
config/ file — model/table names, cost per 1k tokens, and any default alert thresholds.First use case: instrument one AI-backed controller action, confirm a row lands in ai_usage_records, then add a monthly budget alert. That small loop (record → query → alert) is the whole package in miniature.
Centralise recording behind one service. Do not sprinkle the package's recorder across controllers. Instead create something like App\Services\AiUsageRecorder that your feature code calls, and let it handle user/tenant attribution, model naming, and error swallowing. This is the seam you will need when the package's API changes or you swap in a queue.
Attribute usage explicitly. Every record should carry a user_id, tenant_id, or a morphable subject. Retrofitting attribution later means backfilling rows you cannot reconstruct. Pass a Billable-style subject into the recorder rather than relying on auth()->id(), which breaks in jobs and artisan commands.
Record from a queued job, not the request path. Writing a row per LLM call is cheap, but wrapping it in a dispatch() keeps usage persistence failures from breaking a user-facing response, and it makes retries explicit. Give the job a tries = 3 and log a warning on final failure.
Query with aggregates, not row dumps. Day-to-day reporting should group by day and subject:
AiUsageRecord::query()
->where('created_at', '>=', now()->startOfMonth())
->selectRaw('subject_type, subject_id, SUM(total_tokens) as tokens, SUM(cost) as cost')
->groupBy('subject_type', 'subject_id')
->orderByDesc('cost')
->get();
Add a covering index on (subject_type, subject_id, created_at) in your own migration if the package's table lacks one — you will feel it at a few hundred thousand rows.
Wire budget alerts into existing notification channels. Let the package detect threshold breaches, then fan out through your own Notification classes (Slack, mail, or a users relation). Treat the alert record as an event source, not as the delivery mechanism.
Expose a read-only dashboard early. A simple Filament resource or Livewire table over ai_usage_records grouped by day is usually the first thing stakeholders ask for. Build it against the aggregate query above so it stays cheap.
Integration tips:
feature or operation string at the call site; it makes later cost attribution possible without joining application tables.RefreshDatabase and assert on the recorder's record() call count rather than on row contents, so column changes do not break your suite.Migration publishing is now additive only. As of v0.1.6, vendor:publish --tag=ai-usage-migrations copies only new migration files. Previously every update re-published all migrations under a fresh timestamp, so migrate collided with already-existing tables (ai_usage_records, ai_usage_budget_alerts). If you followed the v0.1.4 upgrade note, check database/migrations/ for duplicated create_ai_usage_records_table and create_ai_usage_budget_alerts_table files, delete the ones that have not yet run, and then php artisan migrate. Leaving them in place is harmless until someone runs a fresh migration order that includes them.
Never hand-edit published migrations you have already shipped. The publishing behaviour assumes published files are committed and stable. If you need schema changes, add a new migration in your own database/migrations directory rather than modifying the package's.
migrate:fresh in CI will hide the duplicate problem. Local setups running migrate:fresh succeed on duplicate creation migrations; production does not. Run php artisan migrate --pretend against a production-shaped schema before deploying an upgrade.
Config cache. After composer update, run php artisan config:cache (or config:clear in dev). Stale cached config is the most common cause of "my threshold changes did nothing."
Cost drift. If the package computes cost from a bundled price table, model prices will lag reality. Override the pricing map in your published config and treat the package's numbers as a starting point; reconcile against the provider's billing export monthly.
Token counts are estimates. Provider SDKs sometimes report different usage than the package captures (streaming responses are a frequent gap). Do not bill customers directly off these rows without a reconciliation step.
Extension points. The package's recorder and alert evaluator are typically resolvable from the container — rebind them in a service provider to add custom fields (e.g. request_id, prompt_version) or to change alert routing. Rebinding is preferable to forking, because it survives composer update.
Debugging checklist. Missing row? Check the queue worker is running, then that the recorder is injected (not new'd), then the config's table name. Wrong totals? Check timezone handling on created_at and whether any records are soft-deleted. Alerts not firing? Confirm the evaluator runs on a schedule and that the alert threshold is expressed in the same unit as the stored cost.
How can I help you explore Laravel packages today?