@wendellresearch-ai/benchwarmers 0.4.0 → 0.5.1
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/GATEWAY.md +9 -5
- package/PRIVACY.md +16 -3
- package/README.md +20 -8
- package/USAGE.md +105 -14
- package/VERSION +1 -1
- package/cli.py +13 -4
- package/gateway.py +34 -2
- package/launcher.py +2 -1
- package/metrics/catalog.py +5 -0
- package/metrics/discover.py +68 -18
- package/metrics/learn.py +25 -1
- package/metrics/pacing.py +23 -3
- package/metrics/quota.py +41 -4
- package/metrics/quotaview.py +792 -0
- package/metrics/scheduler.py +2 -1
- package/metrics/sources.py +29 -6
- package/package.json +1 -1
- package/scripts/check.py +1 -0
- package/scripts/test_hubs.py +10 -5
- package/scripts/test_no_owui.py +9 -0
- package/scripts/test_quota.py +12 -2
- package/scripts/test_serve_edit.py +64 -2
- package/serve.py +311 -102
- package/ui/bench.css +92 -6
- package/ui/bench.js +544 -40
|
@@ -0,0 +1,792 @@
|
|
|
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: the provider's last day of calls, all together
|
|
16
|
+
|
|
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:
|
|
24
|
+
|
|
25
|
+
OpenRouter GET /key on a direct OpenRouter endpoint: free tier or
|
|
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)
|
|
31
|
+
CLI Proxy API its management route `requests/api-call`, which calls the
|
|
32
|
+
credential's own quota endpoint with the credential's
|
|
33
|
+
token, as its console does (Google's retrieveUserQuotaSummary,
|
|
34
|
+
ChatGPT's wham/usage). Needs CLI Proxy API on the
|
|
35
|
+
Endpoints page as a hub with its management key
|
|
36
|
+
(endpoints.admin_key, as metrics/cliproxy.py); the proxy is
|
|
37
|
+
on the LAN and calls only providers the credential uses.
|
|
38
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
Live reads are kept in runs/quota-live.json, newest per tag.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import datetime as dt
|
|
50
|
+
import json
|
|
51
|
+
import pathlib
|
|
52
|
+
import sys
|
|
53
|
+
import time
|
|
54
|
+
import urllib.error
|
|
55
|
+
import urllib.parse
|
|
56
|
+
import urllib.request
|
|
57
|
+
|
|
58
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
59
|
+
sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
|
|
60
|
+
import locked # noqa: E402
|
|
61
|
+
from paths import DATA # noqa: E402
|
|
62
|
+
|
|
63
|
+
LIVE = DATA / "runs" / "quota-live.json"
|
|
64
|
+
UTC = dt.timezone.utc
|
|
65
|
+
CPA_PREFIXES = ("/v8/management", "/v0/management") # the console's, then the older one
|
|
66
|
+
# The headers CLI Proxy API's own console sends to each quota endpoint
|
|
67
|
+
# ($TOKEN$ is replaced by the proxy with the credential's token).
|
|
68
|
+
AG_URLS = ("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
|
|
69
|
+
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary")
|
|
70
|
+
# Google answers 403 without the Antigravity client's User-Agent.
|
|
71
|
+
AG_HEAD = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json",
|
|
72
|
+
"User-Agent": "antigravity/hub/2.23.0 darwin/arm64"}
|
|
73
|
+
CODEX_URL = "https://chatgpt.com/backend-api/wham/usage"
|
|
74
|
+
CODEX_HEAD = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json",
|
|
75
|
+
"User-Agent": "codex-tui/0.149.1"}
|
|
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")}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _iso(t: float | None) -> str | None:
|
|
97
|
+
return None if t is None else dt.datetime.fromtimestamp(t, UTC).isoformat(timespec="seconds")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ts(iso) -> float | None:
|
|
101
|
+
try:
|
|
102
|
+
return dt.datetime.fromisoformat(str(iso).replace("Z", "+00:00")).timestamp() if iso else None
|
|
103
|
+
except ValueError:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _seconds_name(s) -> str:
|
|
108
|
+
"""A window's length as a word: 18000 -> "5 hours", 604800 -> "week"."""
|
|
109
|
+
s = int(s or 0)
|
|
110
|
+
return {3600: "hour", 18000: "5 hours", 86400: "day", 604800: "week", 2592000: "month"}.get(
|
|
111
|
+
s, f"{round(s / 3600)} hours" if s else "window")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _utc_midnight(now: float) -> float:
|
|
115
|
+
d = dt.datetime.fromtimestamp(now, UTC)
|
|
116
|
+
return dt.datetime(d.year, d.month, d.day, tzinfo=UTC).timestamp()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---- parsers: a provider's reply -> groups of windows -----------------------
|
|
120
|
+
|
|
121
|
+
def antigravity_groups(body: dict) -> list[dict]:
|
|
122
|
+
"""retrieveUserQuotaSummary -> [{label, windows: [{label, left, reset_at}]}],
|
|
123
|
+
the shorter window first."""
|
|
124
|
+
out = []
|
|
125
|
+
for g in body.get("groups") or []:
|
|
126
|
+
wins = [{"label": WINDOW_NAMES.get(b.get("window"), str(b.get("window") or "window")),
|
|
127
|
+
"left": round(float(b.get("remainingFraction", 0)) * 100),
|
|
128
|
+
"reset_at": b.get("resetTime")}
|
|
129
|
+
for b in g.get("buckets") or [] if isinstance(b, dict)]
|
|
130
|
+
wins.sort(key=lambda w: ["5 hours", "day", "week", "month"].index(w["label"])
|
|
131
|
+
if w["label"] in ("5 hours", "day", "week", "month") else 9)
|
|
132
|
+
if wins:
|
|
133
|
+
out.append({"label": str(g.get("displayName") or ""), "windows": wins})
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def codex_groups(body: dict) -> list[dict]:
|
|
138
|
+
"""wham/usage -> one group with its primary and secondary windows."""
|
|
139
|
+
rl = body.get("rate_limit") or {}
|
|
140
|
+
wins = [{"label": _seconds_name(w.get("limit_window_seconds")),
|
|
141
|
+
"left": max(0, 100 - round(float(w.get("used_percent") or 0))),
|
|
142
|
+
"reset_at": _iso(w["reset_at"]) if w.get("reset_at") else None}
|
|
143
|
+
for w in (rl.get("primary_window"), rl.get("secondary_window")) if isinstance(w, dict)]
|
|
144
|
+
return [{"label": "", "windows": wins}] if wins else []
|
|
145
|
+
|
|
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
|
+
|
|
165
|
+
# ---- live reads --------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def _get_json(url: str, headers: dict, data: bytes | None = None, timeout: int = 20) -> dict:
|
|
168
|
+
req = urllib.request.Request(url, data=data, headers={"Accept": "application/json", **headers},
|
|
169
|
+
method="POST" if data is not None else "GET")
|
|
170
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
171
|
+
return json.loads(r.read() or b"{}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _openrouter_endpoint() -> str | None:
|
|
175
|
+
"""A direct OpenRouter endpoint with a key: the one /key can be asked on."""
|
|
176
|
+
import endpoints
|
|
177
|
+
for name, entry in endpoints.load().items():
|
|
178
|
+
host = urllib.parse.urlsplit(entry.get("base", "")).hostname or ""
|
|
179
|
+
if host.endswith("openrouter.ai") and not endpoints.hub(name) and endpoints.key(name):
|
|
180
|
+
return name
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def read_openrouter() -> dict:
|
|
185
|
+
import endpoints
|
|
186
|
+
name = _openrouter_endpoint()
|
|
187
|
+
if not name:
|
|
188
|
+
raise ValueError("no direct OpenRouter endpoint with a key to ask")
|
|
189
|
+
data = _get_json(endpoints.base(name) + "/key", {"Authorization": f"Bearer {endpoints.key(name)}"}).get("data") or {}
|
|
190
|
+
return {"endpoint": name, "free_tier": bool(data.get("is_free_tier")),
|
|
191
|
+
"limit": data.get("limit"), "remaining": data.get("limit_remaining"), "usage": data.get("usage")}
|
|
192
|
+
|
|
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
|
+
|
|
294
|
+
def _cpa_hub() -> tuple[str, str]:
|
|
295
|
+
"""(management base, key) of the CLI Proxy API hub; ValueError when there is none."""
|
|
296
|
+
import endpoints
|
|
297
|
+
name = endpoints.find_hub("cliproxy")
|
|
298
|
+
if not name:
|
|
299
|
+
raise ValueError("add CLI Proxy API on the Endpoints page (kind hub, CLI Proxy API) with its"
|
|
300
|
+
" management key to read these quotas live")
|
|
301
|
+
secret = endpoints.admin_key(name, fallback=False)
|
|
302
|
+
if not secret:
|
|
303
|
+
raise ValueError(f"CLI Proxy API's management key is not stored: Change key on {name}")
|
|
304
|
+
return endpoints.base(name).removesuffix("/v1"), secret
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _cpa(base: str, secret: str, path: str, body: dict | None = None) -> dict:
|
|
308
|
+
"""One management call; the console's prefix first, the older on a 404.
|
|
309
|
+
A 401 is never retried: the proxy bans an address after 5 refusals."""
|
|
310
|
+
head = {"Authorization": f"Bearer {secret}", "Content-Type": "application/json"}
|
|
311
|
+
for prefix in CPA_PREFIXES:
|
|
312
|
+
try:
|
|
313
|
+
return _get_json(base + prefix + path, head, None if body is None else json.dumps(body).encode())
|
|
314
|
+
except urllib.error.HTTPError as exc:
|
|
315
|
+
if exc.code == 404 and prefix != CPA_PREFIXES[-1]:
|
|
316
|
+
continue
|
|
317
|
+
if exc.code == 401:
|
|
318
|
+
raise ValueError("CLI Proxy API refused the management key; check it before trying again"
|
|
319
|
+
" (5 refusals ban this address for 30 minutes)") from None
|
|
320
|
+
raise ValueError(f"CLI Proxy API answered {exc.code} on {path}") from None
|
|
321
|
+
raise ValueError("CLI Proxy API's management API is off (every route 404s without a secret key)")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _api_call(base, secret, auth_index, method, url, header, data=None) -> dict:
|
|
325
|
+
"""The credential's own request, made by the proxy; the reply's JSON body."""
|
|
326
|
+
reply = _cpa(base, secret, "/requests/api-call",
|
|
327
|
+
{"auth_index": auth_index, "method": method, "url": url, "header": header,
|
|
328
|
+
**({"data": data} if data is not None else {})})
|
|
329
|
+
if not 200 <= int(reply.get("status_code") or 0) < 300:
|
|
330
|
+
raise ValueError(f"{urllib.parse.urlsplit(url).hostname} answered {reply.get('status_code')}")
|
|
331
|
+
body = reply.get("body")
|
|
332
|
+
return json.loads(body) if isinstance(body, str) and body.strip() else (body or {})
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def read_cliproxy(tag: str, sibling_vendors: set[str]) -> dict:
|
|
336
|
+
"""Every enabled credential behind `tag`: `cli-proxy-api/codex` is
|
|
337
|
+
the codex ones, bare `cli-proxy-api` those no sibling tag names."""
|
|
338
|
+
base, secret = _cpa_hub()
|
|
339
|
+
vendor = tag.split("/", 1)[1] if "/" in tag else ""
|
|
340
|
+
creds = [c for c in _cpa(base, secret, "/credentials").get("files") or []
|
|
341
|
+
if not c.get("disabled") and (c.get("provider") == vendor if vendor
|
|
342
|
+
else c.get("provider") not in sibling_vendors)]
|
|
343
|
+
groups, names, plan, notes = [], [], "", []
|
|
344
|
+
for c in creds:
|
|
345
|
+
prov, idx = c.get("provider", ""), c.get("auth_index")
|
|
346
|
+
try:
|
|
347
|
+
if prov == "antigravity":
|
|
348
|
+
body = None
|
|
349
|
+
for url in AG_URLS:
|
|
350
|
+
try:
|
|
351
|
+
body = _api_call(base, secret, idx, "POST", url, AG_HEAD,
|
|
352
|
+
json.dumps({"project": c.get("project_id") or ""}))
|
|
353
|
+
break
|
|
354
|
+
except ValueError:
|
|
355
|
+
continue
|
|
356
|
+
got = antigravity_groups(body or {})
|
|
357
|
+
elif prov == "codex":
|
|
358
|
+
head = dict(CODEX_HEAD)
|
|
359
|
+
acct = (c.get("id_token") or {}).get("chatgpt_account_id")
|
|
360
|
+
if acct:
|
|
361
|
+
head["Chatgpt-Account-Id"] = acct
|
|
362
|
+
body = _api_call(base, secret, idx, "GET", CODEX_URL, head)
|
|
363
|
+
got = codex_groups(body)
|
|
364
|
+
plan = str(body.get("plan_type") or "")
|
|
365
|
+
else:
|
|
366
|
+
notes.append(f"no quota reader for {prov}")
|
|
367
|
+
continue
|
|
368
|
+
except ValueError as exc:
|
|
369
|
+
notes.append(f"{prov}: {exc}")
|
|
370
|
+
continue
|
|
371
|
+
if len(creds) > 1:
|
|
372
|
+
for g in got:
|
|
373
|
+
g["label"] = f"{prov} · {g['label']}".rstrip(" ·")
|
|
374
|
+
groups += got
|
|
375
|
+
names.append(prov)
|
|
376
|
+
if not groups and notes:
|
|
377
|
+
raise ValueError("; ".join(notes))
|
|
378
|
+
return {"groups": groups, "name": ", ".join(dict.fromkeys(names)), "plan": plan, "notes": notes}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def refresh(tag: str) -> dict:
|
|
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":
|
|
400
|
+
got = read_openrouter()
|
|
401
|
+
elif _host(tag) == XKIRO_HOST:
|
|
402
|
+
got = read_xkiro()
|
|
403
|
+
elif _host(tag) == REQUESTY_HOST:
|
|
404
|
+
got = read_requesty()
|
|
405
|
+
else:
|
|
406
|
+
vendors = {t.split("/", 1)[1] for t in _tags() if t.startswith("cli-proxy-api/")}
|
|
407
|
+
got = read_cliproxy(tag, vendors)
|
|
408
|
+
got["at"] = _iso(time.time())
|
|
409
|
+
locked.update(LIVE, lambda d: d.update({tag: got}), ensure_ascii=False)
|
|
410
|
+
return {"ok": True, "how": "live", "note": "; ".join(got.get("notes") or [])}
|
|
411
|
+
|
|
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
|
+
|
|
448
|
+
def check(tag: str) -> dict:
|
|
449
|
+
"""One 16-token call to a model on `tag` that is free to call; the
|
|
450
|
+
built-in client records what its reply headers say (quota.observe)."""
|
|
451
|
+
import client
|
|
452
|
+
import learn
|
|
453
|
+
resting = learn.cooldowns()
|
|
454
|
+
aliases = [a for a in _tags().get(tag, []) if a not in resting]
|
|
455
|
+
if not aliases:
|
|
456
|
+
return {"ok": False, "errors": ["every model on it is resting; nothing was sent"]}
|
|
457
|
+
if not _headers_seen(tag):
|
|
458
|
+
return {"ok": False, "errors": ["this provider has never sent limit headers, so a call would"
|
|
459
|
+
" tell nothing; nothing was sent"]}
|
|
460
|
+
res = client.chat(aliases[0], "Hi", timeout=60, body={"max_tokens": 16}, purpose="quota check", retries=0)
|
|
461
|
+
if res.get("error"):
|
|
462
|
+
return {"ok": False, "errors": [f"{aliases[0]}: {res['error'][:200]}"]}
|
|
463
|
+
return {"ok": True, "note": f"asked {aliases[0]}: one request"}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ---- tiles -------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
def _bench() -> list[str]:
|
|
469
|
+
from run import _registered_names
|
|
470
|
+
gone = locked.read_json(DATA / "config" / "retired.json")
|
|
471
|
+
return [a for a in _registered_names() if a not in gone]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _tags() -> dict[str, list[str]]:
|
|
475
|
+
import route
|
|
476
|
+
out: dict[str, list[str]] = {}
|
|
477
|
+
for a in _bench():
|
|
478
|
+
tag = route.full_tag_of(a)
|
|
479
|
+
if tag != "?":
|
|
480
|
+
out.setdefault(tag, []).append(a)
|
|
481
|
+
return out
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _headers_seen(tag: str) -> bool:
|
|
485
|
+
"""Has this tag's provider ever stated a limit in its headers (kept a day past reset)?"""
|
|
486
|
+
import quota
|
|
487
|
+
import route
|
|
488
|
+
aliases = set(_tags().get(tag, []))
|
|
489
|
+
host = route.connection_of(next(iter(aliases), "")) if aliases else tag
|
|
490
|
+
recs = locked.read_json(quota.FILE).get(host) or {}
|
|
491
|
+
return any(str(r.get("source", "")).startswith("headers") and r.get("limit")
|
|
492
|
+
for a, by in recs.items() if a in aliases and isinstance(by, dict)
|
|
493
|
+
for r in by.values() if isinstance(r, dict))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _model_rows(aliases, rows, resting, limits) -> list[dict]:
|
|
497
|
+
out = []
|
|
498
|
+
for a in sorted(aliases):
|
|
499
|
+
mine = [r for r in rows if r.get("alias") == a]
|
|
500
|
+
ok = [r for r in mine if r.get("status") == "ok"]
|
|
501
|
+
out.append({"alias": a, "ok": len(ok), "failed": len(mine) - len(ok),
|
|
502
|
+
"last_ok": _iso(max(r["t"] for r in ok)) if ok else None,
|
|
503
|
+
"last": _iso(max(r["t"] for r in mine)) if mine else None,
|
|
504
|
+
"until": (resting.get(a) or {}).get("until"),
|
|
505
|
+
"limit": limits.get(a, "")})
|
|
506
|
+
return out
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _limit_text(lim: dict) -> str:
|
|
510
|
+
words = {"rpm": "requests a minute", "rpd": "requests a day", "tpm": "tokens a minute", "tpd": "tokens a day",
|
|
511
|
+
"rpmo": "requests a calendar month"}
|
|
512
|
+
return ", ".join(f"{v:,} {words[k]}" for k, v in lim.items() if k in words and v)
|
|
513
|
+
|
|
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
|
+
|
|
545
|
+
def tiles(now: float | None = None) -> list[dict]:
|
|
546
|
+
"""Every tile, in the saved order (out first, then soonest reset, for new ones)."""
|
|
547
|
+
import catalog
|
|
548
|
+
import learn
|
|
549
|
+
import ledger
|
|
550
|
+
import quota
|
|
551
|
+
import route
|
|
552
|
+
now = time.time() if now is None else now
|
|
553
|
+
tags, live = _tags(), locked.read_json(LIVE)
|
|
554
|
+
resting, learned = learn.cooldowns(), learn.learned()
|
|
555
|
+
day_rows = list(ledger.rows(now - 86400))
|
|
556
|
+
utc_rows = [r for r in day_rows if r["t"] >= _utc_midnight(now)]
|
|
557
|
+
entered = catalog.connection_limits()
|
|
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]
|
|
565
|
+
out = []
|
|
566
|
+
for tag, aliases in tags.items():
|
|
567
|
+
host = route.connection_of(aliases[0])
|
|
568
|
+
mine = set(aliases)
|
|
569
|
+
limits = {a: _limit_text({k: ev.get("value") for k, ev in ((learned.get(a) or {}).get("limits") or {}).items()})
|
|
570
|
+
for a in aliases}
|
|
571
|
+
limits = {a: t for a, t in limits.items() if t}
|
|
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]}."))
|
|
589
|
+
t = {"id": tag, "tag": tag, "host": host, "bench": len(aliases), "kind": "models", "groups": [],
|
|
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),
|
|
593
|
+
"models": _model_rows(aliases, rows, resting, limits),
|
|
594
|
+
"refresh": how_refreshed(tag), "plan": "", "name": ""}
|
|
595
|
+
got = live.get(tag) or {}
|
|
596
|
+
if tag.split("/")[0] == "cli-proxy-api" or host == XKIRO_HOST:
|
|
597
|
+
if got.get("groups"):
|
|
598
|
+
t.update(kind="groups", groups=got["groups"], name=got.get("name", ""), plan=got.get("plan", ""))
|
|
599
|
+
elif tag == "openrouter":
|
|
600
|
+
t["kind"] = "pool"
|
|
601
|
+
t["pool"] = _openrouter_pool(aliases, utc_rows, regs, got, learned, now)
|
|
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]
|
|
615
|
+
by: dict[str, list] = {}
|
|
616
|
+
for r in recs:
|
|
617
|
+
by.setdefault(r["alias"], []).append(
|
|
618
|
+
{"label": r["metric"] + ("" if r["window"] == "unknown" else " · " + r["window"]),
|
|
619
|
+
"left": round(100 * (r.get("remaining") or 0) / r["limit"]),
|
|
620
|
+
"remaining": r.get("remaining"), "of": r["limit"], "reset_at": r.get("reset_at")})
|
|
621
|
+
if by:
|
|
622
|
+
t["kind"] = "groups"
|
|
623
|
+
t["groups"] = [{"label": a if len(by) > 1 else "", "windows": w} for a, w in sorted(by.items())]
|
|
624
|
+
seen = [_ts(r.get("observed_at")) or 0 for r in recs]
|
|
625
|
+
if seen:
|
|
626
|
+
got = {"at": _iso(max(seen)), "how": "seen"}
|
|
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"]
|
|
633
|
+
last = max((r["t"] for r in rows if r.get("status") == "ok"), default=None)
|
|
634
|
+
t["fresh"] = ({"how": "live", "at": got["at"]} if got.get("at") and got.get("how") != "seen"
|
|
635
|
+
else {"how": "seen", "at": got["at"]} if got.get("at")
|
|
636
|
+
else {"how": "answered", "at": _iso(last)} if last else {"how": "none", "at": None})
|
|
637
|
+
t["live"] = sum(1 for a in aliases if a not in resting)
|
|
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"]]
|
|
640
|
+
resets = [r for r in resets if r and r > now]
|
|
641
|
+
t["next_reset"] = _iso(min(resets)) if resets else None
|
|
642
|
+
t["out"] = (t["live"] == 0 or any(w["left"] <= 0 for w in windows)
|
|
643
|
+
or any(p.get("left") == 0 for p in t["pool"]))
|
|
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 [])
|
|
648
|
+
out.append(t)
|
|
649
|
+
return ordered(out)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _openrouter_pool(aliases, utc_rows, regs, got, learned, now) -> list[dict]:
|
|
653
|
+
"""A bar per OpenRouter key the bench uses: free-model requests since
|
|
654
|
+
00:00 UTC, the bench's own count, against the day's limit OpenRouter
|
|
655
|
+
stated (a 429's), else none."""
|
|
656
|
+
import endpoints
|
|
657
|
+
import quota
|
|
658
|
+
seen_429 = locked.read_json(quota.FILE).get("openrouter") or {}
|
|
659
|
+
by_ep: dict[str, list[str]] = {}
|
|
660
|
+
for a in aliases:
|
|
661
|
+
by_ep.setdefault(endpoints.endpoint_name(regs.get(a) or {}) or "?", []).append(a)
|
|
662
|
+
bars = []
|
|
663
|
+
for ep, eps in sorted(by_ep.items()):
|
|
664
|
+
used = sum(r.get("attempts") or 1 for r in utc_rows
|
|
665
|
+
if r.get("alias") in eps and str(r.get("served", "")).endswith(":free"))
|
|
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.
|
|
669
|
+
stated = [((learned.get(a) or {}).get("limits") or {}).get("rpd", {}).get("value") for a in eps]
|
|
670
|
+
stated += [r.get("limit") for a in eps for r in (seen_429.get(a) or {}).values()
|
|
671
|
+
if isinstance(r, dict) and r.get("window") in ("day", "unknown")]
|
|
672
|
+
limit = min([s for s in stated if s] or [0]) or None
|
|
673
|
+
via = "direct key" if not endpoints.hub(ep) else f"via {ep}"
|
|
674
|
+
bar = {"label": f"free requests today · {via}", "used": used, "of": limit,
|
|
675
|
+
"left": max(0, round(100 * (limit - used) / limit)) if limit else None,
|
|
676
|
+
"reset_at": _iso(_utc_midnight(now) + 86400), "models": len(eps)}
|
|
677
|
+
bars.append(bar)
|
|
678
|
+
if got.get("limit"):
|
|
679
|
+
lim, rem = float(got["limit"]), float(got.get("remaining") or 0)
|
|
680
|
+
bars.append({"label": "credits", "used": round(lim - rem, 2), "of": lim,
|
|
681
|
+
"left": round(100 * rem / lim) if lim else 0, "reset_at": None, "models": 0})
|
|
682
|
+
return bars
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def ordered(tiles_: list[dict]) -> list[dict]:
|
|
686
|
+
"""The saved order first (config/ui.json quota_order), then the rest: out
|
|
687
|
+
first, then the soonest reset, then by tag."""
|
|
688
|
+
saved = locked.read_json(DATA / "config" / "ui.json").get("quota_order") or []
|
|
689
|
+
pos = {t: i for i, t in enumerate(saved)}
|
|
690
|
+
return sorted(tiles_, key=lambda t: (pos.get(t["id"], len(pos)), not t["out"],
|
|
691
|
+
t["next_reset"] or "9999", t["id"]))
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def save_order(order) -> dict:
|
|
695
|
+
if order is not None and not (isinstance(order, list) and all(isinstance(t, str) for t in order)):
|
|
696
|
+
return {"ok": False, "errors": ["order must be a list of tags"]}
|
|
697
|
+
locked.update(DATA / "config" / "ui.json", lambda d: d.update(quota_order=order or []))
|
|
698
|
+
return {"ok": True, "errors": []}
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _selftest() -> None:
|
|
702
|
+
ag = {"groups": [{"displayName": "Gemini Models", "buckets": [
|
|
703
|
+
{"window": "weekly", "resetTime": "2026-10-30T23:01:51Z", "remainingFraction": 0.620195},
|
|
704
|
+
{"window": "5h", "resetTime": "2026-10-29T23:11:05Z", "remainingFraction": 0.9362293}]},
|
|
705
|
+
{"displayName": "Gemini 4 Argon", "buckets": [
|
|
706
|
+
{"window": "weekly", "resetTime": "2026-10-31T01:20:30Z", "remainingFraction": 0.40175587}]}]}
|
|
707
|
+
g = antigravity_groups(ag)
|
|
708
|
+
assert [w["label"] for w in g[0]["windows"]] == ["5 hours", "week"], g
|
|
709
|
+
assert [w["left"] for w in g[0]["windows"]] == [94, 62] and g[1]["windows"][0]["left"] == 40, g
|
|
710
|
+
cx = {"plan_type": "free", "rate_limit": {"primary_window": {
|
|
711
|
+
"used_percent": 100, "limit_window_seconds": 2592000, "reset_at": 1795707512}, "secondary_window": None}}
|
|
712
|
+
c = codex_groups(cx)
|
|
713
|
+
assert c == [{"label": "", "windows": [{"label": "month", "left": 0,
|
|
714
|
+
"reset_at": "2026-11-26T15:38:32+00:00"}]}], c
|
|
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
|
|
782
|
+
assert _seconds_name(18000) == "5 hours" and _seconds_name(7200) == "2 hours"
|
|
783
|
+
assert _utc_midnight(1793306000) == dt.datetime(2026, 10, 29, tzinfo=UTC).timestamp()
|
|
784
|
+
assert _limit_text({"rpm": 20, "tpd": 200000}) == "20 requests a minute, 200,000 tokens a day"
|
|
785
|
+
print("quotaview selftest: ok")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
if __name__ == "__main__":
|
|
789
|
+
if "--selftest" in sys.argv:
|
|
790
|
+
_selftest()
|
|
791
|
+
else:
|
|
792
|
+
print(json.dumps(tiles(), indent=1))
|