| """ |
| Budget Guard Middleware for FastAPI. |
| |
| Intercepts requests and aborts with HTTP 402 (Payment Required) |
| when a tenant's cumulative LLM token consumption costs exceed their allocated budget. |
| """ |
|
|
| from collections.abc import Callable |
| from typing import cast |
|
|
| from fastapi import Request, Response |
| from fastapi.responses import JSONResponse |
| from starlette.middleware.base import BaseHTTPMiddleware |
|
|
| from ..auth.utils import get_user_from_token |
| from ..config.logfire_config import get_logger |
| from ..utils import get_supabase_client |
|
|
| logger = get_logger("budget_guard") |
|
|
|
|
| class BudgetGuardMiddleware(BaseHTTPMiddleware): |
| """ |
| Middleware that enforces tenant-level budget controls on write operations. |
| """ |
|
|
| |
| BYPASS_PATHS = {"/health", "/api/health", "/api/auth", "/docs", "/redoc", "/openapi.json"} |
|
|
| async def dispatch(self, request: Request, call_next: Callable) -> Response: |
| |
| if request.method == "GET" or request.url.path in self.BYPASS_PATHS or not request.url.path.startswith("/api/"): |
| return cast(Response, await call_next(request)) |
|
|
| |
| auth_header = request.headers.get("Authorization", "") |
| token = "" |
| if auth_header.startswith("Bearer "): |
| token = auth_header.replace("Bearer ", "") |
|
|
| if not token: |
| |
| return cast(Response, await call_next(request)) |
|
|
| |
| user = await get_user_from_token(token) |
| if not user: |
| return cast(Response, await call_next(request)) |
|
|
| |
| user_metadata = getattr(user, "user_metadata", {}) or {} |
| role = user_metadata.get("role", "") |
| if role == "system_admin" or getattr(user, "id", "") == "system-agent": |
| return cast(Response, await call_next(request)) |
|
|
| |
| try: |
| supabase = get_supabase_client() |
|
|
| |
| profile_res = supabase.table("profiles").select("tenant_id").eq("id", user.id).execute() |
| if not profile_res.data: |
| return cast(Response, await call_next(request)) |
|
|
| tenant_id = profile_res.data[0]["tenant_id"] |
|
|
| |
| usage_res = supabase.table("token_usage").select("cost_usd").eq("tenant_id", tenant_id).execute() |
| total_cost = sum(float(row["cost_usd"] or 0) for row in usage_res.data) |
|
|
| |
| settings_res = supabase.table("archon_settings").select("value").eq("key", "BUDGET_LIMIT_USD").execute() |
| budget_limit = 10.0 |
| if settings_res.data and settings_res.data[0]["value"]: |
| try: |
| budget_limit = float(settings_res.data[0]["value"]) |
| except ValueError: |
| pass |
|
|
| |
| if total_cost >= budget_limit: |
| logger.warning( |
| f"⚠️ [Budget Guard] Blocked request from user={user.email} (Tenant={tenant_id}). " |
| f"Total Cost: ${total_cost:.4f} >= Limit: ${budget_limit:.4f}" |
| ) |
| return JSONResponse( |
| status_code=402, |
| content={ |
| "error": "Payment Required", |
| "detail": f"Tenant budget limit exceeded. Total Cost: ${total_cost:.4f} >= Limit: ${budget_limit:.4f}" |
| } |
| ) |
|
|
| except Exception as e: |
| logger.error(f"[Budget Guard] Error enforcing budget checks: {e}") |
| |
| return JSONResponse( |
| status_code=402, |
| content={ |
| "error": "Payment Required", |
| "detail": f"Database verification failure during budget checks: {e}. Operation blocked for safety." |
| } |
| ) |
|
|
| return cast(Response, await call_next(request)) |
|
|