Budget enforcement#
A budget is a spending limit for an agent, a namespace, a user or a tenant, over a session, a day or a month. Budgets are declared in Kubernetes and enforced by the MAQPNA gateway before every Model Context Protocol (MCP) tool call and every model call. Concurrency limits are enforced earlier, by the operator, before a session gets a sandbox. Code: internal/controller/budget_controller.go, cmd/maqpna-gateway/budgets.go, pkg/finops.
Where budgets come from#
flowchart LR
A["Agent spec.budget<br/>perSessionUSD, perDayUSD, perMonthUSD"] --> R
AM["Agent spec.model<br/>tokensPerMinute, maxTokensPerRequest"] --> R
AC["Agent spec.maxConcurrentSessions"] --> R
BP["BudgetPolicy (cluster)<br/>namespaces, users, onExceed, alertAtPercent"] --> R
TN["Tenant quotas"] -- "generates BudgetPolicy tenant-name" --> BP
R["Budget reconciler<br/>smallest limit, strictest onExceed"] --> CM["ConfigMap maqpna-budgets<br/>budgets.json"]
CM -- "configSync or volume" --> GW["Gateway: budgetsFile"]
AC --> OP["AgentSession reconciler:<br/>concurrency quota"]
Sequence#
sequenceDiagram
autonumber
participant Ag as Agent
participant GW as Gateway
participant M as FinOps meter
participant C as Counters (shared or local)
participant Up as Model or MCP server
participant N as Notifiers
participant L as Audit ledger
Ag->>GW: POST /llm/team-a.coder/v1/chat/completions
GW->>M: spend so far vs every limit (agent, namespace, user x session, day, month)
M-->>GW: within budget
GW->>GW: clamp max_tokens to maxTokensPerRequest
GW->>C: reserve estimated tokens in this minute (tokensPerMinute)
C-->>GW: reserved
GW->>Up: forward
Up-->>GW: completion with usage
GW->>C: settle with reported usage
GW->>M: RecordModelCall (prompt, completion tokens, cost)
M-->>N: budget.threshold at 80 percent (once per limit, period, level)
GW->>L: allow, costUsd, usage prompt and completion
Ag->>GW: next tool call
GW->>M: check
M-->>GW: namespace day limit reached
GW->>L: deny, rule budget/namespace-day
GW-->>Ag: -32001 budget_exceeded:namespace/day
Step by step#
- Render. The budget reconciler turns every Agent's
spec.budget,spec.model.{tokensPerMinute, maxTokensPerRequest}andspec.maxConcurrentSessions, and every cluster-scopedBudgetPolicy(including thetenant-<name>policies the tenant reconciler generates), intobudgets.jsonin the ConfigMapmaqpna-budgets: mapsagents(<ns>/<agent>),namespacesandusers("*"is the default entry). When several policies set the same key, the smallest limit and the strictestonExceedwin; mixed currencies are reported asCurrencyMixed. - Load. The gateway reloads the file every
policyReloadSeconds. An invalid document keeps the previous budgets. An entry in another currency than the gateway'sbudgetCurrencymakes the gateway reject the whole document (posture checkbudget-currencyfails); amounts are never converted. - Meter.
pkg/finopskeeps lifetime totals plusdayandmonthbuckets per session, agent, namespace, user and total, keyed by calendar period inbudgetTimezone(default UTC). It counts the price of tool calls (pricingFile:defaultToolCallUSD, per-tool globs), model tokens (per-model input and output prices) and sandbox time reported by the operator. A fork's spend also rolls up to its lineage root, so N forks share one per-session budget. - Check before the call. Before every MCP
tools/calland every/llmcall, the gateway first checks the legacysessionBudgetUSD, then every applicable limit. Spend at or above a limit means exhausted. - Act on exceed (
onExceed, defaultdeny; agent budgets always deny): -deny: MCP-32001withreason: budget_exceeded:<scope>/<window>and rulebudget/<scope>-<window>; model calls HTTP 402, typemaqpna_budget_exceeded; -require_approval: the MCP call is held for a human (approval flow); model calls are denied, as they cannot wait; -alert: the call is allowed and a notification is sent. - Limit tokens per minute (model calls).
maxTokensPerRequestclamps or setsmax_tokens/max_completion_tokens(reasonmax_tokens_clamped:N). Then the call reserves its estimate (body bytes ÷ 4) in the current minute for the session and route. WithstateBackend.counters: shared(Postgres) the window is cluster-wide; otherwise a local token bucket is used, and it is also the fallback when the database cannot be reached. Over the limit: HTTP 429, typemaqpna_rate_limited, codetokens_per_minute,Retry-After. After the call the reservation is settled with the reported usage. The gateway forcesstream_options.include_usage=trueon streaming requests so usage is always reported. - Notify. Crossing an
alertAtPercentlevel emitsbudget.threshold; reaching a limit emitsbudget.exceeded(whateveronExceed), once per limit, period and level per replica, through the notifier outbox. Metrics:maqpna_gateway_budget_alerts_total{scope,percent},maqpna_gateway_budget_denied_total{scope},maqpna_gateway_rate_limited_total. - Persist. The meter is saved every
finopsSaveSeconds(10) tofinopsStatePath(or the shared blobfinops) and on shutdown. On start, ledger records written after the save are replayed; without a state file, totals are rebuilt from the ledger'scostUsd. With several replicas, each meters the others' spend from the mirrored ledger, about one second late. - Cap concurrency at the operator. A session that has not been admitted stays
Pendingwith conditionQuotaExceededwhile its agent (spec.maxConcurrentSessions) or namespace (BudgetPolicymaxConcurrentSessions) is at its limit. Queued sessions are admitted oldest first, re-checked every 5 seconds and as soon as a session of the namespace ends.
What you see#
maqpna budgets list reads GET /v1/budgets and prints one row per limit and window (columns from cmd/maqpna/admin_finops.go; values illustrative):
SCOPE KEY WINDOW PERIOD SPENT USD LIMIT USD EXCEEDED
agent team-a/coder day 2026-10-02 7.42 10 false
namespace team-a day 2026-10-02 200.10 200 true
maqpna costs summary --session fix-4821 prints session fix-4821: $<total> (tools …, models …, sandbox …); … and the budget line. maqpna costs focus exports FOCUS, the FinOps cost-export format. See maqpna budgets list, maqpna costs summary and maqpna costs focus.
Failure modes and limits#
| Situation | Behaviour |
|---|---|
Invalid budgets.json |
Previous budgets stay in force; reload error reported (LAST RELOAD ERROR in budgets list) |
| Shared counters unreachable | Local token bucket per replica (the request path never fails on a counter) |
| Burst spread over replicas | Can overshoot a USD budget by about one second of spend |
maxCallsPerMinute (policy rules) |
Still counted per replica |
| Tenant-aggregate budgets | Not implemented; tenant quotas apply per namespace (Planned) |