Version 1.0.0
Cap a customer's spending
Plans, the monthly cap, and the two refusals a caller gets when they don't pass.
A team consumes models at your expense, and you want to bound what it can spend in a month. This page describes the plan, its cap, and what the caller gets when they go over it.
The three account states and the two refusals
Section titled “The three account states and the two refusals”A plan is the billing arrangement under which the gateway resells access. plans.max_cost_euro is the
customer’s monthly cap, in euros.
| Account state | What the gateway answers |
|---|---|
users.plan_id empty (no plan) | 403 {"error": "No plan assigned. Ask your administrator to assign one."} |
| plan whose cap is empty | served, whatever the spend: the plan is unlimited |
| plan with a cap | served as long as the month’s spend is under the cap, 402 {"error": "Plan limit reached"} beyond that |
The two codes differ because they are not raised by the same people.
402 says “the holder has spent their plan”: a payment, or the next month, clears it, and
it is the account holder who acts.
403 says “there is no plan to spend”: no payment can be charged against it, the next
month changes nothing, and only the operator clears it, by assigning a plan from the
console.
A third 403 exists, with a different meaning, {"error": "Endpoint \"<nom>\" is disabled."}, when the target endpoint exists but
has been disabled. It is returned before the quota check: a disabled endpoint consumes
nothing.
Create a plan
Section titled “Create a plan”Plans tab, Add plan form: a name, and a monthly cap in euros. The name is unique;
the console refuses a name already taken with 409.
Leaving the cap field empty creates an unlimited plan. That is a value distinct from
zero, and a situation distinct from “this account has no plan”: the unlimited plan always
passes, the account without a plan never passes. The 011-plans-plafond-illimite migration distinguishes the two
in the database: an empty cap is a null value there, not zero.
The administrator plan, seeded by that same migration, is unlimited. The gateway assigns it to the
admin account at startup if that account does not already have a plan: a plan you gave it,
a capped one for instance, is not overwritten by a restart.
Assign a plan, or remove it
Section titled “Assign a plan, or remove it”Users tab, selector in the Plan column on the account’s row. The change takes effect on the next request.
The No plan value in that same selector removes the account’s plan: this is the immediate blocking move. A Blocked badge appears on the row.
Raise or lower a cap
Section titled “Raise or lower a cap”Plans tab, inline editing. The change applies to the plan, so to every account that carries it.
Deleting a plan is refused with 409 as long as accounts carry it; the message asks you
to reassign them first. Deleting a plan in use would block, all at once, every account
that depends on it.
See an overrun coming
Section titled “See an overrun coming”Caps tab. It lists every account with what each one has spent this calendar month, its plan’s cap, and the share of that cap already consumed. Capped accounts come first, from the closest to its cap to the furthest; then accounts on an unlimited plan; then accounts without a plan.
This is the only screen that puts the two numbers side by side. Each account there carries a state spelled out in full:
| State | What the gateway does |
|---|---|
Under cap | serves |
Near cap | serves; at least four fifths of the cap are consumed |
Blocked: cap reached | refuses with 402 |
Blocked: no plan | refuses with 403 |
Unlimited | serves, whatever the spend |
Two things to know before you show this screen to anyone:
- the amounts are a floor, not a total. A call that nothing could price enters the sum for zero euros; the screen shows a banner saying how many there were this month. The causes are detailed below;
- the
Near capthreshold is set at four fifths of the cap. It is not adjustable.
The Usage tab knows nothing about caps: it answers “what was consumed, by whom, on which models”, over a period you choose. See Read consumption in the console.
What the gateway counts
Section titled “What the gateway counts”The cap is not a periodic estimate: the month’s spend is recomputed on every relayed
request, by aggregating over request_logs joined to the per-token prices in the models table.
- The window is the current calendar month:
timestamp >= date_trunc('month', NOW()). It resets to zero on the first of the month, not thirty days after the first request. - The comparison is
>=: at exactly the cap, refusal begins. - The price comes from the models, in nano-euros per input token and per output token. Those columns, and only those, feed the cost.
The request is not executed for an account without a plan: there is no cap to compare against, so no total to compute.
What keeps a cap from holding
Section titled “What keeps a cap from holding”Check these before you promise a customer a cap. Each point below makes real consumption not count, or not count yet, toward the total.
A model without prices counts as zero. Per-token prices are 0 by default when a
model is created. A model that is served and left at zero raises no counter, so it fires
no cap. Fill in the prices at the same time as the model.
A stream in progress is not counted yet. On a streaming response, the request_logs row is
only written at the end of the stream. In the meantime, the consumption exists and is
invisible to the quota check.
A stream without token counters counts as zero. On the OpenAI-format completion
paths, the gateway itself asks the provider for the consumption summary (stream_options.include_usage). If the
provider does not serve it, the row is written without counters and weighs zero against
the cap; the banner on the Caps tab counts those calls.
A billing row refused by the database leaves a trace. On a non-streamed response, the
response is held back and the caller gets a refusal: nothing is served without being
counted. On a streamed response, the caller has already received their stream; the row is
kept in memory in a retry queue and rewritten when the database answers (500 rows at
most, 20 attempts per row). A gateway restart loses whatever is waiting in that queue.
Each deferred row leaves a [BILLING] trace on the gateway’s error output.
The request that crosses the cap is served in full. The check compares the spend already recorded before relaying; it does not guess the cost of the request under way. The overrun is observed after the fact, by at least one call.
Caps entered before the 007 migration may have been rounded. The column was an
integer: 12,50 € was stored as 13 €, without error. The migration fixes the type but does
not restore the lost decimals. On an older database, review the caps with cents and
re-enter them.
See where a customer stands
Section titled “See where a customer stands”The Caps tab puts each account’s monthly spend against the cap that will block it (see above). The Usage tab gives costs by period, filterable by account and by model; the Plans tab gives the caps.
The gateway log also prints the reconciliation on every relayed request, in the form
[COST] <compte>: €<dépensé> used / €<plafond> limit, or [COST] <compte>: €<dépensé> used (plan has no cap) for an unlimited plan.