AI costs and spend caps
An application that calls models through laravel/ai pays the provider per token, and the price differs per model and
per kind of token. The package prices each call and caps what calls may cost per day, so a runaway loop or an abusive
account stops at a limit rather than at the provider's invoice.
Pricing a call
CostCalculator::costOf() prices the tokens a call used at the rate of its model:
use Pushery\Billing\Ai\CostCalculator;
use Pushery\Billing\Ai\TokenUsage;
$response = $agent->prompt('…');
$cost = app(CostCalculator::class)->costOf('gpt-5.4-mini', TokenUsage::fromLaravelAi($response->usage));
$cost->micros; // what it cost, in millionths of the currency
$cost->toMoney(); // the same, rounded once to the currency's minor units, for a person to read
TokenUsage holds the five buckets a provider bills, with the meaning laravel/ai normalizes every provider to:
input is every input token, the ones read from and written to a prompt cache included, and output is every output
token, reasoning included. The calculator prices each bucket on its own: the input that touched no cache at input,
cache reads at cache_read, cache writes at cache_write, the output at output and the reasoning within it at
reasoning. Gemini reports its thought tokens beside the output and Anthropic its cache tokens beside the input;
laravel/ai folds both in, so each token is priced once.
A call is rounded once, after its buckets are summed. Costs are kept in millionths of the currency, because one call often costs a fraction of a cent, and a count in cents would never reach a cap.
The rate table
billing.ai.rates is the price list, per million tokens, as decimal strings:
'ai' => [
'currency' => 'USD',
'rates' => [
'gpt-5.4-mini' => [
['valid_from' => '2026-01-01', 'input' => '0.25', 'output' => '2.00', 'cache_read' => '0.025'],
['valid_from' => '2026-07-01', 'input' => '0.20', 'output' => '1.60', 'cache_read' => '0.02'],
],
'claude-haiku-4-5-20251001' => [
'input' => '1.00', 'output' => '5.00', 'cache_read' => '0.10', 'cache_write' => '1.25',
],
],
],
- A model id is read by array access, never through a dotted configuration key: ids carry dots, and
config('billing.ai.rates.gpt-5.4-mini')would read a keygpt-5with a child4-mini. - A price change keeps the price before it. A model holds one entry or a list with
valid_fromdates, and a call is priced at the entry of its own day, so a cost worked out again later comes to the same figure. cache_read,cache_writeandreasoningfall back to the input and the output price where a provider does not price them apart. An entry may name its owncurrency.- A model without a rate, or with none valid on the day, throws
InvalidBillingConfigrather than pricing the call at nothing.
Daily caps
SpendBrake::spend() counts a cost against the provider's account for the UTC day and, where an owner is given,
against that owner's, and refuses it when it would pass a cap:
use Pushery\Billing\Ai\SpendBrake;
if (! app(SpendBrake::class)->spend('openai', $cost, owner: $user)) {
// A cap refused it: nothing was counted. Pause the feature for the day, or tell the user.
}
billing.ai.daily_caps names a cap per provider, against the margin, and billing.ai.owner_daily_cap one for every
owner, against abuse, both as decimal strings in billing.ai.currency. The check and the count are one statement: an
account is raised only where the raise stays at or below its cap, so two calls at the cap cannot both pass it. A cost
the owner's cap refuses is not left on the provider's account either. Count what a call may cost before making it,
or what it cost afterwards, and the next call meets the cap. SpendBrake::spent() answers what an account has
counted on a day.
A provider without a cap, an owner when owner_daily_cap is null, and a cost in another currency are counted and
never refused, and a cost of nothing counts nothing. An owner's daily accounts go with the owner on an erasure.