diff --git a/app/main.py b/app/main.py index 0a6221e..9db8657 100644 --- a/app/main.py +++ b/app/main.py @@ -402,6 +402,13 @@ def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict: Prefers the `model_group` column, which carries our provider/model alias (e.g. greenpt/glm-5.3-flash); falls back to `model` for pre-alias rows — matching the logic the HTTP path used, expressed in SQL. + + `requests` counts only metered calls that SUCCEEDED. Refused/failed attempts + (rows with status = 'failure', e.g. a 401 on the /v1/systemone decision + route) are excluded from that count and reported separately as `refused`, + so a failure cannot inflate a model's call count. `spend` and `tokens` stay + summed over every row: a call that failed after tokens were consumed still + cost money and still processed those tokens. """ if not litellm_db_ready(): raise RuntimeError("LiteLLM DB credentials not configured") @@ -425,7 +432,8 @@ def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict: COALESCE(NULLIF(model_group, ''), NULLIF(model, ''), 'unknown') AS model, COALESCE(SUM(spend), 0) AS spend, COALESCE(SUM(total_tokens), 0) AS tokens, - COUNT(*) AS requests + COUNT(*) FILTER (WHERE status IS DISTINCT FROM 'failure') AS requests, + COUNT(*) FILTER (WHERE status = 'failure') AS refused FROM "LiteLLM_SpendLogs" {where_sql} GROUP BY 1 @@ -442,7 +450,7 @@ def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict: models = [ {"model": r[0], "spend": float(r[1] or 0.0), "tokens": int(r[2] or 0), - "requests": int(r[3] or 0)} + "requests": int(r[3] or 0), "refused": int(r[4] or 0)} for r in rows ] return { @@ -451,6 +459,7 @@ def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict: "total_spend": sum(m["spend"] for m in models), "total_tokens": sum(m["tokens"] for m in models), "total_requests": sum(m["requests"] for m in models), + "total_refused": sum(m["refused"] for m in models), }, } @@ -3345,8 +3354,10 @@ async def admin_model_usage(request: Request, days: int = 0): """Co-op-wide per-model usage from LiteLLM spend logs. Authenticated by the X-Admin-Token header. Aggregates spend-log rows - (across ALL members) by model: total spend, tokens, and request count, - sorted by spend descending. Optional ?days=N limits to the last N days + (across ALL members) by model: total spend, tokens, and the count of + successful requests (refused/failed attempts are excluded and reported + separately as `refused`), sorted by request count descending. Optional + ?days=N limits to the last N days (0 or omitted = all-time). No per-member breakdown — this is co-op-level so admins can see which models the membership actually uses. @@ -3374,6 +3385,7 @@ async def admin_model_usage(request: Request, days: int = 0): total_spend = 0.0 total_tokens = 0 total_requests = 0 + total_refused = 0 try: async with httpx.AsyncClient(timeout=30.0) as client: r = await client.get( @@ -3395,13 +3407,19 @@ async def admin_model_usage(request: Request, days: int = 0): if started and started < cutoff: continue model = row.get("model_group") or row.get("model") or "unknown" - m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "requests": 0}) + m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "requests": 0, "refused": 0}) m["spend"] += float(row.get("spend") or 0.0) m["tokens"] += int(row.get("total_tokens") or 0) - m["requests"] += 1 + # Refused/failed attempts are not served requests: keep them out + # of the request count (reported separately as `refused`). + if str(row.get("status") or "").lower() == "failure": + m["refused"] += 1 + total_refused += 1 + else: + m["requests"] += 1 + total_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) @@ -3416,6 +3434,7 @@ async def admin_model_usage(request: Request, days: int = 0): "total_spend": total_spend, "total_tokens": total_tokens, "total_requests": total_requests, + "total_refused": total_refused, }, } @@ -3753,7 +3772,9 @@ async def broker_get_usage(email: str, days: int = 0) -> dict: 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 + # Refused/failed attempts are not served requests. + if str(row.get("status") or "").lower() != "failure": + 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)