Expand description
AI token budgets (DW-078): tokens/min and cost/day per consumer or per policy-shared team.
A budget is a LIMIT OF TOTALS, not a rate: it caps how many provider-reported tokens (or how much priced cost) one budget holder may consume inside a window. It composes with — never replaces — the request-count limiter (DW-017) and the request quotas (DW-033): those count REQUESTS, this counts TOKENS.
§Check-then-spend (the locked no-estimation decision)
The gateway never estimates a request’s cost before the provider answers, so enforcement is two-phase:
- Pre-check (before any provider contact): a holder whose
window is ALREADY exhausted is rejected with 429
ai_budget_exceededand aRetry-Afterto the window boundary. - Spend (after usage is known): the provider-REPORTED usage is added to the window — non-streaming from the translated response, streaming from the accumulated DW-077 usage events.
The contract is therefore: reject when the window is exhausted, spend what the provider reports. Overrun within one request is bounded by that one request’s usage — a holder at the limit can always complete one more request, and the next pre-check rejects.
§Mid-stream cutoff
While a stream is being forwarded, the provider’s usage report is
spent as it GROWS — each reported token exactly once — and checked
against the window after every batch. When a dialect reports usage
EARLY (Anthropic carries input tokens in message_start), a
crossing is detected mid-stream: forwarding stops, the client
receives the documented ai_budget_exceeded SSE event and the
terminator, and the provider body is dropped AT ONCE — the upstream
cancel propagates immediately, so even a stalled client cannot keep
the provider generating. Dialects that
only report usage at the END (OpenAI without include_usage would
be one; the gateway forces include_usage, whose chunk is terminal)
cannot cut off mid-stream: their usage is spent at stream end and
enforced by the next pre-check. Documented, honest boundary.
§Windows and keys
tokens_per_min is a fixed 60-second window aligned to the epoch
minute (deterministic in tests; Retry-After counts to the next
boundary). cost_per_day_micros is a UTC calendar day — the same
window shape as the DW-033 quotas. Cost is integer MICRO-USD (no
floating-point money). The budget KEY is the consumer name
(scope: consumer) or, for a shared team budget, the POLICY name
(scope: policy) — every consumer attaching that policy spends
from one ledger entry.
§Pricing seam (DW-079)
Cost enforcement is wired end to end but reads prices through
cost_micros, which returns 0 until the pricing tables land in
DW-079 — enforced-but-inert, the honest seam.
§Lifetime
The ledger (windows) lives on the dataplane and SURVIVES config
reloads (a reload must not reset a 60-second window or a daily
spend); only the limits resolve per generation. A reload PRUNES the
keys the new generation can no longer derive (a removed or renamed
holder’s windows) — carried-forward state is bounded by the config,
not the deployment’s history. Unbudgeted consumers (no policy with
a token_budget binds them) are unlimited — fail-open, the DW-017
default posture. An ANONYMOUS request (a route without
auth_required) cannot bind a consumer-scoped budget — the walk
falls through to the next candidate, so a less-specific
policy-scoped (team) budget still binds rather than silently
unlimiting the request.
Structs§
- AiBudget
Engine - The per-generation budget rules plus the reload-surviving ledger.
- Budget
Guard - The resolved budget for one request: a live guard over the ledger.
- Budget
Ledger - The shared spend ledger: one entry per (budget key, window kind). A single mutex guards the map — AI request rates are orders of magnitude below the GCRA path’s sharding needs, and the critical section is two integer adds (the DW-033 store pays an fsync per request; this pays none).
Enums§
- Budget
Kind - Which budget window denied (the
kindlabel ofdwara_ai_budget_denied_total). - Budget
Verdict - The pre-check verdict.
Functions§
- consumer_
policies_ of - Resolve a consumer’s attached policies from the config (by name).
- cost_
micros - The DW-079 pricing seam: micro-USD for one provider-model call
with this usage. Reads prices through a DEFAULT (empty) pricing
table, so this free function always returns 0 (the unknown-model
allowpolicy) — it exists for test/backward-compat call sites that do not have a dataplane handle. The live path uses the dataplane’s compiledPricingTable(stored on the DataPlane as an ArcSwap, refreshed per generation). - listener_
policies_ of - The listener’s policy list (by name).
- service_
policies_ of - The route’s service’s policy list (empty when the service is missing — validation rejects that elsewhere).