Skip to content

m12-pr08 P0-4: per-org daily cost cap + cascade breaker#26

Open
BalaShankar9 wants to merge 1 commit into
m12-pr07-cost-attributionfrom
m12-pr08-cost-cap
Open

m12-pr08 P0-4: per-org daily cost cap + cascade breaker#26
BalaShankar9 wants to merge 1 commit into
m12-pr07-cost-attributionfrom
m12-pr08-cost-cap

Conversation

@BalaShankar9

Copy link
Copy Markdown
Owner

P0-4 from the World Class Architecture Blueprint. Wires the spend telemetry shipped in #25 into a hard cascade breaker that refuses LLM calls before any provider tier is touched once an org has spent past its configured daily $ cap.

What ships

ai_engine/cost_breaker.py — new module:

  • OrgDailyCostCapExceeded exception (tenant_id, spent_cents, cap_cents).
  • set_tenant_id / reset_tenant_id / get_tenant_id / async tenant_scope ContextVar helpers — single canonical place to bind tenant identity for downstream LLM calls.
  • cap_cents_for(tenant_id) reads ORG_DAILY_COST_CAP_USD (global) and ORG_DAILY_COST_CAP_OVERRIDES (per-tenant JSON map). 0 / unset / parse error all mean "cap disabled" (fail-open).
  • enforce_org_daily_cost_cap() — async; queries CostAttributionService.get_org_cost_today_cents (FRESH base-table read, not the 60s MV) and raises when spent_cents >= cap_cents. Emits cost.cap.tripped on the bound emitter (best-effort). Fail-open on cost-service errors.

ai_engine/client.py — 5 hooks:

  • New _enforce_cost_cap() helper.
  • Called from complete(), complete_json(), stream_completion(), and chat() immediately after _check_budget() and before any cascade work. One chokepoint per public LLM entrypoint covers all 9 production call sites.
  • _record_invocation now auto-pulls tenant_id from the ContextVar when callers don't pass one — same scope drives both attribution writes and cap enforcement.

Testsbackend/tests/unit/test_cost_breaker.py, 20 tests:

  • ContextVar lifecycle (default, set/reset, async scope).
  • Env parsing (global, per-tenant overrides, 0 disables, invalid JSON, invalid string).
  • Per-tenant override > global; 0 in override disables.
  • Enforcement: no tenant = no DB call, no cap = no DB call, under = pass, over = raise, exact-cap = raise, ContextVar tenant resolves, fail-open on DB error.
  • Event: cost.cap.tripped emitted on trip, emit_event=False suppresses.
  • Exception message format.

