Skip to content
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 stateWhat the gateway answers
users.plan_id empty (no plan)403 {"error": "No plan assigned. Ask your administrator to assign one."}
plan whose cap is emptyserved, whatever the spend: the plan is unlimited
plan with a capserved 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.

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.

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.

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.

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:

StateWhat the gateway does
Under capserves
Near capserves; at least four fifths of the cap are consumed
Blocked: cap reachedrefuses with 402
Blocked: no planrefuses with 403
Unlimitedserves, 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 cap threshold 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.

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.

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.

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.