@wendellresearch-ai/benchwarmers 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,460 @@
1
+ """The Quota page's tiles: per provider, what is left, when it resets, who rests.
2
+
3
+ python metrics/quotaview.py # every tile, as the page gets it
4
+ python metrics/quotaview.py --selftest # the parsers and the tile shapes; no calls
5
+
6
+ One tile per connection tag a bench model arrives through
7
+ (route.full_tag_of: `cli-proxy-api/codex`, `openrouter`, `nvidia-api`).
8
+ A tile takes the shape of what its provider says:
9
+
10
+ groups windows per model group, several each (CLI Proxy API's OAuth
11
+ credentials: Gemini 5 hours and week), or per model from the
12
+ rate-limit headers the built-in client saw (metrics/quota.py)
13
+ pool one allowance the models share: OpenRouter's free requests a
14
+ day, a bar per key, counted from the usage ledger
15
+ models nothing stated: each bench model's last day, answered or not
16
+
17
+ REFRESH NEVER SPENDS QUOTA. Two providers can be asked what is left for free,
18
+ and are, when Refresh is pressed:
19
+
20
+ OpenRouter GET /key on a direct OpenRouter endpoint: free tier or
21
+ not (50 or 1,000 free requests a day), and its credits
22
+ CLI Proxy API its management route `requests/api-call`, which calls the
23
+ credential's own quota endpoint with the credential's
24
+ token, as its console does (Google's retrieveUserQuotaSummary,
25
+ ChatGPT's wham/usage). Needs CLI Proxy API on the
26
+ Endpoints page as a hub with its management key
27
+ (endpoints.admin_key, as metrics/cliproxy.py); the proxy is
28
+ on the LAN and calls only providers the credential uses.
29
+
30
+ Every other provider's Refresh re-reads the bench's own records. `check`
31
+ is the one that asks: a 16-token call to one of its models, whose reply
32
+ headers quota.py records. Offered only where the provider has sent limit
33
+ headers before, since elsewhere the reply would say nothing.
34
+
35
+ Live reads are kept in runs/quota-live.json, newest per tag.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import datetime as dt
41
+ import json
42
+ import pathlib
43
+ import sys
44
+ import time
45
+ import urllib.error
46
+ import urllib.parse
47
+ import urllib.request
48
+
49
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
50
+ sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
51
+ import locked # noqa: E402
52
+ from paths import DATA # noqa: E402
53
+
54
+ LIVE = DATA / "runs" / "quota-live.json"
55
+ UTC = dt.timezone.utc
56
+ CPA_PREFIXES = ("/v8/management", "/v0/management") # the console's, then the older one
57
+ # The headers CLI Proxy API's own console sends to each quota endpoint
58
+ # ($TOKEN$ is replaced by the proxy with the credential's token).
59
+ AG_URLS = ("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
60
+ "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary")
61
+ # Google answers 403 without the Antigravity client's User-Agent.
62
+ AG_HEAD = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json",
63
+ "User-Agent": "antigravity/hub/2.23.0 darwin/arm64"}
64
+ CODEX_URL = "https://chatgpt.com/backend-api/wham/usage"
65
+ CODEX_HEAD = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json",
66
+ "User-Agent": "codex-tui/0.149.1"}
67
+ WINDOW_NAMES = {"5h": "5 hours", "weekly": "week", "daily": "day", "monthly": "month"}
68
+
69
+
70
+ def _iso(t: float | None) -> str | None:
71
+ return None if t is None else dt.datetime.fromtimestamp(t, UTC).isoformat(timespec="seconds")
72
+
73
+
74
+ def _ts(iso) -> float | None:
75
+ try:
76
+ return dt.datetime.fromisoformat(str(iso).replace("Z", "+00:00")).timestamp() if iso else None
77
+ except ValueError:
78
+ return None
79
+
80
+
81
+ def _seconds_name(s) -> str:
82
+ """A window's length as a word: 18000 -> "5 hours", 604800 -> "week"."""
83
+ s = int(s or 0)
84
+ return {3600: "hour", 18000: "5 hours", 86400: "day", 604800: "week", 2592000: "month"}.get(
85
+ s, f"{round(s / 3600)} hours" if s else "window")
86
+
87
+
88
+ def _utc_midnight(now: float) -> float:
89
+ d = dt.datetime.fromtimestamp(now, UTC)
90
+ return dt.datetime(d.year, d.month, d.day, tzinfo=UTC).timestamp()
91
+
92
+
93
+ # ---- parsers: a provider's reply -> groups of windows -----------------------
94
+
95
+ def antigravity_groups(body: dict) -> list[dict]:
96
+ """retrieveUserQuotaSummary -> [{label, windows: [{label, left, reset_at}]}],
97
+ the shorter window first."""
98
+ out = []
99
+ for g in body.get("groups") or []:
100
+ wins = [{"label": WINDOW_NAMES.get(b.get("window"), str(b.get("window") or "window")),
101
+ "left": round(float(b.get("remainingFraction", 0)) * 100),
102
+ "reset_at": b.get("resetTime")}
103
+ for b in g.get("buckets") or [] if isinstance(b, dict)]
104
+ wins.sort(key=lambda w: ["5 hours", "day", "week", "month"].index(w["label"])
105
+ if w["label"] in ("5 hours", "day", "week", "month") else 9)
106
+ if wins:
107
+ out.append({"label": str(g.get("displayName") or ""), "windows": wins})
108
+ return out
109
+
110
+
111
+ def codex_groups(body: dict) -> list[dict]:
112
+ """wham/usage -> one group with its primary and secondary windows."""
113
+ rl = body.get("rate_limit") or {}
114
+ wins = [{"label": _seconds_name(w.get("limit_window_seconds")),
115
+ "left": max(0, 100 - round(float(w.get("used_percent") or 0))),
116
+ "reset_at": _iso(w["reset_at"]) if w.get("reset_at") else None}
117
+ for w in (rl.get("primary_window"), rl.get("secondary_window")) if isinstance(w, dict)]
118
+ return [{"label": "", "windows": wins}] if wins else []
119
+
120
+
121
+ # ---- live reads --------------------------------------------------------------
122
+
123
+ def _get_json(url: str, headers: dict, data: bytes | None = None, timeout: int = 20) -> dict:
124
+ req = urllib.request.Request(url, data=data, headers={"Accept": "application/json", **headers},
125
+ method="POST" if data is not None else "GET")
126
+ with urllib.request.urlopen(req, timeout=timeout) as r:
127
+ return json.loads(r.read() or b"{}")
128
+
129
+
130
+ def _openrouter_endpoint() -> str | None:
131
+ """A direct OpenRouter endpoint with a key: the one /key can be asked on."""
132
+ import endpoints
133
+ for name, entry in endpoints.load().items():
134
+ host = urllib.parse.urlsplit(entry.get("base", "")).hostname or ""
135
+ if host.endswith("openrouter.ai") and not endpoints.hub(name) and endpoints.key(name):
136
+ return name
137
+ return None
138
+
139
+
140
+ def read_openrouter() -> dict:
141
+ import endpoints
142
+ name = _openrouter_endpoint()
143
+ if not name:
144
+ raise ValueError("no direct OpenRouter endpoint with a key to ask")
145
+ data = _get_json(endpoints.base(name) + "/key", {"Authorization": f"Bearer {endpoints.key(name)}"}).get("data") or {}
146
+ return {"endpoint": name, "free_tier": bool(data.get("is_free_tier")),
147
+ "limit": data.get("limit"), "remaining": data.get("limit_remaining"), "usage": data.get("usage")}
148
+
149
+
150
+ def _cpa_hub() -> tuple[str, str]:
151
+ """(management base, key) of the CLI Proxy API hub; ValueError when there is none."""
152
+ import endpoints
153
+ name = endpoints.find_hub("cliproxy")
154
+ if not name:
155
+ raise ValueError("add CLI Proxy API on the Endpoints page (kind hub, CLI Proxy API) with its"
156
+ " management key to read these quotas live")
157
+ secret = endpoints.admin_key(name, fallback=False)
158
+ if not secret:
159
+ raise ValueError(f"CLI Proxy API's management key is not stored: Change key on {name}")
160
+ return endpoints.base(name).removesuffix("/v1"), secret
161
+
162
+
163
+ def _cpa(base: str, secret: str, path: str, body: dict | None = None) -> dict:
164
+ """One management call; the console's prefix first, the older on a 404.
165
+ A 401 is never retried: the proxy bans an address after 5 refusals."""
166
+ head = {"Authorization": f"Bearer {secret}", "Content-Type": "application/json"}
167
+ for prefix in CPA_PREFIXES:
168
+ try:
169
+ return _get_json(base + prefix + path, head, None if body is None else json.dumps(body).encode())
170
+ except urllib.error.HTTPError as exc:
171
+ if exc.code == 404 and prefix != CPA_PREFIXES[-1]:
172
+ continue
173
+ if exc.code == 401:
174
+ raise ValueError("CLI Proxy API refused the management key; check it before trying again"
175
+ " (5 refusals ban this address for 30 minutes)") from None
176
+ raise ValueError(f"CLI Proxy API answered {exc.code} on {path}") from None
177
+ raise ValueError("CLI Proxy API's management API is off (every route 404s without a secret key)")
178
+
179
+
180
+ def _api_call(base, secret, auth_index, method, url, header, data=None) -> dict:
181
+ """The credential's own request, made by the proxy; the reply's JSON body."""
182
+ reply = _cpa(base, secret, "/requests/api-call",
183
+ {"auth_index": auth_index, "method": method, "url": url, "header": header,
184
+ **({"data": data} if data is not None else {})})
185
+ if not 200 <= int(reply.get("status_code") or 0) < 300:
186
+ raise ValueError(f"{urllib.parse.urlsplit(url).hostname} answered {reply.get('status_code')}")
187
+ body = reply.get("body")
188
+ return json.loads(body) if isinstance(body, str) and body.strip() else (body or {})
189
+
190
+
191
+ def read_cliproxy(tag: str, sibling_vendors: set[str]) -> dict:
192
+ """Every enabled credential behind `tag`: `cli-proxy-api/codex` is
193
+ the codex ones, bare `cli-proxy-api` those no sibling tag names."""
194
+ base, secret = _cpa_hub()
195
+ vendor = tag.split("/", 1)[1] if "/" in tag else ""
196
+ creds = [c for c in _cpa(base, secret, "/credentials").get("files") or []
197
+ if not c.get("disabled") and (c.get("provider") == vendor if vendor
198
+ else c.get("provider") not in sibling_vendors)]
199
+ groups, names, plan, notes = [], [], "", []
200
+ for c in creds:
201
+ prov, idx = c.get("provider", ""), c.get("auth_index")
202
+ try:
203
+ if prov == "antigravity":
204
+ body = None
205
+ for url in AG_URLS:
206
+ try:
207
+ body = _api_call(base, secret, idx, "POST", url, AG_HEAD,
208
+ json.dumps({"project": c.get("project_id") or ""}))
209
+ break
210
+ except ValueError:
211
+ continue
212
+ got = antigravity_groups(body or {})
213
+ elif prov == "codex":
214
+ head = dict(CODEX_HEAD)
215
+ acct = (c.get("id_token") or {}).get("chatgpt_account_id")
216
+ if acct:
217
+ head["Chatgpt-Account-Id"] = acct
218
+ body = _api_call(base, secret, idx, "GET", CODEX_URL, head)
219
+ got = codex_groups(body)
220
+ plan = str(body.get("plan_type") or "")
221
+ else:
222
+ notes.append(f"no quota reader for {prov}")
223
+ continue
224
+ except ValueError as exc:
225
+ notes.append(f"{prov}: {exc}")
226
+ continue
227
+ if len(creds) > 1:
228
+ for g in got:
229
+ g["label"] = f"{prov} · {g['label']}".rstrip(" ·")
230
+ groups += got
231
+ names.append(prov)
232
+ if not groups and notes:
233
+ raise ValueError("; ".join(notes))
234
+ return {"groups": groups, "name": ", ".join(dict.fromkeys(names)), "plan": plan, "notes": notes}
235
+
236
+
237
+ def refresh(tag: str) -> dict:
238
+ """Ask the provider when it can be asked for free, keep the answer;
239
+ {"ok", "how": "live"|"records", "note"}."""
240
+ if tag == "openrouter" and _openrouter_endpoint():
241
+ got = read_openrouter()
242
+ elif tag.split("/")[0] == "cli-proxy-api":
243
+ vendors = {t.split("/", 1)[1] for t in _tags() if t.startswith("cli-proxy-api/")}
244
+ got = read_cliproxy(tag, vendors)
245
+ else:
246
+ return {"ok": True, "how": "records",
247
+ "note": "this provider has no free quota read: shown from the bench's own records"}
248
+ got["at"] = _iso(time.time())
249
+ locked.update(LIVE, lambda d: d.update({tag: got}), ensure_ascii=False)
250
+ return {"ok": True, "how": "live", "note": "; ".join(got.get("notes") or [])}
251
+
252
+
253
+ def check(tag: str) -> dict:
254
+ """One 16-token call to a model on `tag` that is free to call; the
255
+ built-in client records what its reply headers say (quota.observe)."""
256
+ import client
257
+ import learn
258
+ resting = learn.cooldowns()
259
+ aliases = [a for a in _tags().get(tag, []) if a not in resting]
260
+ if not aliases:
261
+ return {"ok": False, "errors": ["every model on it is resting; nothing was sent"]}
262
+ if not _headers_seen(tag):
263
+ return {"ok": False, "errors": ["this provider has never sent limit headers, so a call would"
264
+ " tell nothing; nothing was sent"]}
265
+ res = client.chat(aliases[0], "Hi", timeout=60, body={"max_tokens": 16}, purpose="quota check", retries=0)
266
+ if res.get("error"):
267
+ return {"ok": False, "errors": [f"{aliases[0]}: {res['error'][:200]}"]}
268
+ return {"ok": True, "note": f"asked {aliases[0]}: one request"}
269
+
270
+
271
+ # ---- tiles -------------------------------------------------------------------
272
+
273
+ def _bench() -> list[str]:
274
+ from run import _registered_names
275
+ gone = locked.read_json(DATA / "config" / "retired.json")
276
+ return [a for a in _registered_names() if a not in gone]
277
+
278
+
279
+ def _tags() -> dict[str, list[str]]:
280
+ import route
281
+ out: dict[str, list[str]] = {}
282
+ for a in _bench():
283
+ tag = route.full_tag_of(a)
284
+ if tag != "?":
285
+ out.setdefault(tag, []).append(a)
286
+ return out
287
+
288
+
289
+ def _headers_seen(tag: str) -> bool:
290
+ """Has this tag's provider ever stated a limit in its headers (kept a day past reset)?"""
291
+ import quota
292
+ import route
293
+ aliases = set(_tags().get(tag, []))
294
+ host = route.connection_of(next(iter(aliases), "")) if aliases else tag
295
+ recs = locked.read_json(quota.FILE).get(host) or {}
296
+ return any(str(r.get("source", "")).startswith("headers") and r.get("limit")
297
+ for a, by in recs.items() if a in aliases and isinstance(by, dict)
298
+ for r in by.values() if isinstance(r, dict))
299
+
300
+
301
+ def _model_rows(aliases, rows, resting, limits) -> list[dict]:
302
+ out = []
303
+ for a in sorted(aliases):
304
+ mine = [r for r in rows if r.get("alias") == a]
305
+ ok = [r for r in mine if r.get("status") == "ok"]
306
+ out.append({"alias": a, "ok": len(ok), "failed": len(mine) - len(ok),
307
+ "last_ok": _iso(max(r["t"] for r in ok)) if ok else None,
308
+ "last": _iso(max(r["t"] for r in mine)) if mine else None,
309
+ "until": (resting.get(a) or {}).get("until"),
310
+ "limit": limits.get(a, "")})
311
+ return out
312
+
313
+
314
+ def _limit_text(lim: dict) -> str:
315
+ words = {"rpm": "requests a minute", "rpd": "requests a day", "tpm": "tokens a minute", "tpd": "tokens a day"}
316
+ return ", ".join(f"{v:,} {words[k]}" for k, v in lim.items() if k in words and v)
317
+
318
+
319
+ def tiles(now: float | None = None) -> list[dict]:
320
+ """Every tile, in the saved order (out first, then soonest reset, for new ones)."""
321
+ import catalog
322
+ import learn
323
+ import ledger
324
+ import quota
325
+ import route
326
+ now = time.time() if now is None else now
327
+ tags, live = _tags(), locked.read_json(LIVE)
328
+ resting, learned = learn.cooldowns(), learn.learned()
329
+ day_rows = list(ledger.rows(now - 86400))
330
+ utc_rows = [r for r in day_rows if r["t"] >= _utc_midnight(now)]
331
+ entered = catalog.connection_limits()
332
+ regs = __import__("endpoints").registrations()
333
+ out = []
334
+ for tag, aliases in tags.items():
335
+ host = route.connection_of(aliases[0])
336
+ mine = set(aliases)
337
+ limits = {a: _limit_text({k: ev.get("value") for k, ev in ((learned.get(a) or {}).get("limits") or {}).items()})
338
+ for a in aliases}
339
+ limits = {a: t for a, t in limits.items() if t}
340
+ rows = [r for r in day_rows if r.get("alias") in mine]
341
+ t = {"id": tag, "tag": tag, "host": host, "bench": len(aliases), "kind": "models", "groups": [],
342
+ "pool": [], "notes": [], "conn_limit": _limit_text(entered.get(host) or {}),
343
+ "models": _model_rows(aliases, rows, resting, limits),
344
+ "refresh": "records", "check": False, "plan": "", "name": ""}
345
+ got = live.get(tag) or {}
346
+ if tag.split("/")[0] == "cli-proxy-api":
347
+ t["refresh"] = "live"
348
+ if got.get("groups"):
349
+ t.update(kind="groups", groups=got["groups"], name=got.get("name", ""), plan=got.get("plan", ""))
350
+ elif tag == "openrouter":
351
+ t["refresh"] = "live" if _openrouter_endpoint() else "records"
352
+ t["kind"] = "pool"
353
+ t["pool"] = _openrouter_pool(aliases, utc_rows, regs, got, learned, now)
354
+ if t["kind"] == "models": # the headers' windows, when any is in force
355
+ recs = [r for r in quota.live(host, now=now) if r.get("alias") in mine
356
+ and r.get("window") != "retry-after" and r.get("limit")]
357
+ by: dict[str, list] = {}
358
+ for r in recs:
359
+ by.setdefault(r["alias"], []).append(
360
+ {"label": r["metric"] + ("" if r["window"] == "unknown" else " · " + r["window"]),
361
+ "left": round(100 * (r.get("remaining") or 0) / r["limit"]),
362
+ "remaining": r.get("remaining"), "of": r["limit"], "reset_at": r.get("reset_at")})
363
+ if by:
364
+ t["kind"] = "groups"
365
+ t["groups"] = [{"label": a if len(by) > 1 else "", "windows": w} for a, w in sorted(by.items())]
366
+ seen = [_ts(r.get("observed_at")) or 0 for r in recs]
367
+ if seen:
368
+ got = {"at": _iso(max(seen)), "how": "seen"}
369
+ t["check"] = t["refresh"] == "records" and _headers_seen(tag)
370
+ last = max((r["t"] for r in rows if r.get("status") == "ok"), default=None)
371
+ t["fresh"] = ({"how": "live", "at": got["at"]} if got.get("at") and got.get("how") != "seen"
372
+ else {"how": "seen", "at": got["at"]} if got.get("at")
373
+ else {"how": "answered", "at": _iso(last)} if last else {"how": "none", "at": None})
374
+ t["live"] = sum(1 for a in aliases if a not in resting)
375
+ resets = [_ts(w.get("reset_at")) for g in t["groups"] for w in g["windows"]] \
376
+ + [_ts(p.get("reset_at")) for p in t["pool"]]
377
+ resets = [r for r in resets if r and r > now]
378
+ t["next_reset"] = _iso(min(resets)) if resets else None
379
+ t["out"] = (t["live"] == 0 or any(w["left"] <= 0 for g in t["groups"] for w in g["windows"])
380
+ or any(p.get("left") == 0 for p in t["pool"]))
381
+ t["notes"] = list(got.get("notes") or [])
382
+ out.append(t)
383
+ return ordered(out)
384
+
385
+
386
+ def _openrouter_pool(aliases, utc_rows, regs, got, learned, now) -> list[dict]:
387
+ """A bar per OpenRouter key the bench uses: free-model requests since
388
+ 00:00 UTC, the bench's own count, against 50 or 1,000 a day."""
389
+ import endpoints
390
+ import quota
391
+ seen_429 = locked.read_json(quota.FILE).get("openrouter") or {}
392
+ by_ep: dict[str, list[str]] = {}
393
+ for a in aliases:
394
+ by_ep.setdefault(endpoints.endpoint_name(regs.get(a) or {}) or "?", []).append(a)
395
+ bars = []
396
+ for ep, eps in sorted(by_ep.items()):
397
+ used = sum(r.get("attempts") or 1 for r in utc_rows
398
+ if r.get("alias") in eps and str(r.get("served", "")).endswith(":free"))
399
+ # What OpenRouter stated wins (learned, else the last 429's, kept a day
400
+ # past reset); the key's free tier only guesses, and was seen wrong.
401
+ stated = [((learned.get(a) or {}).get("limits") or {}).get("rpd", {}).get("value") for a in eps]
402
+ stated += [r.get("limit") for a in eps for r in (seen_429.get(a) or {}).values()
403
+ if isinstance(r, dict) and r.get("window") in ("day", "unknown")]
404
+ limit = min([s for s in stated if s] or [0]) or None
405
+ if not limit and got.get("endpoint") == ep:
406
+ limit = 50 if got.get("free_tier") else 1000
407
+ via = "direct key" if not endpoints.hub(ep) else f"via {ep}"
408
+ bar = {"label": f"free requests today · {via}", "used": used, "of": limit,
409
+ "left": max(0, round(100 * (limit - used) / limit)) if limit else None,
410
+ "reset_at": _iso(_utc_midnight(now) + 86400), "models": len(eps)}
411
+ bars.append(bar)
412
+ if got.get("limit"):
413
+ lim, rem = float(got["limit"]), float(got.get("remaining") or 0)
414
+ bars.append({"label": "credits", "used": round(lim - rem, 2), "of": lim,
415
+ "left": round(100 * rem / lim) if lim else 0, "reset_at": None, "models": 0})
416
+ return bars
417
+
418
+
419
+ def ordered(tiles_: list[dict]) -> list[dict]:
420
+ """The saved order first (config/ui.json quota_order), then the rest: out
421
+ first, then the soonest reset, then by tag."""
422
+ saved = locked.read_json(DATA / "config" / "ui.json").get("quota_order") or []
423
+ pos = {t: i for i, t in enumerate(saved)}
424
+ return sorted(tiles_, key=lambda t: (pos.get(t["id"], len(pos)), not t["out"],
425
+ t["next_reset"] or "9999", t["id"]))
426
+
427
+
428
+ def save_order(order) -> dict:
429
+ if order is not None and not (isinstance(order, list) and all(isinstance(t, str) for t in order)):
430
+ return {"ok": False, "errors": ["order must be a list of tags"]}
431
+ locked.update(DATA / "config" / "ui.json", lambda d: d.update(quota_order=order or []))
432
+ return {"ok": True, "errors": []}
433
+
434
+
435
+ def _selftest() -> None:
436
+ ag = {"groups": [{"displayName": "Gemini Models", "buckets": [
437
+ {"window": "weekly", "resetTime": "2026-10-30T23:01:51Z", "remainingFraction": 0.620195},
438
+ {"window": "5h", "resetTime": "2026-10-29T23:11:05Z", "remainingFraction": 0.9362293}]},
439
+ {"displayName": "Gemini 4 Argon", "buckets": [
440
+ {"window": "weekly", "resetTime": "2026-10-31T01:20:30Z", "remainingFraction": 0.40175587}]}]}
441
+ g = antigravity_groups(ag)
442
+ assert [w["label"] for w in g[0]["windows"]] == ["5 hours", "week"], g
443
+ assert [w["left"] for w in g[0]["windows"]] == [94, 62] and g[1]["windows"][0]["left"] == 40, g
444
+ cx = {"plan_type": "free", "rate_limit": {"primary_window": {
445
+ "used_percent": 100, "limit_window_seconds": 2592000, "reset_at": 1795707512}, "secondary_window": None}}
446
+ c = codex_groups(cx)
447
+ assert c == [{"label": "", "windows": [{"label": "month", "left": 0,
448
+ "reset_at": "2026-11-26T15:38:32+00:00"}]}], c
449
+ assert codex_groups({}) == [] and antigravity_groups({}) == []
450
+ assert _seconds_name(18000) == "5 hours" and _seconds_name(7200) == "2 hours"
451
+ assert _utc_midnight(1793306000) == dt.datetime(2026, 10, 29, tzinfo=UTC).timestamp()
452
+ assert _limit_text({"rpm": 20, "tpd": 200000}) == "20 requests a minute, 200,000 tokens a day"
453
+ print("quotaview selftest: ok")
454
+
455
+
456
+ if __name__ == "__main__":
457
+ if "--selftest" in sys.argv:
458
+ _selftest()
459
+ else:
460
+ print(json.dumps(tiles(), indent=1))
@@ -168,6 +168,7 @@ def _start(name, args, due, running) -> None:
168
168
 