Result: 30/30 green with test_cost_attribution.py (no regression on #25).

Blueprint: P0-4 flipped TODOSHIPPED.

Why this shape

  • Breaker fires BEFORE the cascade loop, not inside it — a tripped cap costs zero LLM tokens.
  • Tenant flows via ContextVar so call sites don't need to thread an org_id parameter through every chain / agent / tool layer. Caller sets tenant_scope("org-X") once at request entry; the breaker AND the recorder both pick it up.
  • Fail-open everywhere: bad env, JSON parse error, missing override, cost service down, event-emitter exception — nothing in the cap machinery blocks a legitimate call when the cap subsystem itself is unhealthy. A telemetry outage must not become an availability outage.
  • Cap source is env-only (no new feature flag) because the value is per-deployment, not per-feature-flip. Per-tenant overrides live in a JSON env to keep the blast radius of a typo small.

Stack

... -> #21 -> #22 -> #23 -> #24 -> #25 (m12-pr07-cost-attribution) -> #THIS

Files: 4 changed, +487 / -1.

Deployment notes

Cap is OFF until an operator sets ORG_DAILY_COST_CAP_USD (e.g. 50 for $50/day). To enable per-tenant:

ORG_DAILY_COST_CAP_OVERRIDES='{"<tenant-uuid>": 200, "<vip-uuid>": 0}'

(0 = disabled for that tenant; positive = override the global cap.)

For tenant scoping to work, request entrypoints (HTTP handlers, worker job pickup, SSE start) must wrap their LLM-touching code with async with tenant_scope(org_id): .... Wiring the entrypoints is intentionally out-of-scope — separate PR per entrypoint family.

Wires the spend telemetry from m12-pr07 (PR #25) into a hard breaker
that refuses LLM calls before any provider tier is touched once an
org is past its configured daily $ cap.

What ships
----------

- New `ai_engine/cost_breaker.py`:
  * `OrgDailyCostCapExceeded` exception carrying tenant_id, spent_cents,
    cap_cents.
  * `current_tenant_id` ContextVar with `set_tenant_id` / `reset_tenant_id`
    / `get_tenant_id` / async `tenant_scope` helpers — single canonical
    place to bind tenant identity for downstream LLM calls.
  * `cap_cents_for(tenant_id)` reads `ORG_DAILY_COST_CAP_USD` (global)
    and `ORG_DAILY_COST_CAP_OVERRIDES` (per-tenant JSON map). 0 / unset
    / parse error all mean "cap disabled" (fail-open).
  * `enforce_org_daily_cost_cap()` — async; queries
    `CostAttributionService.get_org_cost_today_cents` (FRESH base-table
    read, not the 60s MV) and raises `OrgDailyCostCapExceeded` when
    spent_cents >= cap_cents. Emits `cost.cap.tripped` event via the
    bound emitter (best-effort). Fail-open on cost-service errors so a
    Supabase outage cannot black-hole all LLM traffic.

- `ai_engine/client.py`:
  * New `_enforce_cost_cap()` helper.
  * Called from `complete()`, `complete_json()`, `stream_completion()`,
    and `chat()` immediately after `_check_budget()` and before any
    cascade work. One chokepoint per public LLM entrypoint covers all
    9 production call sites.
  * `_record_invocation` now auto-pulls tenant_id from the contextvar
    when callers don't pass one, so cost attribution writes are tied
    to the same tenant scope the breaker enforces.

- Tests: `backend/tests/unit/test_cost_breaker.py` — 20 tests:
  ContextVar lifecycle, env parsing (global + overrides + 0 disables),
  unset = no DB call, under cap = pass, over cap = raise with right
  attrs, exact-cap boundary raises, contextvar tenant resolves,
  fail-open on DB error, event emitted on trip, `emit_event=False`
  suppresses, exception message format. 30/30 green with
  test_cost_attribution.py.

- Blueprint: P0-4 flipped TODO -> SHIPPED.

Why this shape
--------------

- Breaker fires BEFORE the cascade loop, not inside it -- a tripped
  cap costs zero LLM tokens.
- Tenant flows via ContextVar so call sites don't need to thread an
  org_id parameter through every chain / agent / tool layer. Caller
  sets `tenant_scope("org-X")` once at request entry; the breaker
  AND the recorder both pick it up.
- Fail-open everywhere: bad env, JSON parse error, missing override,
  cost service down, event-emitter exception -- nothing in the cap
  machinery blocks a legitimate call when the cap subsystem itself
  is unhealthy.
- Cap source is env-only (no new feature flag) because the value is
  per-deployment, not per-feature-flip. Per-tenant overrides live in
  a JSON env to keep the blast radius of a typo small.

Stack
-----

Branch: m12-pr08-cost-cap (off m12-pr07-cost-attribution / PR #25)

Files: 3 changed, 1 new test file
- ai_engine/cost_breaker.py (new)
- ai_engine/client.py (5 hooks)
- backend/tests/unit/test_cost_breaker.py (new)
- docs/architecture/WORLD_CLASS_ARCHITECTURE_BLUEPRINT.md (flip)
Copilot AI review requested due to automatic review settings May 9, 2026 02:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements blueprint P0-4 by introducing a per-tenant daily LLM spend cap (“cascade breaker”) enforced at the entry of AIClient public LLM methods, driven by tenant identity propagated via a ContextVar and backed by CostAttributionService’s “today” spend read.

Changes:

  • Added ai_engine.cost_breaker with tenant ContextVar helpers, env-driven cap parsing (global + per-tenant overrides), and async enforcement that raises OrgDailyCostCapExceeded.
  • Wired enforcement into AIClient entrypoints and made invocation recording default tenant_id from the ContextVar when not explicitly provided.
  • Added unit tests for breaker behavior and updated the architecture blueprint status for P0-4.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
docs/architecture/WORLD_CLASS_ARCHITECTURE_BLUEPRINT.md Marks P0-4 as shipped and documents the cap/breaker behavior.
backend/tests/unit/test_cost_breaker.py New unit tests covering ContextVar lifecycle, env parsing, enforcement, and event emission.
ai_engine/cost_breaker.py New module implementing cap parsing + enforcement and the OrgDailyCostCapExceeded exception.
ai_engine/client.py Enforces the cap on LLM entrypoints and auto-attaches tenant_id to invocation records from the ContextVar.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

| P0-2 | Eliminate in-process generation fallback; fail-fast 503 | W2 | platform | **SHIPPED** — m7-pr27b |
| P0-3 | Queue ACK on success only; DLQ + idempotent handlers | W3 | platform | **SHIPPED** — m7-pr27c |
| P0-4 | Per-org daily $ cap + cascade-failure breaker | W8 | platform | TODO |
| P0-4 | Per-org daily $ cap + cascade-failure breaker | W8 | platform | SHIPPED (m12-pr08) — `ai_engine.cost_breaker` reads `ORG_DAILY_COST_CAP_USD` (+ per-tenant overrides), pulls today's spend via `CostAttributionService.get_org_cost_today_cents`, raises `OrgDailyCostCapExceeded` at the entry of every cascade method, emits `cost.cap.tripped` event, fail-open on telemetry errors |
Comment thread ai_engine/cost_breaker.py
Comment on lines +75 to +86
def _parse_global_cap_cents() -> Optional[int]:
raw = (os.getenv("ORG_DAILY_COST_CAP_USD", "") or "").strip()
if not raw:
return None
try:
usd = float(raw)
except ValueError:
logger.warning("invalid_org_daily_cost_cap_usd: raw=%r", raw)
return None
if usd <= 0:
return None
return int(round(usd * 100))
Comment thread ai_engine/cost_breaker.py
Comment on lines +191 to +199
try:
from backend.app.services.cost_attribution import (
get_cost_attribution_service,
)
spent = await get_cost_attribution_service().get_org_cost_today_cents(tid)
except Exception as exc:
# Fail-open: telemetry outage must not break the request path.
logger.warning("cost_cap_check_skipped_due_to_error: tenant=%s err=%s",
tid, str(exc)[:200])
Comment thread ai_engine/client.py
Comment on lines +1096 to +1109
async def _enforce_cost_cap(self) -> None:
"""P0-4 / m12-pr08: refuse the call when the org is over its daily $ cap.

Raises :class:`ai_engine.cost_breaker.OrgDailyCostCapExceeded` when
the tenant bound to the current context has spent past its
configured cap. No-op when no tenant is bound or no cap is set.
Fails open on cost-service errors.
"""
try:
from ai_engine.cost_breaker import enforce_org_daily_cost_cap
except Exception: # pragma: no cover — defensive import guard
return
await enforce_org_daily_cost_cap()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants