Skip to content

Budgets and Spending

See also: Budgets, Data Relationships, Pricing Catalog and Accounting, Request Lifecycle and Failure Modes, Identity and Access, Service Accounts, Admin Control Plane

This page is the developer/operator contract for spend accounting and budget enforcement. Product-facing setup guidance lives in Budgets.

Source of Truth

Ledger Contract

usage_cost_events is the canonical usage and spend ledger.

  • request accounting stores at most one row per (request_id, ownership_scope_key)
  • ownership_scope_key uses user:<user_id> or service_account:<service_account_id>
  • pricing is resolved from the internal pricing catalog and persisted into the ledger row
  • spend math uses fixed-point money and integer arithmetic
  • team_id remains reporting metadata for service-account rows, not a spend principal

Pricing states are explicit:

  • priced
  • legacy_estimated
  • unpriced
  • usage_missing

Only priced and legacy_estimated rows count toward budget windows and spend totals. unpriced and usage_missing rows remain report-visible accounting-quality signals.

Embeddings Ledger Behavior

Embedding requests use the same ledger and budget enforcement path as chat and Responses requests.

For native Vertex text embeddings:

  • successful priced rows record operation: embeddings in request-log metadata and a normal usage_cost_events row for the authenticated owner
  • Vertex statistics.token_count (:predict) or usageMetadata.promptTokenCount (google/gemini-embedding-2 :embedContent) is normalized into prompt/input token usage
  • completion/output tokens are zero or absent for pricing purposes
  • the gateway must not convert characters or bytes into token counts
  • priced rows count toward the corresponding budget scope for the authenticated owner: user budget, service-account budget, and any matching user-model budget
  • usage_missing rows are written when Vertex does not return usable token counts; they remain visible but do not count toward budget windows
  • unpriced rows are written when usage exists but no exact pricing row can be resolved; they remain visible but do not count toward budget windows

The Vertex provider's Google Cloud auth.mode: service_account credential is not an ownership scope. Budget ownership still comes from the gateway API key: user:<user_id> for human callers or service_account:<service_account_id> for gateway service-account callers.

Duplicate request-id handling is unchanged for embeddings. A repeated (request_id, ownership_scope_key) is rejected before another ledger write or another round of budget math.

Budget Scopes

Budgets are stored in the generic budgets table. scope_key is canonical and has one active budget at a time.

Supported active scope keys:

  • budget:v1:user:<user_id>
  • budget:v1:service_account:<service_account_id>
  • budget:v1:user:<user_id>:model:<model_id>
  • budget:v1:user:<user_id>:upstream_model:<trimmed_upstream_model>

Supported scope_kind values:

  • user
  • service_account
  • user_model

Team budget scopes do not exist. Migration-only references to historical team budget tables are confined to migration SQL.

Budget Sources

Budget rows carry source metadata:

  • manual: admin UI/API-created budgets and admin UI/API deactivation tombstones
  • config_user_override: explicit users[*].budget entries
  • config_user_default: inherited budgets.users.default rows
  • config_user_model_default: inherited budgets.users.model_defaults rows

Source metadata is exposed in v1 admin spend API responses as budget_source. It lets the admin UI distinguish inherited rows from manual rows without guessing from scope or amount.

Manual rows are precedence boundaries. Default reconciliation does not overwrite an active manual budget. Deactivation writes a manual inactive tombstone so absence continues to inherit by default, while admin/API deactivation remains the explicit escape hatch.

Enforcement Order

Budget checks run after authentication, model access evaluation, and model resolution. API-key grants, principal-centric allowlists, and model-level allowlists can deny a request before the budget guard runs; those denials do not consume budget windows.

For allowed requests, budget checks run before provider execution, then again after provider execution when actual usage and pricing are known.

Human user traffic evaluates:

  1. user model budget, when a matching model scope applies
  2. user budget

User model matching uses the resolved gateway model_id when present. The exact, trim-only upstream model fallback is used only when model_id is absent.

Service-account traffic evaluates only:

  1. service-account budget

Service-account credentials cannot authenticate unless their service account is active and has an active service-account budget.

Hard And Soft Limits

Hard-limit behavior:

  • pre-provider rejection returns 429 budget_exceeded
  • no provider call occurs on the pre-provider rejection path
  • post-provider rejection happens before inserting a new priced ledger row
  • duplicate request ids are rejected with 400 invalid_request before additional budget math or ledger writes

Soft budgets never reject. They still contribute to alert readiness and reporting.

Concurrency caveat: hard-limit enforcement is best effort under concurrent requests and can overshoot. Reservations are intentionally out of scope.

Windows

Budget windows use UTC today:

  • daily windows start at 00:00:00 UTC
  • weekly windows start at Monday 00:00:00 UTC
  • monthly windows start at 00:00:00 UTC on the first day of the month

timezone is stored on budget settings for future display/window work, but live enforcement still uses UTC.

Alerts

Budget alerts are durable-first:

  • alert records are stored in budget_alerts
  • delivery attempts are stored in budget_alert_deliveries
  • email is the only live channel today
  • the threshold is fixed at 20% remaining budget
  • alert creation runs after a chargeable ledger row and after a budget upsert when current spend is already at or below the threshold

Recipients:

  • user budgets notify the user email
  • user model budgets notify the user email
  • service-account budgets notify active owners or admins of the owning team

Admin APIs

Live admin spend APIs:

  • GET /api/v1/admin/spend/report
  • GET /api/v1/admin/spend/focus.csv
  • GET /api/v1/me/spend/focus.csv
  • GET /api/v1/admin/spend/budgets
  • PUT /api/v1/admin/spend/budgets
  • POST /api/v1/admin/spend/budgets/deactivate
  • GET /api/v1/admin/spend/budget-alerts

Budget mutation requests use typed scope objects. Responses include:

  • budget_id
  • typed scope
  • computed scope_key
  • settings
  • source metadata
  • current-window spend
  • alert readiness

Old user, team, and service-account path-specific budget routes are not part of the runtime contract.

Reporting And FOCUS Export

Spend report and FOCUS owner filters support:

  • all
  • user
  • service_account

team is rejected. Service-account rows can still include owning team metadata and tags for reporting context.

FOCUS exports aggregate one row per UTC day, owner scope, upstream provider/model, and pricing status for priced and legacy_estimated ledger rows. unpriced and usage_missing rows are excluded from charge rows and reported through response headers.

Declarative Seed

Config-seeded API keys must declare the service account they create or reconcile:

  • service-account key
  • service-account name
  • owning team
  • active service-account budget
  • model grants

There is no implicit singleton service account, no reserved system-legacy team, and no team-owned runtime key fallback.

Human budget defaults are reconciled after ordinary config seeding:

  • budgets.users.default applies a user budget to every human user unless a manual budget or manual deactivation exists for that scope
  • budgets.users.model_defaults applies a user model budget to every human user for each listed gateway model unless a manual budget or manual deactivation exists for that user/model scope
  • users[*].budget creates a config-owned per-user override for listed users
  • omitting users[*].budget is a no-op for that user's budget; inheritance and manual/API state decide the active row
  • removing a config default deactivates active rows still owned by that config default source

Runtime user creation paths also apply the same defaults for bootstrap admins, admin-created users, and JIT OIDC/OAuth users.

Validation

When changing this area, run:

bash
mise run admin-contract-generate
mise run admin-contract-check
mise -C docs run check
mise run lint