PPact
Administration

AI budget

Per-tenant AI spend caps, cutoff modes, and call-rate limits that make catastrophic cost-drain impossible by construction.

AI budget

The AI budget is the strict cost gate every AI call passes before Pact invokes a model provider. It exists so that a prompt injection, a compromised session, or a runaway loop can never quietly drain your AI credits — anything over the cap is refused at the door, before the provider is called and before a ledger row is written.

The control lives at Admin → Security → AI budget and is backed by core.ai.budget, enforced from core.ai.client.AIClient via check_budget_or_raise(...) on every call.

This is real and enforced

Budget checks run inside the AI client for every feature — not as an advisory dashboard number. When a cap is breached the call raises AIBudgetExceededError and a row is written to ai_budget_blocks so you can see exactly what was refused and why.

The five layers

Checked in order on every call:

  1. Per-tenant monthly spend cap — the hard ceiling. Sums ai_usage.cost_cents for the current calendar month against tenant_ai_budget.monthly_cap_cents.
  2. Per-tenant daily spend cap — optional, scoped to today (UTC). Limits blast radius without lowering the monthly cap.
  3. Per-user daily spend sub-cap — optional. Stops one rogue user from draining the whole tenant pool.
  4. Per-tenant calls-per-minute — in-memory sliding window, default 60. Catches loops before they burn spend.
  5. Per-user calls-per-minute — default 20, within a tenant. A voice-relay utterance counts once here however many model calls it fans out into (router, classifier, answer, suggestions …): the limit reads "20 utterances a minute", which is what it was meant to catch — one user looping, not a conversation's own fan-out. Layer 4 still counts every call.

Layers 4 and 5 are intentionally in-memory (per replica) rather than Redis: the spend cap in the database is the strict gate, and the rate limit is a soft gate that catches runaway loops early. This avoids the failure mode where a Redis blip would let cost-drain succeed.

Cutoff modes

The monthly and daily caps share a cutoff_mode (VALID_CUTOFF_MODES):

  • hard_block (default) — refuse every call once the cap is hit.
  • soft_block — refuse the expensive tiers (default / complex) but keep allowing the cheap extract (Haiku) tier, so critical classification paths stay alive while a runaway elsewhere is investigated.
  • warn — log and write an audit event, but allow the call. For trusted internal tenants who only want telemetry.

Default budget

Tenants without an explicit tenant_ai_budget row inherit DEFAULT_BUDGET:

  • $20.00 / month (monthly_cap_cents = 2000) hard cap
  • No daily cap, no per-user sub-cap
  • 60 tenant calls/minute, 20 per user
  • alert_threshold_pct = 80 — surfaces an alert at 80% of the cap
  • hard_block cutoff

Admins raise caps or switch to warning-only mode by creating an explicit row.

API

All routes are admin/owner only and tenant-scoped from the auth context:

code
GET  /v1/admin/security/ai-budget          # config + live spend + EOM projection + Ask limits
PUT  /v1/admin/security/ai-budget          # upsert config
GET  /v1/admin/security/ai-budget/blocks   # recent block events

The GET also returns a linear end-of-month spend projection and a per-feature cost breakdown (from the ai_usage ledger). Every block is auditable via ai_budget_blocks (alembic 0149_tenant_ai_budget).

Filed under Security on purpose

Budget caps live under /admin/security, not a standalone billing page, because they cap the blast radius of prompt-injection and chat-jacking attacks. An admin doing a security review finds them alongside rate limits, network policy, and the audit log.

Ask limits

The Ask panel has three ceilings of its own (core.ask.guardrails), checked on every question in addition to the spend caps above. The Ask limits card on this page shows them as the limiter applies them:

  • Questions per workspace per day: a rolling 24-hour window. Pact's default is 500. A workspace can have its own limit (tenants.limits_json.ask_questions_per_day), which Pact sets on request. Workspace admins cannot change it here. If a stored value is out of range, the limiter ignores it and the card says so. "Look up" on an empty Ask answer draws from the same window and the same limit.
  • Per person per minute: 30, the same for every workspace.
  • Model spend per answer: 5¢, checked twice. Before any model runs, Ask estimates what the answer will cost from this workspace's recent model costs (the 90th percentile of the last 20 runs of each model step in 30 days, or a fixed budget per step until there are 5) and refuses a question estimated over 5¢. Nothing is spent on it, and the answer says what it would have cost and how to ask more narrowly. Questions Ask answers in SQL without a model are never refused for cost. An answer whose actual cost still comes in over 5¢ is not served. Both kinds of refusal show on the Ask trail as "Refused (cost)".

The card also shows how many questions were asked in the last 24 hours, read from the limiter's own window without using up a question. When Pact runs the in-process counter, each server keeps its own count, and the card says the number covers one server. A count that could not be read is shown as unread, never as 0. When the limit is reached, a new question gets a "limit reached" answer that says when a slot frees up.

Kill switch

The same admin surface exposes the AI kill switch (core.ai.kill_switch) — the outermost break-glass control. It can disable AI for a single tenant, and an auto-trigger engages it when suspicious activity spikes (3+ injection blocks in 5 minutes), writing an auto_engaged row to ai_kill_switch_events. Recovery is always manual. A burst of rate-limit refusals alerts but never disables the workspace: each refused call was already refused, spend is bounded by the caps above, and one voice call fans out into several AI calls, so a busy user must not be able to switch AI off for everyone. See AI defense for the full detection pipeline.

AI budget — Pact Docs · Pact