Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

devdojo/usage

AI usage tracking for Laravel. Every AI call your app makes through the Laravel AI SDK is recorded automatically — provider, model, tokens, the prompt that was asked, what the call cost you, what you billed for it, and the margin between the two. An admin dashboard at /usage shows the whole ledger per call and per user.

Install it and tracking starts. No code changes required.

Requirements

  • PHP 8.3+
  • Laravel 12 or 13
  • laravel/ai ^0.9
  • ext-bcmath (money math never touches floats)

Install

composer require devdojo/usage
php artisan migrate

That's it — the package's event listeners register themselves and every agent prompt, stream, embedding, image, and transcription made through the AI SDK is recorded from the next call onward.

Publish the config when you want to change defaults:

php artisan vendor:publish --tag=usage-config

Tables are prefixed usage_ (usage_events, usage_meters, usage_rollups). Change table_prefix in the config before migrating if that collides with anything.

The fail-open guarantee

Tracking must never break the thing it measures. Every capture path is wrapped in a guard with three layers:

  1. The listener extracts a plain payload and hands it to a queued job, so recording never blocks or delays the AI response.
  2. If the queue is unreachable, the event is written inline instead of lost.
  3. If that write fails too, the failure is logged and swallowed.

An exception inside tracking is never re-thrown into your application. If the usage_events table is missing, the queue is down, and the database is on fire, your AI call still returns normally. This behavior is covered by tests (tests/Feature/FailOpenTest.php).

Queued writes use your default connection unless you point them elsewhere:

'queue' => [
    'connection' => env('USAGE_QUEUE_CONNECTION'),
    'queue' => env('USAGE_QUEUE'),
],

Prompts, responses, and privacy

By default the package stores the user's prompt in full, a 140-character preview for list views, a SHA-256 hash, and the response capped at 2,000 characters — enough to look at any event and know exactly what was being built.

Prompts can contain personal data, so all of it is yours to dial down:

'store_prompts' => true,       // true, 'hash', or false
'prompt_max_length' => null,   // null stores prompts in full
'prompt_preview_length' => 140,

'store_responses' => true,
'response_max_length' => 2000,
  • 'hash' keeps only the SHA-256 hash — identical prompts stay groupable, but nothing readable is stored.
  • false stores nothing about the prompt at all, not even the hash.
  • Anything that looks like a credential (OpenAI/Anthropic keys, GitHub and Slack tokens, AWS keys, bearer tokens, api_key=... assignments) is always stripped before storage. This is not configurable on purpose.

Cost

The AI SDK reports token counts but not cost, so cost comes from the pricing map in config/usage.php — rates per 1 million tokens, as strings, keyed by provider and model. Patterns support a trailing * and match top to bottom, so specific models go before broad families:

'pricing' => [
    'anthropic' => [
        'claude-sonnet-4*' => ['input' => '3.00', 'output' => '15.00', 'cache_read' => '0.30', 'cache_write' => '3.75'],
    ],
    'openai' => [
        'dall-e-3' => ['per_unit' => '0.04'], // priced per image, not per token
    ],
],

Worth knowing:

  • A model missing from the map records a null cost, not zero. Unknown is not free — the dashboard shows a dash and you know what to add.
  • Cached tokens are never double-billed. Providers disagree about whether cached tokens are included in prompt_tokens (OpenAI-style APIs) or reported separately (Anthropic, Gemini). The package normalizes this at capture, so the pricing math is uniform.
  • Shipped rates were correct when written, but provider prices drift — verify against your providers' pricing pages and override freely.
  • Cost, price, and the exact rate card used are snapshotted onto each event. Repricing the config later never rewrites history.

Revenue and margin

Tell the package what you bill and margin appears everywhere:

'prices' => [
    'per_million_tokens' => '30.00',
],

For finer control, set default_unit_price on a meter row — a meter's price beats the global one. No price configured means events record cost only and the dashboard simply shows no revenue.

Who the usage belongs to

Each event is attributed to a polymorphic billable. Resolution order:

  1. A billable forced by the TrackUsage middleware (below)
  2. A Usage::attributeTo($user, fn () => ...) scope around the call
  3. The agent's conversation participant (RemembersConversations agents)
  4. Your custom fallback: Usage::resolveBillableUsing(fn () => ...)
  5. The authenticated user

No match still records the event — as system usage with no billable.

Optional: richer capture per agent

Basic tracking needs nothing. For agents where you want more, add the middleware — never global, always opt-in:

use Devdojo\Usage\Middleware\TrackUsage;

class BuilderAgent implements Agent, HasMiddleware
{
    public function middleware(): array
    {
        return [new TrackUsage(meter: 'builder', metadata: ['feature' => 'builder'])];
    }
}

It can pin events to a meter, attach metadata, force a billable, and store the original prompt as written before other middleware revised it (put it first in the array for the truest original).

Manual recording

For AI calls made outside the SDK — a custom proxy, a raw HTTP call:

use Devdojo\Usage\Facades\Usage;

Usage::record([
    'provider' => 'anthropic',
    'model' => 'claude-sonnet-4-5',
    'input_tokens' => 1200,
    'output_tokens' => 480,
    'billable' => $user,
    'metadata' => ['source' => 'chat-proxy'],
]);

Manual records run through the same pricing and land in the same tables. Pass total_cost yourself and it is kept and marked cost_source: manual.

Rollups

Dashboard reads for closed days come from pre-aggregated usage_rollups, so they never scan raw events. Schedule the command:

Schedule::command('usage:rollup')->hourly();

It recomputes each closed day in a trailing window (default 3 days) and upserts, so it is idempotent and absorbs events that arrived late through the queue. Today is always read live from raw events. Use --days=30 or --since=2026-01-01 to reach further back.

The dashboard

/usage shows the call ledger (user, model, tokens, cost, prompt preview, time) and a Users view — one card per person with avatar, name, email, and their cost for the timeframe (All Time / 30 Days / Week / Day, defaulting to 30 days), plus billed amount and margin when you price usage.

It denies everyone by default. The package registers the viewUsage gate as deny-all; opening it up is an explicit act in your app:

Gate::define('viewUsage', fn (User $user) => $user->isAdmin());

Path, middleware, and the gate name are configurable:

'dashboard' => [
    'enabled' => true,
    'path' => 'usage',
    'middleware' => ['web', 'auth'],
],
'gate' => 'viewUsage',

The dashboard ships its own styles — no Tailwind build step, no asset publishing, works in any app, light and dark.

Good to know

  • Events are immutable. No updated_at, and the model throws if you try to update one. recorded_at is when the usage happened; created_at is when the row was written.
  • Idempotent. The SDK's invocation id doubles as an idempotency key, so a retried queue job can never double-record a call.
  • Meters are referenced by slug, never by foreign key — deleting a meter leaves history intact. Unknown slugs create their meter row on first use.
  • Turn everything off with USAGE_ENABLED=false. Existing data stays put.
  • Prepaid credits, balance grants, and invoicing are deliberately out of scope for v1 — events carry everything (immutable, snapshotted, idempotent) a credits layer needs to sit on top later.

Testing

composer test

The suite covers the fail-open guarantee end to end, cost and margin math, token normalization, privacy modes, idempotency, rollups, and dashboard authorization.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages