model usage: Requests counts only successful calls
The Requests column summed every spend-log row (COUNT(*)), so a model's call count included refused/failed attempts — e.g. Metis showed 26 calls when only 16 succeeded (the other 10 were 401s on /v1/systemone). Count only rows whose status is not 'failure', and report the refused count separately as 'refused' (per model and in totals) so a failure cannot inflate use without disappearing from view. spend and tokens stay summed over all rows: a call that failed after consuming tokens still cost money. Fixes the panel and dashboard model tables, which both read this aggregation (panel via /admin/model-usage, dashboard via /broker/model-leaderboard).
This commit is contained in:
1 parent
8bcb7ff049
commit
c53112fbaf
1 file changed
+27
-6
+27
-6
@@ -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
|
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
|
(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.
|
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():
|
if not litellm_db_ready():
|
||||||
raise RuntimeError("LiteLLM DB credentials not configured")
|
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(NULLIF(model_group, ''), NULLIF(model, ''), 'unknown') AS model,
|
||||||
COALESCE(SUM(spend), 0) AS spend,
|
COALESCE(SUM(spend), 0) AS spend,
|
||||||
COALESCE(SUM(total_tokens), 0) AS tokens,
|
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"
|
FROM "LiteLLM_SpendLogs"
|
||||||
{where_sql}
|
{where_sql}
|
||||||
GROUP BY 1
|
GROUP BY 1
|
||||||
@@ -442,7 +450,7 @@ def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict:
|
|||||||
|
|
||||||
models = [
|
models = [
|
||||||
{"model": r[0], "spend": float(r[1] or 0.0), "tokens": int(r[2] or 0),
|
{"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
|
for r in rows
|
||||||
]
|
]
|
||||||
return {
|
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_spend": sum(m["spend"] for m in models),
|
||||||
"total_tokens": sum(m["tokens"] for m in models),
|
"total_tokens": sum(m["tokens"] for m in models),
|
||||||
"total_requests": sum(m["requests"] 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.
|
"""Co-op-wide per-model usage from LiteLLM spend logs.
|
||||||
|
|
||||||
Authenticated by the X-Admin-Token header. Aggregates spend-log rows
|
Authenticated by the X-Admin-Token header. Aggregates spend-log rows
|
||||||
(across ALL members) by model: total spend, tokens, and request count,
|
(across ALL members) by model: total spend, tokens, and the count of
|
||||||
sorted by spend descending. Optional ?days=N limits to the last N days
|
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
|
(0 or omitted = all-time). No per-member breakdown — this is co-op-level
|
||||||
so admins can see which models the membership actually uses.
|
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_spend = 0.0
|
||||||
total_tokens = 0
|
total_tokens = 0
|
||||||
total_requests = 0
|
total_requests = 0
|
||||||
|
total_refused = 0
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
@@ -3395,13 +3407,19 @@ async def admin_model_usage(request: Request, days: int = 0):
|
|||||||
if started and started < cutoff:
|
if started and started < cutoff:
|
||||||
continue
|
continue
|
||||||
model = row.get("model_group") or row.get("model") or "unknown"
|
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["spend"] += float(row.get("spend") or 0.0)
|
||||||
m["tokens"] += int(row.get("total_tokens") or 0)
|
m["tokens"] += int(row.get("total_tokens") or 0)
|
||||||
|
# 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
|
m["requests"] += 1
|
||||||
|
total_requests += 1
|
||||||
total_spend += float(row.get("spend") or 0.0)
|
total_spend += float(row.get("spend") or 0.0)
|
||||||
total_tokens += int(row.get("total_tokens") or 0)
|
total_tokens += int(row.get("total_tokens") or 0)
|
||||||
total_requests += 1
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("admin model-usage: spend/logs fetch failed: %s", 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_spend": total_spend,
|
||||||
"total_tokens": total_tokens,
|
"total_tokens": total_tokens,
|
||||||
"total_requests": total_requests,
|
"total_requests": total_requests,
|
||||||
|
"total_refused": total_refused,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3753,6 +3772,8 @@ async def broker_get_usage(email: str, days: int = 0) -> dict:
|
|||||||
m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "calls": 0})
|
m = models.setdefault(model, {"spend": 0.0, "tokens": 0, "calls": 0})
|
||||||
m["spend"] += float(row.get("spend") or 0.0)
|
m["spend"] += float(row.get("spend") or 0.0)
|
||||||
m["tokens"] += int(row.get("total_tokens") or 0)
|
m["tokens"] += int(row.get("total_tokens") or 0)
|
||||||
|
# Refused/failed attempts are not served requests.
|
||||||
|
if str(row.get("status") or "").lower() != "failure":
|
||||||
m["calls"] += 1
|
m["calls"] += 1
|
||||||
total_tokens += int(row.get("total_tokens") or 0)
|
total_tokens += int(row.get("total_tokens") or 0)
|
||||||
except Exception as e: # spend logs are best-effort; don't fail the whole view
|
except Exception as e: # spend logs are best-effort; don't fail the whole view
|
||||||
|
|||||||
Reference in new issue
Block a user