Usage
Attributing usage
Every laravel/ai call is recorded without any code. To know what the cost belongs to, give your agents a feature and link calls to the record they were made for.
Implement ProvidesAiUsageContext on an agent to set its feature. Without it, the agent's class name in snake_case is used:
use Illuminate\Database\Eloquent\Model;
use SpaanProductions\LaravelAiUsage\Contracts\ProvidesAiUsageContext;
class InvoiceReader implements Agent, ProvidesAiUsageContext
{
use Promptable;
public function aiUsageFeature(): string
{
return 'invoice_reading';
}
public function aiUsageSubject(): ?Model
{
return null;
}
}
Wrap a call in AiUsage::context() to link it to a subject, override the feature or user, or add metadata. Contexts can be nested, with the inner values winning, and the callback's return value is passed through:
use SpaanProductions\LaravelAiUsage\Facades\AiUsage;
$response = AiUsage::context(
fn () => (new InvoiceReader())->prompt($text),
subject: $invoice,
);
AiUsage::context(fn () => $this->importAll(), feature: 'bulk_import', user: $user, metadata: ['batch' => 7]);
The user defaults to the authenticated user. In queued jobs there is none, so pass it to the context when it matters.
Add the HasAiUsage trait to a subject model to get its usage and cost:
use SpaanProductions\LaravelAiUsage\Concerns\HasAiUsage;
class Invoice extends Model
{
use HasAiUsage;
}
$invoice->aiUsage; // AiUsageRecord collection
$invoice->aiUsageCost(); // total cost
Invoice::query()->withSum('aiUsage', 'total_cost')->get(); // no N+1 in lists
If the subject has an aiUsageUrl(): ?string method, the record detail page links to it.
To skip recording for a block of code, such as health checks, wrap it in AiUsage::withoutRecording():
AiUsage::withoutRecording(fn () => (new HealthCheckAgent())->prompt('ping'));
Execution traces
Every row is linked to its trace: trace_id is the invocation id of the root call, and a sub-agent (or any call made inside a tool) stores the parent_invocation_id and parent_tool_invocation_id it was delegated from. This needs laravel/ai 0.11 or later; on older versions every call is its own trace. The record page shows the rows of one trace as a tree with timing bars.
Turn on AI_USAGE_TRACE=true to also store the full execution trace on each agent row (laravel/ai 1.x): the instructions, input and output, every model step with its tokens, cost and text, and every tool call with its arguments, result and duration. The record page shows it as a waterfall like a tracing tool, and each span opens its details. Each string is truncated to trace.content_limit (by default content_limit). Traces can contain document or mail contents, so values under sensitive keys (*_token, password, api_key, …) are masked by redaction, which is on by default. Add regex patterns for values in free text (e.g. e-mail addresses) and set a retention_days.
With AI_USAGE_TRACE_ATTACHMENTS=true, files attached to a prompt are copied to trace.disk (default local) under trace.path/{date}/{invocation}/ and can be downloaded from the record page. Remote and provider files are only described, never downloaded. The folders are deleted by model:prune together with the rows.
To keep the rows (tokens and cost) longer than their traces, set trace_retention_days: model:prune then clears the trace of older rows and deletes their attachments. attachment_retention_days deletes only the attachment files, and content_retention_days clears the stored prompt and response. Attachments are kept for the shortest of the row, trace and attachment retention.
Failed calls get an error_kind (rate_limited, overloaded, connection, credits, tool_error or exception), which the record list can filter on.
Pricing
Costs are calculated when a call is recorded, using the prices (per 1M tokens) of the matching row in ai_models. Input, cache-read, cache-write and output tokens are each charged at their own rate. The token counts are read the same way from laravel/ai 0.x and 1.x, even though 1.x includes the cached tokens in inputTokens. The rates used are stored on the record. A model is matched on provider and model id, then on the id without a date or -latest suffix, then on its OpenRouter id. Calls to a model without a price are recorded with a cost of 0 and flagged as "without pricing".
After the prices change, or once a missing model has been synced, recalculate the old rows:
php artisan ai-usage:reprice --missing-only --dry-run
php artisan ai-usage:reprice --missing-only
php artisan ai-usage:reprice --from=2026-10-01 --to=2026-10-31
Budgets
'budgets' => [
'monthly' => [
'period' => 'month', // day, week or month
'limit' => 50, // in the configured currency
'thresholds' => [0.8, 1.0], // fractions of the limit
'feature' => null, // or limit the budget to one feature
'mail' => ['admin@example.com'],
],
],
When the spend in a period crosses a threshold, BudgetThresholdReached fires once for that budget, period and threshold, and the addresses are mailed. AiUsageRecorded fires after every recorded call.
Pages
| Route | Path | |
|---|---|---|
ai-usage.dashboard |
/ai-usage |
Totals, a cost, tokens or requests chart and breakdowns by model, feature, provider and subject. |
ai-usage.records.index |
/ai-usage/records |
Filterable, sortable list with CSV export. |
ai-usage.records.show |
/ai-usage/records/{record} |
Tokens, cost, price snapshot, subject, metadata, content and execution trace of one call. |
ai-usage.models.index |
/ai-usage/models |
The synced models with their modalities, context and prices, and a button that runs ai-usage:sync-models. |
The pages use a standalone layout with the package's own stylesheet. To show them in your application's layout instead, set layout to your layout component (e.g. layouts::app). Then add the package views to your Tailwind sources:
@source "../../vendor/spaanproductions/laravel-ai-usage/resources/views";
To change the views, publish them. To serve the stylesheet as a static file instead of through a route, publish the assets:
php artisan vendor:publish --tag=ai-usage-views
php artisan vendor:publish --tag=ai-usage-assets
Een fout gevonden of een vraag? Maak een issue aan op GitHub.