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:
GebaRoanoke
2026-08-11 12:15:04 +02:00
committed by GitHub
co-authored by Claude Opus 5 David Heinemeier Hansson
parent f97ba7375e
commit 2b38f75506
3 changed files with 179 additions and 3 deletions
+64
View File
@@ -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."}
+12 -3
View File
@@ -92,9 +92,13 @@ Panel {
return plain === "" ? "Limit" : plain
}
function limitWindow(label, percent, resetAt) {
// A collector that already knows which window a limit belongs to says so,
// and that beats reading it back out of the label: a model-scoped limit is
// titled after its model, and a name like "Opus 5 (1M context)" would parse
// as a one-minute window.
function limitWindow(label, percent, resetAt, title) {
return {
title: windowTitle(label),
title: String(title || "") !== "" ? String(title) : windowTitle(label),
percent: Number(percent),
resetAt: String(resetAt || "")
}
@@ -107,7 +111,7 @@ Panel {
for (var i = 0; i < list.length; i++) {
var entry = list[i] || {}
var percent = Number(entry.percent)
if (percent >= 0) out.push(limitWindow(entry.label, percent, entry.resetsAt))
if (percent >= 0) out.push(limitWindow(entry.label, percent, entry.resetsAt, entry.title))
}
return out
}
@@ -701,11 +705,16 @@ Panel {
Text {
id: limitLabel
// A model-scoped window is titled after its model, and those names run
// long enough to reach the percentage, so the title gives way first.
text: limitRow.window ? limitRow.window.title : ""
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
elide: Text.ElideRight
anchors.left: parent.left
anchors.right: limitValue.left
anchors.rightMargin: Style.spacing.sm
anchors.verticalCenter: parent.verticalCenter
}
@@ -0,0 +1,103 @@
#!/bin/bash
source "$(dirname "$0")/base-test.sh"
require_command jq
require_command python3
# probe_limits reaches Anthropic, so the reader that interprets its answer is
# exercised on its own: the collector loads as a module, and a recorded payload
# stands in for the response.
read_limits() {
COLLECTOR="$ROOT/bin/omarchy-agent-usage-claude" PAYLOAD="$1" python3 - <<'PY'
import importlib.machinery, importlib.util, io, json, os
loader = importlib.machinery.SourceFileLoader("collector", os.environ["COLLECTOR"])
spec = importlib.util.spec_from_loader(loader.name, loader)
collector = importlib.util.module_from_spec(spec)
loader.exec_module(collector)
collector.urllib.request.urlopen = lambda request, timeout=None: io.BytesIO(os.environ["PAYLOAD"].encode())
print(json.dumps(collector.probe_limits("token")))
PY
}
# The two flat buckets, then every scoped shape that matters: a model's weekly
# window, a second window for that same model, a model that names only an id,
# and — dropped — a repeat of a window already read, a blank name, and a
# percent that will not parse.
limits=$(read_limits '{
"five_hour": { "utilization": 78.0 },
"seven_day": { "utilization": 12.0 },
"seven_day_opus": null,
"limits": [
{ "kind": "session", "percent": 78, "scope": null },
{ "kind": "weekly_all", "percent": 12, "scope": null },
{ "kind": "weekly_scoped", "percent": 17, "resets_at": "2026-08-15T03:00:00+00:00",
"scope": { "model": { "id": "claude-fable-5", "display_name": "Fable" }, "surface": null } },
{ "kind": "weekly_scoped", "percent": 99, "scope": { "model": { "display_name": "Fable" } } },
{ "kind": "five_hour_scoped", "percent": 95, "scope": { "model": { "display_name": "Fable" } } },
{ "kind": "weekly_scoped", "percent": 42, "scope": { "model": { "id": "claude-opus-5", "display_name": null } } },
{ "kind": "weekly_scoped", "percent": 5, "scope": { "model": { "display_name": " " } } },
{ "kind": "weekly_scoped", "percent": "unknown", "scope": { "model": { "display_name": "Opus" } } }
]
}')
expected='[{"label":"Session (5-hour)","percent":0.78,"resetsAt":""},{"label":"Weekly (7-day)","percent":0.12,"resetsAt":""},{"label":"Fable weekly","title":"Fable weekly","percent":0.17,"resetsAt":"2026-08-15T03:00:00+00:00"},{"label":"Fable session","title":"Fable session","percent":0.95,"resetsAt":""},{"label":"claude-opus-5 weekly","title":"claude-opus-5 weekly","percent":0.42,"resetsAt":""}]'
[[ $(jq -c '.limits' <<<"$limits") == "$expected" ]] ||
fail "Claude collector reads every model-scoped window once and drops unusable entries" "$limits"
pass "Claude collector reads every model-scoped window once and drops unusable entries"
# A payload that speaks fractions says so in its buckets, and the scoped
# entries are read on the same scale rather than assuming percentages.
fractions=$(read_limits '{
"five_hour": { "utilization": 0.78 },
"limits": [
{ "kind": "session", "percent": 0.78, "scope": null },
{ "kind": "weekly_scoped", "percent": 0.42, "scope": { "model": { "display_name": "Fable" } } }
]
}')
[[ $(jq -c '[.limits[].percent]' <<<"$fractions") == "[0.78,0.42]" ]] ||
fail "Claude collector reads scoped percentages on the payload's own scale" "$fractions"
pass "Claude collector reads scoped percentages on the payload's own scale"
# An account with no model-scoped allowance, and an endpoint that never grew
# the array, both keep the session and weekly windows they always had.
for payload in '{"five_hour":{"utilization":78.0},"limits":[{"kind":"session","percent":78,"scope":null}]}' \
'{"five_hour":{"utilization":78.0},"seven_day":{"utilization":12.0}}'; do
[[ $(jq -c '[.limits[].label]' <<<"$(read_limits "$payload")") != *" weekly"* ]] ||
fail "Claude collector adds no limit when the payload scopes none" "$payload"
done
pass "Claude collector adds no limit when the payload scopes none"
# The panel reads a window out of a label, and that guess cannot survive a
# model name — "Opus 5 (1M context)" parses as a one-minute window. A collector
# that states the title outright is taken at its word.
run_node_test <<'JS'
const fs = require('fs')
const source = fs.readFileSync(root + '/shell/plugins/agents/Panel.qml', 'utf8')
const start = source.indexOf('function windowIsLong')
const end = source.indexOf('// The window that decides')
assert(start > 0 && end > start, 'agents panel exposes its limit-window helpers')
eval(source.slice(start, end))
assertDeepEqual(
limitWindows({ limits: [
{ label: 'Session (5-hour)', percent: 0.78, resetsAt: '' },
{ label: 'Opus 5 (1M context) weekly', title: 'Opus 5 (1M context) weekly', percent: 0.42, resetsAt: '' }
] }),
[
{ title: 'Session', percent: 0.78, resetAt: '' },
{ title: 'Opus 5 (1M context) weekly', percent: 0.42, resetAt: '' }
],
'agents panel titles a limit off the collector when it states one'
)
assertDeepEqual(
limitWindows({ limits: [{ label: 'Weekly (7-day)', percent: 0.12, resetsAt: '' }] }),
[{ title: 'Weekly', percent: 0.12, resetAt: '' }],
'agents panel still reads a window out of a label that carries no title'
)
JS