Skip to content

symfonic.agent.fastapi.budget_dep

budget_dep

FastAPI circuit-breaker dependency: reject LLM calls when tenant is broke.

Gate 2 of v5.5.0. Wires :class:symfonic.core.observability.TokenBudgetTracker into the request cycle so a tenant that blew through its daily or monthly ceiling never reaches agent.run() at all. When no tracker is registered the dependency is a no-op — pre-existing deployments keep working unchanged.

Admin callers bypass the breaker. Admin status is signalled two ways:

  1. request.state.is_admin — set by the scaffolded tenant-auth verifier after JWT decode. Preferred.
  2. X-Admin-Override: 1 header — only trusted when the registered set_tenant_auth_verifier hook has already authenticated the caller AND set request.state.is_admin = True (defence in depth).

The dependency deliberately reads tenant id directly from the X-Tenant-ID header rather than re-running :func:get_tenant_scope because FastAPI caches Depends per request — we want this check to run BEFORE auth-expensive scope construction when possible. When used together (as the router does), the scope check still fires first because the router declares it earlier in the signature.

check_budget_before_llm async

check_budget_before_llm(request: Request) -> None

Raise 429 when the tenant is over its daily/monthly budget.

No-op when
  • no :class:TokenBudgetTracker has been registered via set_budget_tracker(); OR
  • the request is flagged as admin.

Raises:

Type Description
HTTPException 429

{"detail": "Budget exceeded: <reason>"}.

Source code in src/symfonic/agent/fastapi/budget_dep.py
async def check_budget_before_llm(request: Request) -> None:
    """Raise 429 when the tenant is over its daily/monthly budget.

    No-op when:
        * no :class:`TokenBudgetTracker` has been registered via
          ``set_budget_tracker()``; OR
        * the request is flagged as admin.

    Raises:
        HTTPException 429: ``{"detail": "Budget exceeded: <reason>"}``.
    """
    tracker = get_budget_tracker()
    if tracker is None:
        return

    tenant_id = request.headers.get("X-Tenant-ID")
    if not tenant_id:
        # Let get_tenant_scope handle the 401; the breaker has nothing
        # to say about an unauthenticated caller.
        return

    is_admin = bool(getattr(request.state, "is_admin", False))
    result = await tracker.check_budget(tenant_id, is_admin=is_admin)
    if not result.allowed:
        logger.info(
            "Budget breaker fired: tenant=%s reason=%s", tenant_id, result.reason,
        )
        raise HTTPException(
            status_code=429,
            detail=f"Budget exceeded: {result.reason}",
            headers={"Retry-After": "3600"},
        )