MAQPNADocs

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#

  1. Render. The budget reconciler turns every Agent's spec.budget, spec.model.{tokensPerMinute, maxTokensPerRequest} and spec.maxConcurrentSessions, and every cluster-scoped BudgetPolicy (including the tenant-<name> policies the tenant reconciler generates), into budgets.json in the ConfigMap maqpna-budgets: maps agents (<ns>/<agent>), namespaces and users ("*" is the default entry). When several policies set the same key, the smallest limit and the strictest onExceed win; mixed currencies are reported as CurrencyMixed.
  2. Load. The gateway reloads the file every policyReloadSeconds. An invalid document keeps the previous budgets. An entry in another currency than the gateway's budgetCurrency makes the gateway reject the whole document (posture check budget-currency fails); amounts are never converted.
  3. Meter. pkg/finops keeps lifetime totals plus day and month buckets per session, agent, namespace, user and total, keyed by calendar period in budgetTimezone (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.
  4. Check before the call. Before every MCP tools/call and every /llm call, the gateway first checks the legacy sessionBudgetUSD, then every applicable limit. Spend at or above a limit means exhausted.
  5. Act on exceed (onExceed, default deny; agent budgets always deny): - deny: MCP -32001 with reason: budget_exceeded:<scope>/<window> and rule budget/<scope>-<window>; model calls HTTP 402, type maqpna_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.
  6. Limit tokens per minute (model calls). maxTokensPerRequest clamps or sets max_tokens / max_completion_tokens (reason max_tokens_clamped:N). Then the call reserves its estimate (body bytes ÷ 4) in the current minute for the session and route. With stateBackend.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, type maqpna_rate_limited, code tokens_per_minute, Retry-After. After the call the reservation is settled with the reported usage. The gateway forces stream_options.include_usage=true on streaming requests so usage is always reported.
  7. Notify. Crossing an alertAtPercent level emits budget.threshold; reaching a limit emits budget.exceeded (whatever onExceed), 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.
  8. Persist. The meter is saved every finopsSaveSeconds (10) to finopsStatePath (or the shared blob finops) and on shutdown. On start, ledger records written after the save are replayed; without a state file, totals are rebuilt from the ledger's costUsd. With several replicas, each meters the others' spend from the mirrored ledger, about one second late.
  9. Cap concurrency at the operator. A session that has not been admitted stays Pending with condition QuotaExceeded while its agent (spec.maxConcurrentSessions) or namespace (BudgetPolicy maxConcurrentSessions) 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)