169
169
 
170
170
  _stop = threading.Event()
171
+ RUNNING: dict = {} # job name -> True while it runs (serve.restart reads it)
171
172
 
172
173
 
173
174
  def _loop() -> None:
@@ -180,7 +181,7 @@ def _loop() -> None:
180
181
  except TimeoutError:
181
182
  return # another dashboard already runs it
182
183
  try:
183
- running: dict = {}
184
+ running = RUNNING
184
185
  conf = settings()
185
186
  _log(f"scheduler started, mode {conf['mode']}, jobs {', '.join(enabled(conf))}")
186
187
  while True:
@@ -1,6 +1,10 @@
1
- """Public model lists the bench reads: models.dev and FreeLLMAPI's free catalog.
1
+ """Public model lists the bench reads: FreeLLMAPI's free catalog and models.dev.
2
2
 
3
- python metrics/sources.py # fetch both now (normally once a day)
3
+ python metrics/sources.py # fetch them now (normally once a day)
4
+
5
+ Discover leads with FreeLLMAPI (easier for a person to follow: a page per
6
+ model, its limits, ranks and quirks) and fills the rest from models.dev, which
7
+ alone has prices, paid models, providers' API base URLs and modalities.
4
8
 
5
9
  WHY THESE TWO
6
10
  -------------
