backtik-ch / laravel-ai-usage
Track and log AI usage (tokens, cost, duration) for any Laravel application, with optional auto-discovery for laravel/ai SDK.
Requires
- php: ^8.3
- illuminate/console: ^12.0
- illuminate/database: ^12.0
- illuminate/events: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
Suggests
- filament/filament: Enable Filament panel integration (resource + widget).
- laravel/ai: Enable automatic logging via laravel/ai events.
README
Track and log AI usage (tokens, cost, duration) for any Laravel application, with optional auto-discovery for the laravel/ai SDK and an optional Filament panel integration.
Requirements
- PHP 8.3+
- Laravel 12.x
Installation
composer require backtik-ch/laravel-ai-usage
Publish the config file and run the migrations:
php artisan vendor:publish --tag=ai-usage-config php artisan migrate
How It Works
This package hooks into the laravel/ai SDK lifecycle via two events:
PromptingAgent— fired when an agent starts. The package creates a pending record with the agent class, label, and an invocation ID.AgentPrompted— fired when the agent completes. The package locates the pending record, fills in token counts, duration, model/driver info, and snapshots the token prices from your config.
The result is a complete, immutable log of every AI call — no manual code required if you use laravel/ai.
If you're not using laravel/ai, you can log calls manually via the fluent AiUsage facade (see Manual logging).
Configuration
The config file (config/ai-usage.php) exposes the following options:
| Key | Default | Description |
|---|---|---|
table |
ai_usage |
Database table name |
queue_logs |
false |
Dispatch log writes as queued jobs |
log_system_prompt |
false |
Store system prompts in the log |
log_response_text |
true |
Store AI response bodies |
max_text_length |
10_000 |
Truncate prompt/response text (chars); null disables |
stale_timeout_minutes |
60 |
Minutes before a processing record is considered stale |
auto_discover |
true |
Auto-listen to laravel/ai events |
attach_authenticated_user |
false |
Associate auto-discovered logs with the authenticated user |
prices |
(see below) | Token prices (USD / 1 M tokens) per driver & model |
Token prices
Prices are snapshotted into each log row at write time so historical cost estimates remain accurate after you update the config.
// config/ai-usage.php 'prices' => [ 'openai' => [ 'gpt-4o' => ['prompt' => 2.50, 'completion' => 10.00], 'gpt-4o-mini' => ['prompt' => 0.15, 'completion' => 0.60], // ... ], 'anthropic' => [ 'claude-sonnet-4-5' => [ 'prompt' => 3.00, 'completion' => 15.00, 'cache_read' => 0.30, 'cache_write' => 3.75, ], // ... ], 'gemini' => [ 'gemini-2.0-flash' => ['prompt' => 0.10, 'completion' => 0.40], // ... ], ],
Supported price keys: prompt, completion, cache_read, cache_write, reasoning.
The package ships with indicative prices for common OpenAI, Anthropic and Gemini models. Always verify against your provider's current pricing page.
Usage
Tip
Using laravel/ai? You're already done. If auto-discovery is enabled (default) and laravel/ai is installed, every AI agent call is logged automatically — no code needed. The sections below are for manual logging or custom integrations.
Auto-discovery mode
When auto_discover is true (default) and laravel/ai is installed, the package automatically listens for Laravel\Ai\Events\PromptingAgent and Laravel\Ai\Events\AgentPrompted. A pending record is created when the agent starts, then updated with tokens, duration, model info, and cost data when the agent finishes.
To disable auto-discovery, set auto_discover to false in config/ai-usage.php.
Attach an owner automatically
To associate each automatically discovered log with the authenticated user, enable the following option:
// config/ai-usage.php 'attach_authenticated_user' => true,
The package uses the user from Laravel's default authentication guard. Numeric IDs, UUIDs, and ULIDs are supported. Guests and contexts without authentication, such as jobs and commands, are logged without an owner. If the authenticated user cannot be resolved, is not an Eloquent model, or has no primary key, the package writes a warning and creates the usage log without an owner.
Manual logging
Use the AiUsage facade with a fluent builder:
use BacktikCh\LaravelAiUsage\Facades\AiUsage; AiUsage::driver('openai') ->model('gpt-4o') ->label('summarize-article') ->agentClass(MyAgent::class) // optional — FQCN of the agent ->tokens( prompt: 512, completion: 256, cacheWrite: 0, // optional cacheRead: 0, // optional reasoning: 0, // optional ) ->duration(milliseconds: 1200) ->prompt('Summarize this article...') ->response('The article discusses...') ->status('completed') ->requestMeta(['temperature' => 0.7]) // optional arbitrary metadata ->responseMeta(['finish_reason' => 'stop']) ->log();
Attach to a model (polymorphic owner)
AiUsage::driver('openai') ->model('gpt-4o') ->tokens(300, 150) ->owner($user) // any Eloquent model ->log();
Override prices at log time
AiUsage::driver('openai') ->model('gpt-4o') ->tokens(300, 150) ->costPrices(['prompt' => 2.50, 'completion' => 10.00]) ->log();
If costPrices() is not called, prices are resolved automatically from config('ai-usage.prices').
How cost estimation works
Token prices (USD per 1 million tokens) are snapshotted from your config at log time and stored alongside each record. This means historical cost data remains accurate even when you update prices later.
The AiUsageLog model exposes an estimated_cost computed attribute derived from the stored token counts and prices:
$log = AiUsageLog::find(1); echo $log->estimated_cost; // e.g. 0.003450
Returns null when no price data is available (i.e., the model isn't listed in your prices config).
To add a new model, simply add its prices to config('ai-usage.prices.{driver}.{model}'). Supported price keys: prompt, completion, cache_read, cache_write, reasoning.
Status values
Logs use the AiUsageStatus enum: pending, processing, completed, failed.
Filament integration
Requires filament/filament. Register the plugin in your panel provider:
use BacktikCh\LaravelAiUsage\Filament\AiUsagePlugin; public function panel(Panel $panel): Panel { return $panel // ... ->plugin(AiUsagePlugin::make()); }
This registers:
AiUsageResource— browsable, filterable list of all log entries with a detail view.AiUsageStatsWidget— stats overview: total calls, prompt/completion tokens, average duration, failed count.
Summary widget (standalone)
AiUsageSummaryWidget is a full-width Livewire widget with a period selector and per-driver / per-model token breakdowns. Add it to any page:
use BacktikCh\LaravelAiUsage\Filament\Widgets\AiUsageSummaryWidget; protected function getHeaderWidgets(): array { return [AiUsageSummaryWidget::class]; }
Artisan Commands
Prune old records
php artisan ai-usage:prune --days=90
Mark stale records as failed
If an AI call throws an exception, the laravel/ai SDK never fires AgentPrompted, so the record stays in processing status indefinitely. Run this command on a schedule to mark those stale records as failed:
php artisan ai-usage:mark-stale-failed
The timeout is controlled by stale_timeout_minutes in your config (default: 60). You can also override it per-call:
php artisan ai-usage:mark-stale-failed --minutes=30
Add both commands to your scheduler in routes/console.php:
Schedule::command('ai-usage:mark-stale-failed')->hourly(); Schedule::command('ai-usage:prune')->daily();
Testing
composer test
License
MIT — see LICENSE.