Show the per-model weekly limit Claude's usage endpoint reports (#6691)
* Show the per-model weekly limit Claude's usage endpoint reports Model-scoped allowances arrive in the payload's limits array, not in the seven_day_<model> buckets, which come back null. Read them so a window like Fable's own weekly limit stops being spent against invisibly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Read every model-scoped window, and title it for what it is A model can hold more than one scoped window, so keying the dedupe on the model alone dropped whichever came second — including a fuller one that decides the headline. The model and the window kind together make the key, and both make the title, so one model's two rows read apart. The panel guesses a window out of the label, and that guess cannot survive a model name: "Opus 5 (1M context)" parses as a one-minute window and renders as a second "Session". The collector states the title outright now and the panel takes it, eliding a long one rather than running it into the percentage. Scoped percentages are read on whatever scale the payload speaks, the way the flat buckets already are, rather than assuming percentages, and a model that names only an id still names a window worth showing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: David Heinemeier Hansson <david@hey.com>
This commit is contained in:
co-authored by
Claude Opus 5
David Heinemeier Hansson
parent
f97ba7375e
commit
2b38f75506
@@ -649,6 +649,64 @@ def usage_bucket(payload: dict[str, Any], key: str) -> dict[str, Any] | None:
|
||||
return bucket if isinstance(bucket, dict) else None
|
||||
|
||||
|
||||
# An entry's `kind` names its window the way the flat buckets' keys do
|
||||
# ("weekly_scoped", "five_hour_scoped"). The panel reads a window out of free
|
||||
# text, which cannot survive a model name like "Opus 5 (1M context)" — the
|
||||
# "1M" reads as a one-minute window — so the window is settled here instead
|
||||
# and travels as an explicit title.
|
||||
def scoped_window(kind: str) -> str:
|
||||
text = kind.lower()
|
||||
if "month" in text:
|
||||
return "monthly"
|
||||
if "week" in text or "day" in text:
|
||||
return "weekly"
|
||||
if "hour" in text or "session" in text:
|
||||
return "session"
|
||||
return ""
|
||||
|
||||
|
||||
# Alongside the flat buckets, the payload carries a `limits` array, and that
|
||||
# array is the only place a model-scoped allowance shows up — a weekly window
|
||||
# that only Fable draws from, say. The matching legacy keys
|
||||
# (`seven_day_opus`, `seven_day_sonnet`, …) stayed behind at null, so a
|
||||
# collector that reads buckets alone silently drops a limit the account is
|
||||
# actually spending against. A model can hold more than one scoped window, and
|
||||
# only the pair of model and window tells them apart, so both make the title
|
||||
# and both make the key that keeps a repeat out.
|
||||
def scoped_limits(payload: dict[str, Any], percent_scale: bool) -> list[dict[str, Any]]:
|
||||
entries = payload.get("limits")
|
||||
if not isinstance(entries, list):
|
||||
return []
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
scope = entry.get("scope")
|
||||
model = scope.get("model") if isinstance(scope, dict) else None
|
||||
if not isinstance(model, dict):
|
||||
continue
|
||||
# A display name is what the panel wants, but an entry carrying only an id
|
||||
# still names a window worth showing.
|
||||
name = str(model.get("display_name") or model.get("id") or "").strip()
|
||||
kind = str(entry.get("kind") or "").strip()
|
||||
if name == "" or (name, kind) in seen:
|
||||
continue
|
||||
percent = normalize_utilization(entry.get("percent"), percent_scale)
|
||||
if percent < 0:
|
||||
continue
|
||||
seen.add((name, kind))
|
||||
window = scoped_window(kind)
|
||||
title = name + " " + window if window else name
|
||||
out.append({
|
||||
"label": title,
|
||||
"title": title,
|
||||
"percent": percent,
|
||||
"resetsAt": normalize_reset_at(entry.get("resets_at")),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def probe_limits(access_token: str) -> dict[str, Any]:
|
||||
request = urllib.request.Request(
|
||||
USAGE_ENDPOINT,
|
||||
@@ -683,6 +741,11 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
||||
weekly = usage_bucket(payload, "seven_day_oauth_apps") or usage_bucket(payload, "seven_day")
|
||||
session = usage_bucket(payload, "five_hour")
|
||||
raw = [session.get("utilization") if session else None, weekly.get("utilization") if weekly else None]
|
||||
# One payload speaks one convention, so the scoped entries settle the scale
|
||||
# alongside the buckets rather than assuming their own.
|
||||
entries = payload.get("limits")
|
||||
if isinstance(entries, list):
|
||||
raw += [entry.get("percent") for entry in entries if isinstance(entry, dict)]
|
||||
percent_scale = any(parse_utilization(v) >= 1 for v in raw)
|
||||
|
||||
limits = []
|
||||
@@ -694,6 +757,7 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
||||
percent = normalize_utilization(weekly.get("utilization"), percent_scale)
|
||||
if percent >= 0:
|
||||
limits.append({"label": "Weekly (7-day)", "percent": percent, "resetsAt": normalize_reset_at(weekly.get("resets_at"))})
|
||||
limits.extend(scoped_limits(payload, percent_scale))
|
||||
|
||||
if not limits:
|
||||
return {"ok": False, "helpText": "Anthropic's usage endpoint returned no limits. Local Claude Code stats are still shown."}
|
||||
|
||||
Reference in New Issue
Block a user