@@ -37,6 +41,9 @@ CACHE = DATA / "runs" / "cache"
37
41
  FEEDS = {
38
42
  "modelsdev": "https://models.dev/api.json",
39
43
  "freellmapi": "https://api.freellmapi.co/v1/latest",
44
+ # The model pages freellmapi.co has (its sitemap): a page exists only for
45
+ # what its monthly snapshot lists, so Discover links only to those.
46
+ "freellmapi-pages": "https://freellmapi.co/sitemap-core.xml",
40
47
  }
41
48
  MAX_AGE_S = 20 * 3600
42
49
 
@@ -90,7 +97,8 @@ def fetch(name: str, force: bool = False) -> dict:
90
97
  return json.loads(path.read_text(encoding="utf-8"))
91
98
  req = urllib.request.Request(FEEDS[name], headers={"User-Agent": "benchwarmers"})
92
99
  with urllib.request.urlopen(req, timeout=90) as r:
93
- data = json.load(r)
100
+ data = (json.load(r) if not FEEDS[name].endswith(".xml") else
101
+ {"slugs": sorted(set(re.findall(r"/model/([^<\"/]+)<", r.read().decode("utf-8"))))})
94
102
  CACHE.mkdir(parents=True, exist_ok=True)
95
103
  tmp = path.with_suffix(".part")
