@wendellresearch-ai/benchwarmers 0.5.0 → 0.5.2
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.
- package/PRIVACY.md +9 -2
- package/README.md +11 -6
- package/USAGE.md +58 -21
- package/VERSION +1 -1
- package/metrics/catalog.py +5 -0
- package/metrics/learn.py +25 -1
- package/metrics/quota.py +41 -4
- package/metrics/quotaview.py +366 -34
- package/package.json +1 -1
- package/scripts/auto_retire.py +44 -4
- package/scripts/test_serve_edit.py +9 -0
- package/serve.py +40 -14
- package/suite/run.py +1 -0
- package/ui/bench.css +31 -10
- package/ui/bench.js +156 -72
package/metrics/quotaview.py
CHANGED
|
@@ -12,13 +12,22 @@ A tile takes the shape of what its provider says:
|
|
|
12
12
|
rate-limit headers the built-in client saw (metrics/quota.py)
|
|
13
13
|
pool one allowance the models share: OpenRouter's free requests a
|
|
14
14
|
day, a bar per key, counted from the usage ledger
|
|
15
|
-
models nothing stated:
|
|
15
|
+
models nothing stated: the provider's last day of calls, all together
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
and any of them its day and month caps (caps): yours, else a limit its replies
|
|
18
|
+
state with nothing left said, counted from the ledger. A model is named only
|
|
19
|
+
where a limit is its own. Nothing comes from a provider's docs: they change
|
|
20
|
+
unseen.
|
|
21
|
+
|
|
22
|
+
ONE REFRESH, THREE WAYS (how_refreshed). Three providers can be asked what is
|
|
23
|
+
left for free, and are:
|
|
19
24
|
|
|
20
25
|
OpenRouter GET /key on a direct OpenRouter endpoint: free tier or
|
|
21
|
-
not
|
|
26
|
+
not, and its credits (the free requests' limit comes
|
|
27
|
+
from its 429s, never its docs)
|
|
28
|
+
xKiro GET /v1/usage: free-model tokens left today (to midnight
|
|
29
|
+
UTC) and a plan's spend windows; the key is a direct
|
|
30
|
+
endpoint's, else its Open WebUI connection's (_key_for)
|
|
22
31
|
CLI Proxy API its management route `requests/api-call`, which calls the
|
|
23
32
|
credential's own quota endpoint with the credential's
|
|
24
33
|
token, as its console does (Google's retrieveUserQuotaSummary,
|
|
@@ -27,10 +36,10 @@ and are, when Refresh is pressed:
|
|
|
27
36
|
(endpoints.admin_key, as metrics/cliproxy.py); the proxy is
|
|
28
37
|
on the LAN and calls only providers the credential uses.
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
headers quota.py records.
|
|
33
|
-
|
|
39
|
+
Where a provider has sent limit headers before (Groq; Routeway, through Open
|
|
40
|
+
WebUI too, since the bench streams), Refresh is `check`: a 16-token call to
|
|
41
|
+
one of its models, whose reply headers quota.py records. Everywhere else it
|
|
42
|
+
re-reads the bench's own records, since a call would say nothing.
|
|
34
43
|
|
|
35
44
|
Live reads are kept in runs/quota-live.json, newest per tag.
|
|
36
45
|
"""
|
|
@@ -65,6 +74,23 @@ CODEX_URL = "https://chatgpt.com/backend-api/wham/usage"
|
|
|
65
74
|
CODEX_HEAD = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json",
|
|
66
75
|
"User-Agent": "codex-tui/0.149.1"}
|
|
67
76
|
WINDOW_NAMES = {"5h": "5 hours", "weekly": "week", "daily": "day", "monthly": "month"}
|
|
77
|
+
# Free to ask: no tokens, outside the rate limits (docs.xkiro.com/api/usage, read 2026-10-29).
|
|
78
|
+
XKIRO_HOST = "api.xkiro.com"
|
|
79
|
+
XKIRO_USAGE = "https://api.xkiro.com/v1/usage"
|
|
80
|
+
# A key may read its own usage with completions permission alone; the org's
|
|
81
|
+
# needs manage (docs.requesty.ai/features/key-management-api, tried 2026-10-30).
|
|
82
|
+
REQUESTY_HOST = "router.requesty.ai"
|
|
83
|
+
REQUESTY_KEYS = "https://api-v2.requesty.ai/v1/manage/apikey"
|
|
84
|
+
REQUESTY_MODELS = "https://router.requesty.ai/v1/models" # public; a free model is priced at zero
|
|
85
|
+
# The one figure from a provider's published text, not its replies: the file
|
|
86
|
+
# build.nvidia.com reads for its "Your API Rate Limit" badge, the same for
|
|
87
|
+
# everyone. Watched (watch_nvidia): a change or a failure to read it is an alert.
|
|
88
|
+
NVIDIA_COPY = "https://build.nvidia.com/api/runtime/content/copy.yaml"
|
|
89
|
+
# A day or month cap is drawn counted from the ledger, as pacing.py counts:
|
|
90
|
+
# yours from Endpoints (enforced), else one a provider states in its replies
|
|
91
|
+
# without saying what is left (Cohere's x-endpoint-monthly-call-limit; shown,
|
|
92
|
+
# never enforced). Never one from a provider's docs: they change unseen.
|
|
93
|
+
COUNTED = {"rpd": ("requests", "day"), "tpd": ("tokens", "day"), "rpmo": ("requests", "month")}
|
|
68
94
|
|
|
69
95
|
|
|
70
96
|
def _iso(t: float | None) -> str | None:
|
|
@@ -118,6 +144,24 @@ def codex_groups(body: dict) -> list[dict]:
|
|
|
118
144
|
return [{"label": "", "windows": wins}] if wins else []
|
|
119
145
|
|
|
120
146
|
|
|
147
|
+
def xkiro_groups(body: dict, now: float) -> list[dict]:
|
|
148
|
+
"""/v1/usage -> the free models' tokens today (to midnight UTC), then the
|
|
149
|
+
plan's spend windows (USD, as strings), where there is a plan."""
|
|
150
|
+
out, ft = [], body.get("free_tokens") or {}
|
|
151
|
+
if ft.get("limit_per_day"):
|
|
152
|
+
lim, rem = int(ft["limit_per_day"]), int(ft.get("remaining") or 0)
|
|
153
|
+
out.append({"label": "free models", "windows": [
|
|
154
|
+
{"label": "tokens · day", "left": round(100 * rem / lim), "remaining": rem, "of": lim,
|
|
155
|
+
"reset_at": _iso(_utc_midnight(now) + 86400)}]})
|
|
156
|
+
wins = [{"label": _seconds_name(w.get("window_sec")) + " · spend",
|
|
157
|
+
"left": round(100 * float(w.get("remaining_usd") or 0) / float(w["cap_usd"])),
|
|
158
|
+
"reset_at": _iso(now + float(w.get("resets_in_sec") or 0))}
|
|
159
|
+
for w in body.get("windows") or [] if isinstance(w, dict) and float(w.get("cap_usd") or 0) > 0]
|
|
160
|
+
if wins:
|
|
161
|
+
out.append({"label": "plan", "windows": wins})
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
121
165
|
# ---- live reads --------------------------------------------------------------
|
|
122
166
|
|
|
123
167
|
def _get_json(url: str, headers: dict, data: bytes | None = None, timeout: int = 20) -> dict:
|
|
@@ -147,6 +191,106 @@ def read_openrouter() -> dict:
|
|
|
147
191
|
"limit": data.get("limit"), "remaining": data.get("limit_remaining"), "usage": data.get("usage")}
|
|
148
192
|
|
|
149
193
|
|
|
194
|
+
def _key_for(host: str) -> str:
|
|
195
|
+
"""A key for `host`: a direct endpoint's, else the Open WebUI connection's
|
|
196
|
+
(read with the hub's admin key, as owui.py reads its connections)."""
|
|
197
|
+
import endpoints
|
|
198
|
+
for name, entry in endpoints.load().items():
|
|
199
|
+
if (urllib.parse.urlsplit(entry.get("base", "")).hostname or "") == host and not endpoints.hub(name):
|
|
200
|
+
if endpoints.key(name):
|
|
201
|
+
return endpoints.key(name)
|
|
202
|
+
import owui
|
|
203
|
+
cfg = owui._call("/openai/config")
|
|
204
|
+
for url, key in zip(cfg.get("OPENAI_API_BASE_URLS") or [], cfg.get("OPENAI_API_KEYS") or []):
|
|
205
|
+
if (urllib.parse.urlsplit(url).hostname or "") == host and key:
|
|
206
|
+
return key
|
|
207
|
+
raise ValueError(f"no key for {host}: neither an endpoint nor an Open WebUI connection has one")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def read_xkiro() -> dict:
|
|
211
|
+
"""Only the figures are kept: the reply also names the account's owner."""
|
|
212
|
+
try: # Cloudflare refuses Python's own User-Agent (1010)
|
|
213
|
+
body = _get_json(XKIRO_USAGE, {"Authorization": f"Bearer {_key_for(XKIRO_HOST)}", "User-Agent": "benchwarmers"})
|
|
214
|
+
except urllib.error.HTTPError as exc:
|
|
215
|
+
raise ValueError(f"xKiro answered {exc.code} on /v1/usage") from None
|
|
216
|
+
return {"groups": xkiro_groups(body, time.time()), "plan": str(body.get("plan") or ""), "notes": []}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def requesty_free(usage: dict, models: dict) -> int:
|
|
220
|
+
"""Requests to free models in a usage reply's (first) period. Its
|
|
221
|
+
model_used drops the provider (`muse-glimmer-30b` for nvidia/muse-glimmer-30b)."""
|
|
222
|
+
free = {str(m.get("id", "")).split("/")[-1] for m in models.get("data") or []
|
|
223
|
+
if not float(m.get("input_price") or 0) and not float(m.get("output_price") or 0)}
|
|
224
|
+
day = next(iter((usage.get("usage") or {}).values()), {})
|
|
225
|
+
return sum(r.get("total_requests") or 0 for r in day.get("grouped_data") or []
|
|
226
|
+
if str((r.get("group_by_values") or {}).get("model_used", "")).split("/")[-1] in free)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def read_requesty() -> dict:
|
|
230
|
+
"""Requesty's own count of this key's free-model requests since midnight
|
|
231
|
+
UTC (its usage API, whose periods are UTC dates). Its free limit is not
|
|
232
|
+
reported anywhere: only a 429 that states one gives the pool its `of`."""
|
|
233
|
+
head = {"Authorization": f"Bearer {_key_for(REQUESTY_HOST)}", "User-Agent": "benchwarmers"}
|
|
234
|
+
try:
|
|
235
|
+
me = _get_json(REQUESTY_KEYS + "/self", head)
|
|
236
|
+
start = dt.datetime.fromtimestamp(_utc_midnight(time.time()), UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
237
|
+
usage = _get_json(f"{REQUESTY_KEYS}/{me['id']}/usage?start={start}&resolution=day&group_by=model_used", head)
|
|
238
|
+
models = _get_json(REQUESTY_MODELS, {"User-Agent": "benchwarmers"})
|
|
239
|
+
except urllib.error.HTTPError as exc:
|
|
240
|
+
raise ValueError(f"Requesty answered {exc.code} on its usage API") from None
|
|
241
|
+
return {"free_used": requesty_free(usage, models), "notes": []}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def nvidia_figures(body: dict) -> dict:
|
|
245
|
+
"""copy.yaml (JSON in fact) -> {rpd, rpm_text}; ValueError when its
|
|
246
|
+
rateLimits or a day's number is missing: the file changed shape."""
|
|
247
|
+
import re
|
|
248
|
+
rl = body.get("rateLimits") if isinstance(body, dict) else None
|
|
249
|
+
if not isinstance(rl, dict):
|
|
250
|
+
raise ValueError("it no longer has rateLimits")
|
|
251
|
+
m = re.search(r"([\d,]+)\s+requests?\s+(?:per|a)\s+day", str(rl.get("requestsPerDay") or ""))
|
|
252
|
+
if not m:
|
|
253
|
+
raise ValueError(f"its requestsPerDay no longer says a number a day: {str(rl.get('requestsPerDay'))[:60]!r}")
|
|
254
|
+
return {"rpd": int(m.group(1).replace(",", "")), "rpm_text": str(rl.get("requestsPerMinute") or "")}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _nvidia_on_bench() -> bool:
|
|
258
|
+
return any("nvidia" in (t + " " + _host(t)).lower() for t in _tags())
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def watch_nvidia() -> dict:
|
|
262
|
+
"""Read NVIDIA's stated figures and keep the last good read (LIVE
|
|
263
|
+
"_nvidia"). A change raises an alert you dismiss; a failure (moved,
|
|
264
|
+
broken, a new shape) raises one that clears once it reads again. Run
|
|
265
|
+
daily by the catalog job and by the tile's Refresh; nothing without an
|
|
266
|
+
NVIDIA connection on the bench."""
|
|
267
|
+
import alerts
|
|
268
|
+
prev = locked.read_json(LIVE).get("_nvidia") or {}
|
|
269
|
+
if not _nvidia_on_bench():
|
|
270
|
+
return prev
|
|
271
|
+
try:
|
|
272
|
+
try:
|
|
273
|
+
body = _get_json(NVIDIA_COPY, {"User-Agent": "benchwarmers"})
|
|
274
|
+
except urllib.error.HTTPError as exc:
|
|
275
|
+
raise ValueError(f"build.nvidia.com answered {exc.code}") from None
|
|
276
|
+
got = dict(nvidia_figures(body), at=_iso(time.time()))
|
|
277
|
+
except (ValueError, OSError) as exc:
|
|
278
|
+
since = f" (last read {prev['at'][:10]})" if prev.get("at") else ""
|
|
279
|
+
alerts.raise_alert("limits:nvidia:unread", "limits", f"NVIDIA's limits file could not be read: {exc}",
|
|
280
|
+
action=f"It may have moved from {NVIDIA_COPY}. The NVIDIA tile keeps the figures"
|
|
281
|
+
f" it read before{since}.")
|
|
282
|
+
locked.update(LIVE, lambda d: d.setdefault("_nvidia", {}).update(broken=str(exc)), ensure_ascii=False)
|
|
283
|
+
return dict(prev, broken=str(exc))
|
|
284
|
+
alerts.clear("limits:nvidia:unread")
|
|
285
|
+
if prev.get("rpd") and (prev["rpd"], prev.get("rpm_text")) != (got["rpd"], got["rpm_text"]):
|
|
286
|
+
alerts.raise_alert("limits:nvidia:changed", "limits",
|
|
287
|
+
f"NVIDIA changed its stated limits: {prev['rpd']:,} a day, {prev.get('rpm_text')!r}"
|
|
288
|
+
f" -> {got['rpd']:,} a day, {got['rpm_text']!r}",
|
|
289
|
+
action="The NVIDIA tile counts against the new figures.", dismiss=True)
|
|
290
|
+
locked.update(LIVE, lambda d: d.update({"_nvidia": got}), ensure_ascii=False)
|
|
291
|
+
return got
|
|
292
|
+
|
|
293
|
+
|
|
150
294
|
def _cpa_hub() -> tuple[str, str]:
|
|
151
295
|
"""(management base, key) of the CLI Proxy API hub; ValueError when there is none."""
|
|
152
296
|
import endpoints
|
|
@@ -235,21 +379,72 @@ def read_cliproxy(tag: str, sibling_vendors: set[str]) -> dict:
|
|
|
235
379
|
|
|
236
380
|
|
|
237
381
|
def refresh(tag: str) -> dict:
|
|
238
|
-
"""
|
|
239
|
-
|
|
240
|
-
|
|
382
|
+
"""The tile's one Refresh, which picks its own way (how_refreshed): ask the
|
|
383
|
+
provider when it can be asked for free; else, where it states limits in its
|
|
384
|
+
replies, one 16-token call (check); else the bench's own records.
|
|
385
|
+
{"ok", "how": "live"|"check"|"records", "note"}."""
|
|
386
|
+
how = how_refreshed(tag)
|
|
387
|
+
if "nvidia" in (tag + " " + _host(tag)).lower():
|
|
388
|
+
got = watch_nvidia()
|
|
389
|
+
return {"ok": True, "how": "live", "note": f"NVIDIA's limits file could not be read: {got['broken']};"
|
|
390
|
+
" showing the figures read before" if got.get("broken") else ""}
|
|
391
|
+
if how == "check":
|
|
392
|
+
r = check(tag)
|
|
393
|
+
if r.get("ok"):
|
|
394
|
+
return dict(r, how="check")
|
|
395
|
+
return {"ok": True, "how": "records", "note": "; ".join(r.get("errors") or []) + "; shown from the bench's own records"}
|
|
396
|
+
if how == "records":
|
|
397
|
+
return {"ok": True, "how": "records",
|
|
398
|
+
"note": "this provider states no limits to ask about: shown from the bench's own records"}
|
|
399
|
+
if tag == "openrouter":
|
|
241
400
|
got = read_openrouter()
|
|
242
|
-
elif tag
|
|
401
|
+
elif _host(tag) == XKIRO_HOST:
|
|
402
|
+
got = read_xkiro()
|
|
403
|
+
elif _host(tag) == REQUESTY_HOST:
|
|
404
|
+
got = read_requesty()
|
|
405
|
+
else:
|
|
243
406
|
vendors = {t.split("/", 1)[1] for t in _tags() if t.startswith("cli-proxy-api/")}
|
|
244
407
|
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
408
|
got["at"] = _iso(time.time())
|
|
249
409
|
locked.update(LIVE, lambda d: d.update({tag: got}), ensure_ascii=False)
|
|
250
410
|
return {"ok": True, "how": "live", "note": "; ".join(got.get("notes") or [])}
|
|
251
411
|
|
|
252
412
|
|
|
413
|
+
def about(tag: str) -> dict:
|
|
414
|
+
"""How a tile's provider is reached, for its Models popup: {connection,
|
|
415
|
+
routes: [{via, url}]}, a direct endpoint by that name and each Open WebUI
|
|
416
|
+
connection named so (scripts/provider_tags.py's names). URLs only:
|
|
417
|
+
/openai/config returns Open WebUI's keys too, never read here."""
|
|
418
|
+
import endpoints
|
|
419
|
+
host, routes = tag.split("/")[0], []
|
|
420
|
+
if host in endpoints.load() and not endpoints.hub(host):
|
|
421
|
+
routes.append({"via": f"its own endpoint, {host}", "url": endpoints.base(host)})
|
|
422
|
+
hub = endpoints.find_hub("openwebui")
|
|
423
|
+
if hub:
|
|
424
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
425
|
+
import provider_tags
|
|
426
|
+
urls = provider_tags.get(hub, "/openai/config").get("OPENAI_API_BASE_URLS", [])
|
|
427
|
+
routes += [{"via": f"an Open WebUI connection on {hub}", "url": url}
|
|
428
|
+
for name, url in zip(provider_tags.connection_names(hub), urls) if name == host]
|
|
429
|
+
return {"connection": tag, "routes": routes}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _host(tag: str) -> str:
|
|
433
|
+
"""The connection a tile's models are called through (route.connection_of)."""
|
|
434
|
+
import route
|
|
435
|
+
aliases = _tags().get(tag) or []
|
|
436
|
+
return route.connection_of(aliases[0]) if aliases else tag
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def how_refreshed(tag: str) -> str:
|
|
440
|
+
""""live" (a free read), "check" (one short call whose headers say what is
|
|
441
|
+
left) or "records" (nothing to ask: the bench's own records)."""
|
|
442
|
+
if (tag.split("/")[0] == "cli-proxy-api" or tag == "openrouter" and _openrouter_endpoint()
|
|
443
|
+
or _host(tag) in (XKIRO_HOST, REQUESTY_HOST) or "nvidia" in (tag + " " + _host(tag)).lower()):
|
|
444
|
+
return "live"
|
|
445
|
+
return "check" if _headers_seen(tag) else "records"
|
|
446
|
+
|
|
447
|
+
|
|
253
448
|
def check(tag: str) -> dict:
|
|
254
449
|
"""One 16-token call to a model on `tag` that is free to call; the
|
|
255
450
|
built-in client records what its reply headers say (quota.observe)."""
|
|
@@ -312,10 +507,41 @@ def _model_rows(aliases, rows, resting, limits) -> list[dict]:
|
|
|
312
507
|
|
|
313
508
|
|
|
314
509
|
def _limit_text(lim: dict) -> str:
|
|
315
|
-
words = {"rpm": "requests a minute", "rpd": "requests a day", "tpm": "tokens a minute", "tpd": "tokens a day"
|
|
510
|
+
words = {"rpm": "requests a minute", "rpd": "requests a day", "tpm": "tokens a minute", "tpd": "tokens a day",
|
|
511
|
+
"rpmo": "requests a calendar month"}
|
|
316
512
|
return ", ".join(f"{v:,} {words[k]}" for k, v in lim.items() if k in words and v)
|
|
317
513
|
|
|
318
514
|
|
|
515
|
+
def caps(host: str, entered: dict, stated: dict, day_rows: list, month_rows, now: float) -> list[dict]:
|
|
516
|
+
"""The connection's day and month caps as groups of windows, each counted
|
|
517
|
+
from the bench's ledger since midnight UTC or the month's start (what a
|
|
518
|
+
provider's calendar counts): yours, else `stated` ({axis: (limit, source,
|
|
519
|
+
detail)}: what its replies say with nothing left given, or NVIDIA's site).
|
|
520
|
+
`month_rows` is called only when a month cap needs it."""
|
|
521
|
+
mine = {k: (v, "your cap", "Set on Endpoints, Rate limits per connection; the bench keeps to it.")
|
|
522
|
+
for k, v in (entered.get(host) or {}).items() if k in COUNTED and v}
|
|
523
|
+
for axis, (n, source, detail) in stated.items():
|
|
524
|
+
mine.setdefault(axis, (n, source, detail + " Shown, not enforced: set it on Endpoints to have the bench"
|
|
525
|
+
" keep to it."))
|
|
526
|
+
by: dict[str, list] = {}
|
|
527
|
+
about: dict[str, str] = {}
|
|
528
|
+
for axis, (n, source, detail) in mine.items():
|
|
529
|
+
about[source] = detail
|
|
530
|
+
metric, window = COUNTED[axis]
|
|
531
|
+
start, end = ((_utc_midnight(now), _utc_midnight(now) + 86400) if window == "day"
|
|
532
|
+
else (__import__("ledger").month_start(now), __import__("ledger").next_month(now)))
|
|
533
|
+
rows = [r for r in (day_rows if window == "day" else month_rows())
|
|
534
|
+
if r.get("connection") == host and r["t"] >= start]
|
|
535
|
+
used = sum((r.get("attempts") or 0) if metric == "requests"
|
|
536
|
+
else (r.get("tokens_in") or 0) + (r.get("tokens_out") or 0) for r in rows)
|
|
537
|
+
left = max(0, n - used)
|
|
538
|
+
by.setdefault(source, []).append({"label": f"{metric} · {window}", "left": round(100 * left / n),
|
|
539
|
+
"remaining": left, "of": n, "reset_at": _iso(end)})
|
|
540
|
+
return [{"label": f"{source} · counted by the bench", "title": about[source] + " Counted from the bench's"
|
|
541
|
+
" own calls: calls made elsewhere with the same key are not in it.", "windows": w}
|
|
542
|
+
for source, w in by.items()]
|
|
543
|
+
|
|
544
|
+
|
|
319
545
|
def tiles(now: float | None = None) -> list[dict]:
|
|
320
546
|
"""Every tile, in the saved order (out first, then soonest reset, for new ones)."""
|
|
321
547
|
import catalog
|
|
@@ -330,6 +556,12 @@ def tiles(now: float | None = None) -> list[dict]:
|
|
|
330
556
|
utc_rows = [r for r in day_rows if r["t"] >= _utc_midnight(now)]
|
|
331
557
|
entered = catalog.connection_limits()
|
|
332
558
|
regs = __import__("endpoints").registrations()
|
|
559
|
+
month: list = [] # this month's ledger, read once if a month cap asks
|
|
560
|
+
|
|
561
|
+
def month_rows() -> list:
|
|
562
|
+
if not month:
|
|
563
|
+
month.append(list(ledger.rows(ledger.month_start(now))))
|
|
564
|
+
return month[0]
|
|
333
565
|
out = []
|
|
334
566
|
for tag, aliases in tags.items():
|
|
335
567
|
host = route.connection_of(aliases[0])
|
|
@@ -338,22 +570,48 @@ def tiles(now: float | None = None) -> list[dict]:
|
|
|
338
570
|
for a in aliases}
|
|
339
571
|
limits = {a: t for a, t in limits.items() if t}
|
|
340
572
|
rows = [r for r in day_rows if r.get("alias") in mine]
|
|
573
|
+
# What the replies' headers said, kept a day past each reset (quota.py).
|
|
574
|
+
# A limit with nothing left said (Cohere's month) is a cap to count.
|
|
575
|
+
seen_recs = [r for a, by in (locked.read_json(quota.FILE).get(host) or {}).items()
|
|
576
|
+
if a in mine and isinstance(by, dict) for r in by.values()
|
|
577
|
+
if isinstance(r, dict) and r.get("limit") and r.get("window") != "retry-after"]
|
|
578
|
+
stated: dict[str, tuple] = {}
|
|
579
|
+
for r in seen_recs:
|
|
580
|
+
if r.get("remaining") is None and r["metric"] == "requests" and r.get("window") in ("day", "month"):
|
|
581
|
+
axis = "rpd" if r["window"] == "day" else "rpmo"
|
|
582
|
+
if r["limit"] > (stated.get(axis) or (0,))[0]:
|
|
583
|
+
stated[axis] = (r["limit"], "its replies", "The provider's replies state this limit but not"
|
|
584
|
+
" what is left.")
|
|
585
|
+
nv = (live.get("_nvidia") or {}) if "nvidia" in (host + " " + tag).lower() else {}
|
|
586
|
+
if nv.get("rpd"):
|
|
587
|
+
stated.setdefault("rpd", (nv["rpd"], "NVIDIA's site", "The figure build.nvidia.com shows every account"
|
|
588
|
+
f" (not this key's own), read {str(nv.get('at'))[:10]}."))
|
|
341
589
|
t = {"id": tag, "tag": tag, "host": host, "bench": len(aliases), "kind": "models", "groups": [],
|
|
342
|
-
"pool": [], "notes": [],
|
|
590
|
+
"pool": [], "notes": [], # a day or month cap is a meter (caps), a minute one is said
|
|
591
|
+
"conn_limit": _limit_text({k: v for k, v in (entered.get(host) or {}).items() if k in ("rpm", "tpm")}),
|
|
592
|
+
"caps": caps(host, entered, stated, day_rows, month_rows, now),
|
|
343
593
|
"models": _model_rows(aliases, rows, resting, limits),
|
|
344
|
-
"refresh":
|
|
594
|
+
"refresh": how_refreshed(tag), "plan": "", "name": ""}
|
|
345
595
|
got = live.get(tag) or {}
|
|
346
|
-
if tag.split("/")[0] == "cli-proxy-api":
|
|
347
|
-
t["refresh"] = "live"
|
|
596
|
+
if tag.split("/")[0] == "cli-proxy-api" or host == XKIRO_HOST:
|
|
348
597
|
if got.get("groups"):
|
|
349
598
|
t.update(kind="groups", groups=got["groups"], name=got.get("name", ""), plan=got.get("plan", ""))
|
|
350
599
|
elif tag == "openrouter":
|
|
351
|
-
t["refresh"] = "live" if _openrouter_endpoint() else "records"
|
|
352
600
|
t["kind"] = "pool"
|
|
353
601
|
t["pool"] = _openrouter_pool(aliases, utc_rows, regs, got, learned, now)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
602
|
+
elif host == REQUESTY_HOST and "free_used" in got and (_ts(got.get("at")) or 0) >= _utc_midnight(now):
|
|
603
|
+
# Requesty's count; the limit only as a 429 stated it (learned).
|
|
604
|
+
of = min([v for v in (((learned.get(a) or {}).get("limits") or {}).get("rpd", {}).get("value")
|
|
605
|
+
for a in aliases) if v] or [0]) or None
|
|
606
|
+
used = got["free_used"]
|
|
607
|
+
t["kind"] = "pool"
|
|
608
|
+
t["pool"] = [{"label": "free requests today · this key", "used": used, "of": of,
|
|
609
|
+
"left": max(0, round(100 * (of - used) / of)) if of else None, "reset_at": None}]
|
|
610
|
+
if t["kind"] == "models": # the headers' windows, as last seen
|
|
611
|
+
# A window past its reset has filled again: its limit is known, all
|
|
612
|
+
# of it left, the next reset not.
|
|
613
|
+
recs = [r if (_ts(r.get("reset_at")) or 0) > now else dict(r, remaining=r["limit"], reset_at=None)
|
|
614
|
+
for r in seen_recs if r.get("remaining") is not None]
|
|
357
615
|
by: dict[str, list] = {}
|
|
358
616
|
for r in recs:
|
|
359
617
|
by.setdefault(r["alias"], []).append(
|
|
@@ -366,26 +624,35 @@ def tiles(now: float | None = None) -> list[dict]:
|
|
|
366
624
|
seen = [_ts(r.get("observed_at")) or 0 for r in recs]
|
|
367
625
|
if seen:
|
|
368
626
|
got = {"at": _iso(max(seen)), "how": "seen"}
|
|
369
|
-
|
|
627
|
+
# Where the tile's figures come from, said on it: exact (an endpoint,
|
|
628
|
+
# the replies' headers) or a stated limit counted by the bench.
|
|
629
|
+
t["source"] = ((["its reply headers" if got.get("how") == "seen" else "its quota endpoint"] if t["groups"] else [])
|
|
630
|
+
+ ([("its usage endpoint" if host == REQUESTY_HOST
|
|
631
|
+
else "its stated daily limit, counted by the bench")] if t["pool"] else [])
|
|
632
|
+
+ [g["label"] for g in t["caps"]]) or ["nothing stated"]
|
|
370
633
|
last = max((r["t"] for r in rows if r.get("status") == "ok"), default=None)
|
|
371
634
|
t["fresh"] = ({"how": "live", "at": got["at"]} if got.get("at") and got.get("how") != "seen"
|
|
372
635
|
else {"how": "seen", "at": got["at"]} if got.get("at")
|
|
373
636
|
else {"how": "answered", "at": _iso(last)} if last else {"how": "none", "at": None})
|
|
374
637
|
t["live"] = sum(1 for a in aliases if a not in resting)
|
|
375
|
-
|
|
376
|
-
|
|
638
|
+
windows = [w for g in t["groups"] + t["caps"] for w in g["windows"]]
|
|
639
|
+
resets = [_ts(w.get("reset_at")) for w in windows] + [_ts(p.get("reset_at")) for p in t["pool"]]
|
|
377
640
|
resets = [r for r in resets if r and r > now]
|
|
378
641
|
t["next_reset"] = _iso(min(resets)) if resets else None
|
|
379
|
-
t["out"] = (t["live"] == 0 or any(w["left"] <= 0 for
|
|
642
|
+
t["out"] = (t["live"] == 0 or any(w["left"] <= 0 for w in windows)
|
|
380
643
|
or any(p.get("left") == 0 for p in t["pool"]))
|
|
381
|
-
t["notes"] = list(got.get("notes") or [])
|
|
644
|
+
t["notes"] = list(got.get("notes") or []) \
|
|
645
|
+
+ ([f"NVIDIA's site: {nv['rpm_text']}, varying by model and traffic."] if nv.get("rpm_text") else []) \
|
|
646
|
+
+ ([f"NVIDIA's limits file could not be read ({nv['broken']}); these are the figures read"
|
|
647
|
+
f" {str(nv.get('at'))[:10]}."] if nv.get("broken") else [])
|
|
382
648
|
out.append(t)
|
|
383
649
|
return ordered(out)
|
|
384
650
|
|
|
385
651
|
|
|
386
652
|
def _openrouter_pool(aliases, utc_rows, regs, got, learned, now) -> list[dict]:
|
|
387
653
|
"""A bar per OpenRouter key the bench uses: free-model requests since
|
|
388
|
-
00:00 UTC, the bench's own count, against
|
|
654
|
+
00:00 UTC, the bench's own count, against the day's limit OpenRouter
|
|
655
|
+
stated (a 429's), else none."""
|
|
389
656
|
import endpoints
|
|
390
657
|
import quota
|
|
391
658
|
seen_429 = locked.read_json(quota.FILE).get("openrouter") or {}
|
|
@@ -396,14 +663,13 @@ def _openrouter_pool(aliases, utc_rows, regs, got, learned, now) -> list[dict]:
|
|
|
396
663
|
for ep, eps in sorted(by_ep.items()):
|
|
397
664
|
used = sum(r.get("attempts") or 1 for r in utc_rows
|
|
398
665
|
if r.get("alias") in eps and str(r.get("served", "")).endswith(":free"))
|
|
399
|
-
#
|
|
400
|
-
# past reset)
|
|
666
|
+
# Only what OpenRouter stated (learned, else the last 429's, kept a day
|
|
667
|
+
# past reset): its docs' 50-or-1,000 by tier changes unseen, and the
|
|
668
|
+
# tier was seen wrong.
|
|
401
669
|
stated = [((learned.get(a) or {}).get("limits") or {}).get("rpd", {}).get("value") for a in eps]
|
|
402
670
|
stated += [r.get("limit") for a in eps for r in (seen_429.get(a) or {}).values()
|
|
403
671
|
if isinstance(r, dict) and r.get("window") in ("day", "unknown")]
|
|
404
672
|
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
673
|
via = "direct key" if not endpoints.hub(ep) else f"via {ep}"
|
|
408
674
|
bar = {"label": f"free requests today · {via}", "used": used, "of": limit,
|
|
409
675
|
"left": max(0, round(100 * (limit - used) / limit)) if limit else None,
|
|
@@ -447,6 +713,72 @@ def _selftest() -> None:
|
|
|
447
713
|
assert c == [{"label": "", "windows": [{"label": "month", "left": 0,
|
|
448
714
|
"reset_at": "2026-11-26T15:38:32+00:00"}]}], c
|
|
449
715
|
assert codex_groups({}) == [] and antigravity_groups({}) == []
|
|
716
|
+
now = 1793306000.0 # 2026-10-29 ~19:13 UTC
|
|
717
|
+
xk = xkiro_groups({"free_tokens": {"used_today": 125000, "limit_per_day": 500000, "remaining": 375000},
|
|
718
|
+
"windows": [{"kind": "short", "window_sec": 18000, "spent_usd": "1.500000",
|
|
719
|
+
"cap_usd": "6.000000", "remaining_usd": "4.500000", "resets_in_sec": 600}]}, now)
|
|
720
|
+
assert xk[0]["windows"][0] == {"label": "tokens · day", "left": 75, "remaining": 375000, "of": 500000,
|
|
721
|
+
"reset_at": "2026-10-30T00:00:00+00:00"}, xk
|
|
722
|
+
assert xk[1]["windows"][0]["label"] == "5 hours · spend" and xk[1]["windows"][0]["left"] == 75, xk
|
|
723
|
+
assert xkiro_groups({"free_tokens": {"limit_per_day": None}, "windows": []}, now) == []
|
|
724
|
+
rq = {"usage": {"2026-10-30": {"total_requests": 10, "grouped_data": [
|
|
725
|
+
{"group_by_values": {"model_used": "muse-glimmer-30b"}, "total_requests": 2},
|
|
726
|
+
{"group_by_values": {"model_used": "nemotron-3-nano-omni-30b-a3b-reasoning"}, "total_requests": 7},
|
|
727
|
+
{"group_by_values": {"model_used": "some-paid-model"}, "total_requests": 1}]}}}
|
|
728
|
+
ms = {"data": [{"id": "nvidia/muse-glimmer-30b", "input_price": 0, "output_price": 0},
|
|
729
|
+
{"id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", "input_price": 0, "output_price": 0},
|
|
730
|
+
{"id": "lab/some-paid-model", "input_price": 0.000001, "output_price": 0.000002}]}
|
|
731
|
+
assert requesty_free(rq, ms) == 9 and requesty_free({}, ms) == 0
|
|
732
|
+
# Caps: yours beat a stated one on the same axis; counted since midnight UTC or the month's start.
|
|
733
|
+
mid = _utc_midnight(now)
|
|
734
|
+
day = [{"connection": "router.requesty.ai", "t": mid + 60, "attempts": 2},
|
|
735
|
+
{"connection": "router.requesty.ai", "t": mid - 60, "attempts": 5}, # yesterday
|
|
736
|
+
{"connection": "api.cohere.ai", "t": mid + 60, "attempts": 1, "tokens_in": 10, "tokens_out": 5},
|
|
737
|
+
{"connection": "router.requesty.ai", "t": mid + 90, "attempts": 0}] # refused: not sent
|
|
738
|
+
said = (200, "its replies", "Stated.")
|
|
739
|
+
g = caps("router.requesty.ai", {}, {"rpd": said}, day, lambda: [], now)
|
|
740
|
+
assert len(g) == 1 and g[0]["windows"][0] == {"label": "requests · day", "left": 99, "remaining": 198,
|
|
741
|
+
"of": 200, "reset_at": _iso(mid + 86400)}, g
|
|
742
|
+
assert g[0]["label"] == "its replies · counted by the bench" and "not enforced" in g[0]["title"], g
|
|
743
|
+
g = caps("router.requesty.ai", {"router.requesty.ai": {"rpd": 10, "rpm": 5}}, {"rpd": said}, day, lambda: [], now)
|
|
744
|
+
assert g[0]["label"] == "your cap · counted by the bench" and g[0]["windows"] == [
|
|
745
|
+
{"label": "requests · day", "left": 80, "remaining": 8, "of": 10, "reset_at": _iso(mid + 86400)}], g
|
|
746
|
+
asked = []
|
|
747
|
+
g = caps("api.cohere.ai", {}, {"rpmo": (1000, "its replies", "")}, day, lambda: asked.append(1) or day, now)
|
|
748
|
+
assert asked and g[0]["windows"][0]["remaining"] == 999 and g[0]["windows"][0]["label"] == "requests · month", g
|
|
749
|
+
assert caps("nvidia-api", {}, {}, day, lambda: 1 / 0, now) == [] # no cap: no ledger read
|
|
750
|
+
# NVIDIA's file: read, a change alerts (to dismiss), a move or a new shape alerts and keeps the last read.
|
|
751
|
+
nv = {"rateLimits": {"requestsPerMinute": "Up to 40 rpm", "requestsPerDay": "10,000 requests per day"}}
|
|
752
|
+
assert nvidia_figures(nv) == {"rpd": 10000, "rpm_text": "Up to 40 rpm"}
|
|
753
|
+
import tempfile
|
|
754
|
+
import alerts as al
|
|
755
|
+
g, seen = globals(), []
|
|
756
|
+
saved = (g["LIVE"], g["_get_json"], g["_nvidia_on_bench"], al.raise_alert, al.clear)
|
|
757
|
+
try:
|
|
758
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
759
|
+
g["LIVE"], g["_nvidia_on_bench"] = pathlib.Path(tmp) / "live.json", lambda: True
|
|
760
|
+
al.raise_alert = lambda key, *a, **k: seen.append(("raise", key, k.get("dismiss")))
|
|
761
|
+
al.clear = lambda key: seen.append(("clear", key))
|
|
762
|
+
g["_get_json"] = lambda url, head: nv
|
|
763
|
+
assert watch_nvidia()["rpd"] == 10000 and not [s for s in seen if s[0] == "raise"], seen
|
|
764
|
+
nv = {"rateLimits": {"requestsPerMinute": "Up to 30 rpm", "requestsPerDay": "5,000 requests per day"}}
|
|
765
|
+
seen.clear()
|
|
766
|
+
assert watch_nvidia()["rpd"] == 5000 and ("raise", "limits:nvidia:changed", True) in seen, seen
|
|
767
|
+
|
|
768
|
+
def gone(url, head):
|
|
769
|
+
raise urllib.error.HTTPError(url, 404, "Not Found", {}, None)
|
|
770
|
+
g["_get_json"] = gone
|
|
771
|
+
seen.clear()
|
|
772
|
+
r = watch_nvidia()
|
|
773
|
+
assert r["rpd"] == 5000 and "404" in r["broken"] and ("raise", "limits:nvidia:unread", None) in seen, r
|
|
774
|
+
assert locked.read_json(g["LIVE"])["_nvidia"]["rpd"] == 5000 # the last good read stays
|
|
775
|
+
g["_get_json"] = lambda url, head: {"somethingElse": {}}
|
|
776
|
+
assert "rateLimits" in watch_nvidia()["broken"]
|
|
777
|
+
g["_get_json"] = lambda url, head: nv
|
|
778
|
+
seen.clear()
|
|
779
|
+
assert "broken" not in watch_nvidia() and ("clear", "limits:nvidia:unread") in seen, seen
|
|
780
|
+
finally:
|
|
781
|
+
g["LIVE"], g["_get_json"], g["_nvidia_on_bench"], al.raise_alert, al.clear = saved
|
|
450
782
|
assert _seconds_name(18000) == "5 hours" and _seconds_name(7200) == "2 hours"
|
|
451
783
|
assert _utc_midnight(1793306000) == dt.datetime(2026, 10, 29, tzinfo=UTC).timestamp()
|
|
452
784
|
assert _limit_text({"rpm": 20, "tpd": 200000}) == "20 requests a minute, 200,000 tokens a day"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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/auto_retire.py
CHANGED
|
@@ -19,6 +19,14 @@ THE RULES (decided 2026-10-15)
|
|
|
19
19
|
reason -- a rate limit that never lifts is a quota or a dead model.
|
|
20
20
|
`mistral-medium-3-5` answered 429 to 11 calls out of 11.
|
|
21
21
|
- NEVER on a rate limit or timeout alone inside those 7 days.
|
|
22
|
+
- OUT OF QUOTA IS NOT FAILING (2026-11-02): a call the bench held back because
|
|
23
|
+
a limit is spent until a stated reset, or an account-wide quota, says
|
|
24
|
+
nothing about the model. Its days pause the clock, and while it is the
|
|
25
|
+
latest word there is no countdown. `gpt-5.6-luna` sat on "retires in 3
|
|
26
|
+
days" while CLI Proxy API's codex sign-in was out until 26 November and the
|
|
27
|
+
bench had not sent it a call since 28 October. A refusal that names a short
|
|
28
|
+
wait and is refused again after it still counts: that is a model that keeps
|
|
29
|
+
failing, not one that is tapped out.
|
|
22
30
|
|
|
23
31
|
"No answer" means from ANY source: the perf probe, a useful offload, or a
|
|
24
32
|
suite output that was a real answer. A model that answers in the suite resets
|
|
@@ -68,6 +76,16 @@ def account_quota(detail: str) -> bool:
|
|
|
68
76
|
return any(marker in detail for marker in ACCOUNT_QUOTAS)
|
|
69
77
|
|
|
70
78
|
|
|
79
|
+
HELD = "not sent; paced by the bench" # metrics/pacing.py's refusal
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tapped_out(detail: str) -> bool:
|
|
83
|
+
"""Out of quota right now, so the model could not be tried: the bench held
|
|
84
|
+
the call back (a limit spent until its reset, the provider's or a cap you
|
|
85
|
+
set), or the account's shared quota is spent (account_quota)."""
|
|
86
|
+
return HELD in detail or account_quota(detail)
|
|
87
|
+
|
|
88
|
+
|
|
71
89
|
def plan(today: dt.date, models: dict[str, str], live: set[str],
|
|
72
90
|
first_missing: dict[str, str], rows: list[dict],
|
|
73
91
|
answers: dict[str, dt.date], restored: dict[str, dt.date],
|
|
@@ -87,15 +105,18 @@ def plan(today: dt.date, models: dict[str, str], live: set[str],
|
|
|
87
105
|
down: dict[str, bool] = {}
|
|
88
106
|
candidates: list[tuple[str, str, str]] = [] # (alias, bucket, why)
|
|
89
107
|
for alias, served in models.items():
|
|
90
|
-
mine =
|
|
108
|
+
mine = sorted((r for r in rows if r["alias"] == alias), key=lambda r: r["date"]) # [-1] is the latest word
|
|
91
109
|
last_ok = max([r["date"] for r in mine if r["answered"]]
|
|
92
110
|
+ ([answers[alias]] if alias in answers else []),
|
|
93
111
|
default=None)
|
|
94
112
|
floor = max(d for d in (last_ok, restored.get(alias), dt.date.min) if d)
|
|
95
113
|
# A refused key (`signin`) is the endpoint's, fixed by signing in again.
|
|
96
114
|
fails = sorted((r for r in mine if not r["answered"] and r["failure"] and r["failure"] != "signin"
|
|
97
|
-
and r["date"] > floor and not
|
|
115
|
+
and r["date"] > floor and not tapped_out(r["detail"])),
|
|
98
116
|
key=lambda r: r["date"])
|
|
117
|
+
quiet = {r["date"] for r in mine if not r["answered"] and r["date"] > floor
|
|
118
|
+
and tapped_out(r["detail"])}
|
|
119
|
+
resting = bool(mine) and not mine[-1]["answered"] and tapped_out(mine[-1]["detail"])
|
|
99
120
|
missing_since = None
|
|
100
121
|
if served not in live and served in first_missing:
|
|
101
122
|
missing_since = max(dt.date.fromisoformat(first_missing[served]),
|
|
@@ -107,8 +128,10 @@ def plan(today: dt.date, models: dict[str, str], live: set[str],
|
|
|
107
128
|
why = (f"{latest['failure']}: {latest['detail']}".rstrip(": ")
|
|
108
129
|
if latest else "")
|
|
109
130
|
other_days = {r["date"] for r in fails if r["failure"] == "other"}
|
|
110
|
-
span = (today - fails[0]["date"]).days if fails else 0
|
|
111
131
|
fail_days = {r["date"] for r in fails}
|
|
132
|
+
# Days it was out of quota, and nothing else, do not count: the clock pauses.
|
|
133
|
+
span = ((today - fails[0]["date"]).days
|
|
134
|
+
- len({q for q in quiet - fail_days if q > fails[0]["date"]})) if fails else 0
|
|
112
135
|
|
|
113
136
|
# A CONNECTION PREFIX ADDED IN OPEN WEBUI renames every model on it
|
|
114
137
|
# (`codestral-latest` -> `llm7.codestral-latest`), and the old names
|
|
@@ -146,7 +169,7 @@ def plan(today: dt.date, models: dict[str, str], live: set[str],
|
|
|
146
169
|
left.append((OTHER_DAYS - len(other_days), why))
|
|
147
170
|
if fails:
|
|
148
171
|
left.append((BACKSTOP_DAYS - span, why))
|
|
149
|
-
if left:
|
|
172
|
+
if left and not resting: # resting: the Quota page says when it is back
|
|
150
173
|
days, reason = min(left)
|
|
151
174
|
out["pending"].append((alias, f"{max(days, 1)}|{reason}"))
|
|
152
175
|
|
|
@@ -351,6 +374,23 @@ def _selfcheck() -> None:
|
|
|
351
374
|
detail="429 Rate limit exceeded: free-models-per-day") for n in (8, 5, 0)]
|
|
352
375
|
p = plan(d, {"quota": "v/quota"}, {"v/quota"}, {}, shared, {}, {}, {"v/quota": "v"})
|
|
353
376
|
assert not p["retire"] and not p["pending"], p
|
|
377
|
+
# held back by the bench for days (its codex sign-in spent until a reset): no countdown
|
|
378
|
+
held = [dict(row("luna", n, "capacity"), detail=f"rate limit ({HELD}): 0 left") for n in (9, 5, 0)]
|
|
379
|
+
p = plan(d, {"luna": "c/luna"}, {"c/luna"}, {}, held, {}, {}, {"c/luna": "c"})
|
|
380
|
+
assert not p["retire"] and not p["pending"], p
|
|
381
|
+
# real failures, then out of quota for a week: those days pause the clock
|
|
382
|
+
paused = ([row("p", n, "capacity") for n in (10, 9, 8, 0)]
|
|
383
|
+
+ [dict(row("p", n, "capacity"), detail=HELD) for n in range(1, 8)])
|
|
384
|
+
p = plan(d, {"p": "c/p"}, {"c/p"}, {}, paused, {}, {}, {"c/p": "c"})
|
|
385
|
+
assert not p["retire"] and dict(p["pending"])["p"].startswith("4|"), p
|
|
386
|
+
# ...and while it is still out of quota, no countdown is shown at all
|
|
387
|
+
p = plan(d, {"p": "c/p"}, {"c/p"}, {}, paused[:3] + paused[4:], {}, {}, {"c/p": "c"})
|
|
388
|
+
assert not p["retire"] and not p["pending"], p
|
|
389
|
+
# a short stated wait, refused again day after day, is a model that keeps failing
|
|
390
|
+
busy = [dict(row("u", n, "capacity"), detail="429 allows 1 request(s) every 1 min, retry in 60s")
|
|
391
|
+
for n in (8, 5, 0)]
|
|
392
|
+
p = plan(d, {"u": "v/u"}, {"v/u"}, {}, busy, {}, {}, {"v/u": "v"})
|
|
393
|
+
assert {a for a, _ in p["retire"]} == {"u"}, p
|
|
354
394
|
# A refused key is the endpoint's, never the model's (suite/run.py SIGNIN)
|
|
355
395
|
from run import failure_class
|
|
356
396
|
assert failure_class("Error: Error code: 401 - {'error': 'invalid key'}") == "signin"
|