diff --git a/app/main.py b/app/main.py index 17d6a2e..9661035 100644 --- a/app/main.py +++ b/app/main.py @@ -1318,6 +1318,59 @@ async def admin_overview(request: Request): } +@app.get("/admin/model-usage") +@limiter.limit("30/minute") +async def admin_model_usage(request: Request): + """Co-op-wide per-model usage from LiteLLM spend logs. + + Authenticated by the X-Admin-Token header. Aggregates every spend-log row + (across ALL members) by model: total spend, tokens, and request count, + sorted by spend descending. No per-member breakdown — this is co-op-level + so admins can see which models the membership actually uses. + """ + _verify_admin_token(request) + + models = {} + total_spend = 0.0 + total_tokens = 0 + total_requests = 0 + try: + async with httpx.AsyncClient(timeout=30.0) as client: + r = await client.get( + f"{LITELLM_BASE}/spend/logs", + headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, + ) + r.raise_for_status() + rows = r.json() + if isinstance(rows, dict): + rows = rows.get("data", rows.get("logs", [])) + for row in rows if isinstance(rows, list) else []: + model = row.get("model") or row.get("model_group") or "unknown" + m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "requests": 0}) + m["spend"] += float(row.get("spend") or 0.0) + m["tokens"] += int(row.get("total_tokens") or 0) + m["requests"] += 1 + total_spend += float(row.get("spend") or 0.0) + total_tokens += int(row.get("total_tokens") or 0) + total_requests += 1 + except Exception as e: + logger.warning("admin model-usage: spend/logs fetch failed: %s", e) + + model_list = [ + {"model": name, **stats} + for name, stats in sorted(models.items(), key=lambda kv: kv[1]["spend"], reverse=True) + ] + + return { + "models": model_list, + "totals": { + "total_spend": total_spend, + "total_tokens": total_tokens, + "total_requests": total_requests, + }, + } + + # --- Broker HTTP endpoints (called by the member dashboard) --- # The dashboard is Cloudron-SSO-gated; it authenticates the member and passes # their email with a shared secret. The portal trusts the email only because it