From 0453cee01933be9bcc67a6862e97a394b0a43c13 Mon Sep 17 00:00:00 2001 From: inference-bot Date: Mon, 14 Sep 2026 14:18:59 -0600 Subject: [PATCH] Broker usage: add per-model token/spend breakdown + total_tokens + email --- app/main.py | 63 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index a8571c5..20ab966 100644 --- a/app/main.py +++ b/app/main.py @@ -1183,14 +1183,19 @@ async def broker_revoke(name: str, request: Request): async def broker_get_usage(email: str) -> dict: - """Return the member's spend vs. balance, from their team. + """Return the member's spend vs. balance, plus per-model and token detail. - LiteLLM's team object exposes `spend` and `max_budget` directly, so we read - the member's team and return {balance, spend, remaining}. This is the - read-only primitive the member dashboard uses (no master key on the client). + Reads the member's team (spend + max_budget) and aggregates their recent + spend logs by model for token counts. This is the read-only primitive the + member dashboard uses (no master key on the client). """ team_id = await litellm_get_or_create_team(email, get_member_balance(email)) + + balance = get_member_balance(email) + spend = 0.0 + async with httpx.AsyncClient() as client: + # 1. Team spend + budget r = await client.get( f"{LITELLM_BASE}/team/list", headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, @@ -1198,14 +1203,50 @@ async def broker_get_usage(email: str) -> dict: r.raise_for_status() for t in r.json(): if t.get("team_id") == team_id: - balance = float(t.get("max_budget") or get_member_balance(email)) + balance = float(t.get("max_budget") or balance) spend = float(t.get("spend") or 0.0) - return { - "balance": balance, - "spend": spend, - "remaining": max(balance - spend, 0.0), - } - return {"balance": get_member_balance(email), "spend": 0.0, "remaining": get_member_balance(email)} + break + + # 2. Per-model token/spend breakdown from spend logs (filtered by team). + # /spend/logs does not reliably filter by team_id server-side, so we + # filter client-side on the returned rows. + models = {} + total_tokens = 0 + try: + r2 = await client.get( + f"{LITELLM_BASE}/spend/logs", + headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, + ) + r2.raise_for_status() + rows = r2.json() + if isinstance(rows, dict): + rows = rows.get("data", rows.get("logs", [])) + for row in rows if isinstance(rows, list) else []: + if row.get("team_id") != team_id: + continue + model = row.get("model") or "unknown" + m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "calls": 0}) + m["spend"] += float(row.get("spend") or 0.0) + m["tokens"] += int(row.get("total_tokens") or 0) + m["calls"] += 1 + total_tokens += int(row.get("total_tokens") or 0) + except Exception as e: # spend logs are best-effort; don't fail the whole view + logger.warning("spend/logs fetch failed: %s", e) + + # Sort models by spend descending (which models are draining the most). + model_list = [ + {"model": name, **stats} + for name, stats in sorted(models.items(), key=lambda kv: kv[1]["spend"], reverse=True) + ] + + return { + "email": email, + "balance": balance, + "spend": spend, + "remaining": max(balance - spend, 0.0), + "total_tokens": total_tokens, + "models": model_list, + } @app.get("/broker/usage")