phoenix1331/laravel-ai-headroom-ledger
Laravel AI Headroom Ledger provides an auditable record of AI token usage and remaining headroom, helping you track consumption, enforce budgets, and monitor provider limits through simple Laravel-friendly APIs.
php artisan migrate. The ledger tables and daily rollups must exist before any query returns data; if entries() comes back empty on a fresh install, check migrations ran before debugging the query.Ledger facade, LedgerQuery, GroupedQuery, and the enums EntrySort / CountMethod. The README's warning block about Ledger::find() / Ledger::run() being unscoped is the single most important paragraph to read before writing lookups.Ledger::query()->orderBy(EntrySort::CountedAt)->paginate(25) gives you newest-first-by-default paging with no extra work, then layer filters as the UI grows.Ledger::query()->orderBy(EntrySort::Cost) for the top-spend list, and Ledger::grouped()->byModel()->accuracy() (or per day) for the "how good are our estimates" panel.orderBy(EntrySort $sort, bool $descending = true) replaces it. Treat sort as part of the query state object you pass around your controller/service, not something you sprinkle per-call.paginate($perPage, $page) returns a readonly LedgerPage with entries, total, page, perPage, and the helpers lastPage(), hasMorePages(), isEmpty(). The readonly value object maps cleanly onto a JSON resource: emit entries plus a meta block built from the rest, don't try to hand it to Laravel's paginate() view helpers.Ledger::query()->forTenant($id)->find($requestId) and ...->run($invocationId). Both return null unless the record matches every filter, so a wrong-tenant ID 404s naturally instead of leaking existence. Never call the unscoped Ledger::find() / Ledger::run() from a request-scoped controller.forCountMethod(CountMethod ...$methods), date ranges, tenant/agent/tag, and whatever else your version exposes) are answered from daily rollups, so they're cheap and stack with orderBy() and paginate(). Build a single LedgerQuery in a form request → service → query pipeline rather than branching per filter in the controller.GroupedQuery::accuracy() returns a Breakdown of AccuracyStats, which is Arrayable — so it drops straight into a Blade table loop or ->toArray() for a chart endpoint. Combine with byDay() and a date range for a trend line; days without samples are still present and empty, so the chart has no gaps and you don't need to backfill client-side.orderBy(EntrySort::PercentUsed, false) is not the reverse of the ascending order — unsampled rows float to the end either way. If you build a "click to flip direction" table, don't assume symmetry, and don't use array_reverse() on the entries.Cost and InputTokens fall back to the estimate when no actual figure exists. A cost-sorted list mixes measured and estimated spend without flagging it — join against the estimate-accuracy view (or filter to actuals) if the distinction matters.Cost, otherwise a cheap-in-JPY request can outrank an expensive-in-USD one on raw numbers.find()/run() are unchanged — only the docblocks now warn. Any existing code calling Ledger::find() in a tenant context is a latent IDOR; grep for it after upgrading.total. So isEmpty() on a deep page does not mean "no results" — check total before showing a "nothing here" empty state.byDay() + range (rollup-friendly) versus a high-cardinality grouping.EntrySort and CountMethod enums are the natural place to hang your own label/icon mapping (a small match in a presenter) rather than in Blade; LedgerPage and AccuracyStats being readonly/Arrayable makes them safe to pass into resources and jobs.How can I help you explore Laravel packages today?