96
104
  tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
@@ -144,6 +152,20 @@ def freellmapi_entry(served: str, platform: str | None = None) -> dict | None:
144
152
  return None
145
153
 
146
154
 
155
+ def freellmapi_name(display_name: str) -> str:
156
+ """A FreeLLMAPI display name without its trailing note, "Llama 4 Scout (CF)"
157
+ -> "Llama 4 Scout": the name freellmapi.co gives the model on every platform."""
158
+ return re.sub(r"\s*\([^)]*\)\s*$", "", display_name or "").strip()
159
+
160
+
161
+ def freellmapi_page(display_name: str, pages: set[str]) -> str | None:
162
+ """The model's freellmapi.co page, if `pages` (its sitemap) has it. The site
163
+ names a page by the display name without its note, lowercased, each run of
164
+ other characters one dash (buildBundles in its app.js, read 2026-10-28)."""
165
+ slug = "-".join(re.findall(r"[a-z0-9]+", freellmapi_name(display_name).lower()))
166
+ return f"https://freellmapi.co/model/{slug}" if slug in pages else None
167
+
168
+
147
169
  def is_free(cost: dict | None) -> bool | None:
148
170
  if not cost:
149
171
  return None
@@ -156,9 +178,10 @@ def main() -> None:
156
178
  for name in FEEDS:
