← Larameter 7 / 12

Prices and rates

Two tables, because there are two kinds of cost. An action has a price: creating a form, sending an email, verifying an identity. Consumption has a rate: tokens, minutes, pages.

Everything is expressed in credits, including the rates. What a credit is worth in money is your business and the package never asks.

Fixed prices

'prices' => [
    'create_form'      => 1,
    'create_poll'      => 1,
    'generate_report'  => 25,
    'verify_identity'  => 10,
],
$org->credits()->price('generate_report');   // 25
$org->credits()->charge('generate_report');

An action you never priced is free, not guessed at. A package should not invent a price for something you did not write down, and the alternative, refusing to charge for an unknown action, would mean a typo in an operation name stops the feature working rather than costing nothing.

That default has a consequence worth knowing: charge('typo_in_the_name') writes a usage row of zero credits. The row is still there, which is how you find the typo, and the observer skips the balance entirely for a zero charge.

An explicit amount overrides the table, for the case where the count is only known at the call site:

$price = $org->credits()->price('generate_document');

$org->credits()->charge('generate_document', credits: $price * $documentCount);

Metered rates

'rates' => [
    'gpt-4o'      => ['input' => 25_000, 'output' => 100_000],
    'gpt-4o-mini' => ['input' => 1_500,  'output' => 6_000],
    '*'           => ['input' => 50_000, 'output' => 150_000],
],

input and output are the credits for a million units in and a million out, which is how providers quote tokens, so the table reads the way the published price lists do. Anything not priced by the million is a fixed price and belongs in prices.

$org->credits()->meter('gpt-4o', 'token', $in, $out);

The operation is what gets priced, which for a model call is the model name. The unit is only a label, kept on the row so a bill can tell one kind of consumption from another.

A charge is ceil()ed and never less than one credit, so a call that consumed something always costs something.

Keys are indexed directly

Rates are looked up by array index, not through config() dot notation. A name with a dot in it, such as gpt-5.4, would otherwise be split into a nested path, miss, and silently fall through to the wildcard. That undercharges, and it only shows up on the provider's invoice.

This is the one place where the package deliberately does not use dot notation, and it is why credits_key may name a nested key while a rate key may not.

The wildcard, and the floor beneath it

'*' is the fallback for anything not listed. If there is no wildcard either:

'fallback_units_per_credit' => 100,

which prices an unknown model at one credit per hundred units. A metered unit you never priced still costs something, because the alternative is that metering an unknown model is free and the gap only surfaces on your provider's bill at the end of the month. That is the opposite default from fixed prices, and for the opposite reason: an unpriced action is a decision, an unpriced model is an oversight.

Pricing without charging

Both have a read-only sibling, for showing a cost before the fact or storing one after a client already charged it elsewhere:

$org->credits()->price('send_email');                      // 1
$org->credits()->meterPrice('gpt-4o', $in, $out);          // what it would cost

meterPrice() uses exactly the same rate resolution as meter(), including the wildcard and the fallback, so the number you show and the number you charge cannot drift.

Where to put the rate table

Rates change more often than anything else in this config, and they change for reasons outside the application. Keeping them in config/larameter.php is fine; keeping them in their own file and merging is also fine, since the package only ever reads config('larameter.rates').

What you should not do is derive them from the provider's dollar prices at runtime. A usage row records what it cost in credits at the time, and the whole point of storing that split is that a bill from last March still adds up next year.