157
179
  try:
158
180
  data = fetch(name, force=True)
159
- size = len(data) if name == "modelsdev" else len(data.get("models", []))
160
- print(f" {name}: {size} {'providers' if name == 'modelsdev' else 'models'}"
161
- f" at {dt.datetime.now():%H:%M}")
181
+ size, unit = {"modelsdev": (len(data), "providers"),
182
+ "freellmapi-pages": (len(data.get("slugs", [])), "model pages")}.get(
183
+ name, (len(data.get("models", [])), "models"))
184
+ print(f" {name}: {size} {unit} at {dt.datetime.now():%H:%M}")
162
185
  except Exception as exc: # noqa: BLE001
163
186
  print(f" {name}: could not fetch ({type(exc).__name__}: {exc}); keeping the cached copy")
164
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendellresearch-ai/benchwarmers",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
5
5
  "mcpName": "io.github.WendellResearch-AI/benchwarmers",
6
6
  "license": "MIT",
package/scripts/check.py CHANGED
@@ -39,6 +39,7 @@ CHECKS = [
39
39
  ("metrics/bench_collections.py", [], False),
40
40
  ("metrics/learn.py", ["--selftest"], False),
41
41
  ("metrics/quota.py", ["--selftest"], False),
42
+ ("metrics/quotaview.py", ["--selftest"], False),
42
43
  ("metrics/discover.py", ["--selftest"], False),
43
44
  ("metrics/test_decide.py", [], False),
44
45
  ("metrics/wrong.py", [], False),
@@ -278,16 +278,21 @@ def discover_add() -> None:
278
278
  os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "mgmt-test"
279
279
 
280
280
  onboarded = []
281
- serve.queue_onboard = lambda items, who="ui": onboarded.append((items[0]["served"], items[0]["endpoint"])) or {
281
+ serve.queue_onboard = lambda items, who="ui": onboarded.append(
282
+ (items[0]["served"], items[0]["endpoint"], items[0]["mode"], items[0]["vision"])) or {
282
283
  "ok": True, "errors": [], "items": []}
283
284
  owui.add = lambda c, m: (m, [f"owui added {m} on {c}"])
284
285
  r = serve.discover_add("groq", "vendor/new", endpoint="lite")
285
- assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite"), r
286
- r = serve.discover_add("openrouter", "vendor/y:free", endpoint="cpa")
287
- assert r["ok"] and onboarded[-1] == ("vendor/y:free", "cpa"), r
286
+ assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite", "full", False), r
287
+ # The confirm's Onboard as and Vision box reach the queue; a bad mode is refused before the hub has it.
288
+ r = serve.discover_add("openrouter", "vendor/y:free", endpoint="cpa", mode="quick-adhoc", vision=True)
289
+ assert r["ok"] and onboarded[-1] == ("vendor/y:free", "cpa", "quick-adhoc", True), r
288
290
  assert [m for m, p, _b in SEEN if "management" in p and m == "PATCH"] == ["PATCH"]
291
+ before = len(SEEN)
292
+ assert "mode must be" in serve.discover_add("openrouter", "vendor/q", endpoint="cpa", mode="slow")["errors"][0]
293
+ assert len(SEEN) == before, SEEN[before:]
289
294
  r = serve.discover_add("groq-api", "vendor/z") # no endpoint: Open WebUI, as before
290
- assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui"), r
295
+ assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui", "full", False), r
291
296
  assert serve.discover_add("groq-api", "vendor/z", endpoint="owui")["ok"]
292
297
  assert "not a hub" in serve.discover_add("groq", "v/z", endpoint="plain")["errors"][0]
293
298
  changes = [json.loads(line)["field"] for line in serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()]
@@ -132,6 +132,15 @@ def stranger(fake: str) -> None:
132
132
  assert c["ok"] and not c["errors"], c
133
133
  assert sorted(x["served"] for x in c["candidates"]) == ["vendor/c", "vendor/d", "vendor/e", "vendor/f"], c
134
134
  assert {x["group"] for x in c["candidates"]} == {"openrouter (direct)"} and not any(x["new"] for x in c["candidates"])
135
+ # Queued or onboarding now: left out until the queue is done with it.
136
+ kept_items = serve._OQ["items"]
137
+ serve._OQ["items"] = [{"served": "vendor/c", "endpoint": "openrouter", "state": "running"},
138
+ {"served": "vendor/d", "endpoint": "openrouter", "state": "done"}]
139
+ try:
140
+ busy = sorted(x["served"] for x in serve.onboard_candidates()["candidates"])
141
+ finally:
142
+ serve._OQ["items"] = kept_items
143
+ assert busy == ["vendor/d", "vendor/e", "vendor/f"], busy
135
144
 
136
145
  # The served check: the registered models only, not the aggregator's catalogue.
137
146
  assert check_deprecations.served() == {"vendor/a:free", "vendor/b:free"}
@@ -290,6 +290,16 @@ def main() -> int:
290
290
  check("rate limit (not sent" in r["error"] and "a calendar month" in r["error"] and "this month" in r["error"],
291
291
  "rpmo 3: the fourth call this month is refused", r["error"])
292
292
  check(client.chat("t-ok", "hi")["text"] != "", "another connection is not held by that cap")
293
+ # Seating asks the same question first (gateway.refused_now): held past any
294
+ # seat's wait, so not seated; a model with room is.
295
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
296
+ import gateway
297
+ import pacing
298
+ h = pacing.held("t-month", "monthprov")
299
+ check(h is not None and h[0] > gateway.SEAT_TIMEOUT and "this month" in h[1], "held() sees the monthly cap", h)
300
+ check(pacing.held("t-ok", gateway.route.connection_of("t-ok")) is None, "held() is None with room")
301
+ out = gateway.refused_now(["t-month", "t-ok"], 10, gateway.SEAT_TIMEOUT)
302
+ check(list(out) == ["t-month"] and "rate limit" in out["t-month"], "refused_now names only the held model", out)
293
303
 
294
304
  # The free-tier guard: a model that went paid is held until allowed.
295
305
  before = len(seen.get("paid", []))
@@ -322,8 +332,8 @@ def main() -> int:
322
332
  and "rpmo" not in ((serve.catalog.overrides().get("t-ok") or {}).get("limits") or {}),
323
333
  "a bad rpmo is refused; a model's own limits have none", bad)
324
334
  page = serve.quotas_page()
325
- check("What providers said is left" in page and "fakeprov" in page and "What the bench sent" in page,
326
- "the Quotas page shows the records and the ledger's counts")
335
+ check("id='quota-data'" in page and "fakeprov" in page and "What the bench sent" in page,
336
+ "the Quotas page carries its tiles and the ledger's counts")
327
337
 
328
338
  srv.shutdown()
329
339
  print("\nthe ledger, quotas and pacing hold" if not failures else f"\n{failures} FAILED")