@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/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Privacy policy
|
|
2
2
|
|
|
3
|
-
*Effective
|
|
3
|
+
*Effective 2 November 2026.*
|
|
4
4
|
|
|
5
5
|
Benchwarmers runs on your own machine or server. It has no account, no
|
|
6
6
|
telemetry and no server of its own, and nothing you do with it reaches
|
|
@@ -15,12 +15,19 @@ Wendell Research.
|
|
|
15
15
|
applies to what it receives; some free tiers keep prompts or train on them.
|
|
16
16
|
- **Catalog reads**, carrying nothing of yours: FreeLLMAPI (its feed and its
|
|
17
17
|
site's sitemap) and models.dev (at most once a day) and OpenRouter's public
|
|
18
|
-
model list.
|
|
18
|
+
model list. With an NVIDIA connection on the bench, the public file of rate
|
|
19
|
+
limits build.nvidia.com states, once a day and when you press Refresh on its
|
|
20
|
+
quota tile.
|
|
19
21
|
- **Sign-ins**, only when you choose one: the provider's own sign-in page, in
|
|
20
22
|
your browser, and the provider's token endpoint.
|
|
21
23
|
- **Quota reads**, only when you press Refresh on Quota management, carrying
|
|
22
24
|
no prompt:
|
|
23
25
|
- OpenRouter's key status, asked with your key.
|
|
26
|
+
- xKiro's usage, asked with your key for it (from its Open WebUI connection
|
|
27
|
+
when no endpoint has one). Its reply names the account's owner; only the
|
|
28
|
+
quota figures are kept.
|
|
29
|
+
- Requesty's usage for the key and its public model list, asked the same
|
|
30
|
+
way; only the count of free-model requests is kept.
|
|
24
31
|
- For CLI Proxy API's sign-ins, through the proxy, the quota endpoint of the
|
|
25
32
|
provider each sign-in belongs to (Google's, ChatGPT's).
|
|
26
33
|
- **Charts**: the dashboard's page loads Observable Plot from jsDelivr, at a
|
package/README.md
CHANGED
|
@@ -187,7 +187,8 @@ dependencies either way: the app is the Python standard library.
|
|
|
187
187
|
- **Health.** Every morning each model gets a one-word prompt; once a week, a
|
|
188
188
|
short and a long one for speed. A model that stops answering, or that its
|
|
189
189
|
endpoint no longer serves, is retired automatically after one confirming
|
|
190
|
-
call, with the evidence kept, and can be restored in one click.
|
|
190
|
+
call, with the evidence kept, and can be restored in one click. A model
|
|
191
|
+
that is only out of quota until a stated reset is not counted against. The
|
|
191
192
|
built-in scheduler runs all of this once Settings switches it on; a fresh
|
|
192
193
|
install spends no calls until then.
|
|
193
194
|
- **Rate limits and usage.** Limits come from providers' rate-limit headers
|
|
@@ -196,8 +197,10 @@ dependencies either way: the app is the Python standard library.
|
|
|
196
197
|
connection, since models behind one key share them, and enforced: a call
|
|
197
198
|
waits when the wait fits, or is refused before it goes out. Every call lands
|
|
198
199
|
in a monthly usage ledger with its tokens. Quota management has a tile per
|
|
199
|
-
provider showing what is left, when it resets and who is resting. OpenRouter
|
|
200
|
-
and CLI Proxy API's sign-ins are read live, at no cost to
|
|
200
|
+
provider showing what is left, when it resets and who is resting. OpenRouter,
|
|
201
|
+
xKiro, Requesty and CLI Proxy API's sign-ins are read live, at no cost to
|
|
202
|
+
quota. Nothing is taken from a provider's docs; NVIDIA's own stated figures
|
|
203
|
+
are read from its site and watched for changes.
|
|
201
204
|
- **Discover.** FreeLLMAPI's free catalog, with models.dev for the details
|
|
202
205
|
and paid models it lacks, merged into one list of models worth adding,
|
|
203
206
|
smartest first, each with the routes this install has to it.
|
|
@@ -438,11 +441,13 @@ reaches Wendell Research. What it sends, and where:
|
|
|
438
441
|
are content to send that work to.
|
|
439
442
|
- **Catalog reads**, no key and nothing of yours: FreeLLMAPI (its feed and
|
|
440
443
|
its site's sitemap) and models.dev once a day, and OpenRouter's public
|
|
441
|
-
model list.
|
|
444
|
+
model list. With an NVIDIA connection, the public file of rate limits
|
|
445
|
+
build.nvidia.com states, once a day and on Refresh.
|
|
442
446
|
- **Sign-ins**, only when you choose one: the provider's own sign-in page.
|
|
443
447
|
- **Quota reads**, only when you press Refresh on Quota management, with no
|
|
444
|
-
prompt: OpenRouter's key status,
|
|
445
|
-
|
|
448
|
+
prompt: OpenRouter's key status, and xKiro's and Requesty's usage, each with
|
|
449
|
+
your key for it (from its Open WebUI connection when no endpoint has one). CLI Proxy
|
|
450
|
+
API asks each sign-in's own provider for its quota.
|
|
446
451
|
- **Charts**: the dashboard's browser page loads Observable Plot from jsDelivr
|
|
447
452
|
at a pinned version.
|
|
448
453
|
- **Seeding real-bug tasks**, only when you run `suite/rebench.py`: the
|
package/USAGE.md
CHANGED
|
@@ -37,8 +37,13 @@ coding agent doing it with you); the gateway's whole contract is in
|
|
|
37
37
|
|
|
38
38
|
"Action needed", above the pages, lists what will not fix itself, such as a
|
|
39
39
|
model about to be retired, naming each model and where it comes from, with
|
|
40
|
-
Retry, Open and Retire now beside it.
|
|
41
|
-
|
|
40
|
+
Retry, Open and Retire now beside it. It starts folded to its heading and
|
|
41
|
+
count; show opens it in that browser for 12 hours. An alert whose models have
|
|
42
|
+
all been retired goes at once. A model that is out of quota is not counting
|
|
43
|
+
down: days the bench held its calls back until a stated reset, or an account's
|
|
44
|
+
shared free quota was spent, pause its clock, and while it is still out of
|
|
45
|
+
quota no countdown shows. One refused again after the short wait it named is
|
|
46
|
+
failing, and keeps counting.
|
|
42
47
|
|
|
43
48
|
**Restart** (Settings, Server and access), or `bench restart`, starts the
|
|
44
49
|
dashboard again on the code now on disk, after an update. It waits for
|
|
@@ -109,8 +114,11 @@ seats dashed, and the merger after an arrow.
|
|
|
109
114
|
In the popup, the panel's seats are on top, and the member list has one row
|
|
110
115
|
per model with its context, vision and tools, and price. A model served by
|
|
111
116
|
several providers is still one row, with a chip per provider: tick the chip
|
|
112
|
-
of the route you want. Ticking two is allowed, and the count says so. The
|
|
113
|
-
|
|
117
|
+
of the route you want. Ticking two is allowed, and the count says so. The
|
|
118
|
+
search matches a model, provider or served name, and the select beside it
|
|
119
|
+
shows one provider's models; ticked ones always stay in view. A seat's ×
|
|
120
|
+
takes that member off, as unticking it does. The tags grey out the models that
|
|
121
|
+
do not fit them.
|
|
114
122
|
|
|
115
123
|
Each call also seats explorers, the models unused longest, up to the gateway's
|
|
116
124
|
limits (Gateway settings, on the same page: how many calls a request may make,
|
|
@@ -231,25 +239,54 @@ app**, switches a provider either way or adds one.
|
|
|
231
239
|
|
|
232
240
|
## Quota management
|
|
233
241
|
|
|
234
|
-
One tile per provider a bench model arrives through
|
|
235
|
-
provider limits
|
|
242
|
+
One tile per provider a bench model arrives through. Each draws the
|
|
243
|
+
provider's limits at the level they apply, and names a model only where a
|
|
244
|
+
limit is that model's own:
|
|
236
245
|
- **Windows**: CLI Proxy API's sign-ins have a 5-hour and a weekly limit per
|
|
237
|
-
model group
|
|
246
|
+
model group, a Codex sign-in one for the account. Other providers have the
|
|
247
|
+
windows their replies' headers state; a window past its reset is shown full.
|
|
238
248
|
- **Pool**: OpenRouter's free requests a day, shared by its models, with a bar
|
|
239
|
-
per key.
|
|
240
|
-
- **
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
per key, against the limit OpenRouter stated in a rate-limit reply.
|
|
250
|
+
- **Caps**: a day or month cap, counted from the bench's own calls: one you set
|
|
251
|
+
on Endpoints (Rate limits per connection), else one the provider states in
|
|
252
|
+
its replies without saying what is left (a Cohere trial key's 1,000 calls a
|
|
253
|
+
month), shown but not enforced until you set it. Calls made elsewhere with
|
|
254
|
+
the same key are not in the count.
|
|
255
|
+
- **Nothing stated**: how the provider's calls went in the last 24 hours, all
|
|
256
|
+
its models together (green answered, red failed).
|
|
257
|
+
|
|
258
|
+
Nothing is taken from a provider's docs, which can change without notice; a
|
|
259
|
+
limit a provider states in a rate-limit error (UnoRouter's "allows 1
|
|
260
|
+
request(s) every 1 min" for a model) is learned and kept to. One exception,
|
|
261
|
+
watched: NVIDIA states no limit per key, so its tile reads the file
|
|
262
|
+
build.nvidia.com's own badge reads (10,000 requests a day, up to 40 a minute,
|
|
263
|
+
the same for every account) daily and on Refresh, and counts the bench's
|
|
264
|
+
requests against it. If NVIDIA changes those figures, Action needed says so
|
|
265
|
+
(Got it dismisses it); if the file moves or changes shape, Action needed says
|
|
266
|
+
that too, and the tile keeps the figures it last read. Each meter leads
|
|
267
|
+
with the percentage left, then what is left of what in the provider's own unit
|
|
268
|
+
("94% left · 189 of 200 requests · day"), with its reset on the right. Under the name, one block says where the figures come
|
|
269
|
+
from (**From**: the provider's quota endpoint or its replies' headers, exact;
|
|
270
|
+
or a stated limit counted by the bench), your per-minute caps, and anything a
|
|
271
|
+
live read reported. **Models** names the connection and how it is reached
|
|
272
|
+
(each endpoint or Open WebUI connection, with its address), then lists the
|
|
273
|
+
provider's bench models (each opens its popup), whether each is free to call
|
|
274
|
+
now, and its last 24 hours of calls.
|
|
275
|
+
|
|
276
|
+
**Refresh** picks its own way, and hovering it says which:
|
|
277
|
+
- **A free read** on OpenRouter (it asks the key what is left), on xKiro (its
|
|
278
|
+
usage endpoint: free-model tokens left today), on Requesty (its usage
|
|
279
|
+
endpoint: the key's free-model requests today, by Requesty's count; its limit
|
|
280
|
+
only once a rate-limit reply has stated it) and on CLI Proxy API's sign-ins
|
|
281
|
+
(it asks the proxy for each one's quota, as its own console does; this needs
|
|
282
|
+
CLI Proxy API on the Endpoints page as a hub, with its management key).
|
|
283
|
+
xKiro's and Requesty's keys come from their endpoint, else their Open WebUI
|
|
284
|
+
connection.
|
|
285
|
+
- **One 16-token request** where a provider states its limits only in its
|
|
286
|
+
replies' headers, to read them (Groq, Routeway, a Cohere trial key). Every
|
|
287
|
+
ordinary call reads them too, through Open WebUI as well, since the bench
|
|
288
|
+
streams.
|
|
289
|
+
- **The bench's own records** again everywhere else.
|
|
253
290
|
|
|
254
291
|
Tiles sort with the ones that are out first, then the soonest reset. Drag a
|
|
255
292
|
tile, or move it with the arrow keys, and the order is kept; **Sort** brings
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.5.
|
|
1
|
+
0.5.2
|
package/metrics/catalog.py
CHANGED
|
@@ -283,6 +283,11 @@ def refresh() -> dict:
|
|
|
283
283
|
"refreshed": dt.datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
284
284
|
"models": models}
|
|
285
285
|
CATALOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
286
|
+
try: # NVIDIA's stated limits, watched daily (an alert on a change)
|
|
287
|
+
import quotaview
|
|
288
|
+
quotaview.watch_nvidia()
|
|
289
|
+
except Exception: # noqa: BLE001
|
|
290
|
+
pass
|
|
286
291
|
return data
|
|
287
292
|
|
|
288
293
|
|
package/metrics/learn.py
CHANGED
|
@@ -60,9 +60,17 @@ RETRY = [
|
|
|
60
60
|
r"\"retry_?delay\"\s*:\s*\"(\d+(?:\.\d+)?)s",
|
|
61
61
|
r"\"retry_after\"\s*:\s*(\d+(?:\.\d+)?)",
|
|
62
62
|
r"(?:try|retry) again in\s*(\d+(?:\.\d+)?)\s*(s|sec|seconds?|m|min|minutes?|h|hours?)\b",
|
|
63
|
-
r"
|
|
63
|
+
r"\bretry in\s*(\d+(?:\.\d+)?)\s*(s|m|h)", # "please retry in 2s", UnoRouter's "retry in 60s"
|
|
64
64
|
]
|
|
65
65
|
RATE_LIMITED = re.compile(r"rate.?limit|too many requests|\b429\b|quota exceeded|resource_exhausted")
|
|
66
|
+
# The number and its window in one phrase: UnoRouter's free tier, "allows 5
|
|
67
|
+
# request(s) every 1 min per account on <model>:free". "every" only: the
|
|
68
|
+
# bench's own refusal says "allows 3 requests per minute" (pacing.py), and a
|
|
69
|
+
# limit the bench made up must never be learned back.
|
|
70
|
+
ALLOWS = re.compile(r"allows?\s+([\d,]+)\s+requests?(?:\(s\))?\s+every\s+(?:1\s+)?(min(?:ute)?|day)\b")
|
|
71
|
+
# new-api gateways (Agnes): "maximum 20 requests in 1 minutes" (its
|
|
72
|
+
# middleware/model-rate-limit.go); a window of 1 minute or 1,440 only.
|
|
73
|
+
MAXIMUM = re.compile(r"maximum\s+([\d,]+)\s+requests?\s+in\s+(1|1440)\s+minutes?\b")
|
|
66
74
|
|
|
67
75
|
|
|
68
76
|
def _int(s: str) -> int:
|
|
@@ -83,6 +91,10 @@ def parse(text: str) -> dict:
|
|
|
83
91
|
named = [axis for axis, pat in AXES.items() if re.search(pat, low)]
|
|
84
92
|
if len(named) == 1 and (m := re.search(r"\blimit\b[\s:=]*([\d,]+)", low)):
|
|
85
93
|
out["limits"] = {named[0]: _int(m.group(1))}
|
|
94
|
+
elif m := ALLOWS.search(low):
|
|
95
|
+
out["limits"] = {"rpd" if m.group(2) == "day" else "rpm": _int(m.group(1))}
|
|
96
|
+
elif m := MAXIMUM.search(low):
|
|
97
|
+
out["limits"] = {"rpd" if m.group(2) == "1440" else "rpm": _int(m.group(1))}
|
|
86
98
|
for pat in RETRY:
|
|
87
99
|
if m := re.search(pat, low):
|
|
88
100
|
n = float(m.group(1))
|
|
@@ -205,6 +217,18 @@ def _selftest() -> None:
|
|
|
205
217
|
assert p["retry_after_s"] == 180 and "limits" not in p, p # no number under a named axis
|
|
206
218
|
# Two axes named: the number is not filed under either.
|
|
207
219
|
assert "limits" not in parse("limit 100 (tokens per minute) and requests per minute")
|
|
220
|
+
# UnoRouter's free tier says the number and its window together.
|
|
221
|
+
p = parse("Error code: 429 - {'error': {'message': 'Too many requests. The free tier allows 5 request(s) every"
|
|
222
|
+
" 1 min per account on llama-3.2-11b-vision:free - nothing is used up, retry in 60s.")
|
|
223
|
+
assert p["limits"] == {"rpm": 5} and p["retry_after_s"] == 60 and p["rate_limited"], p
|
|
224
|
+
assert parse("The free tier allows 200 requests every day")["limits"] == {"rpd": 200}
|
|
225
|
+
assert "limits" not in parse("allows 3 requests every 5 min") # a window no axis holds
|
|
226
|
+
assert "limits" not in parse("rate limit (not sent; paced by the bench): x allows 3 requests per minute")
|
|
227
|
+
# A new-api gateway's 429 (Agnes).
|
|
228
|
+
p = parse("Error code: 429 - {'error': {'message': 'You have reached the request limit: maximum 20 requests"
|
|
229
|
+
" in 1 minutes', 'type': 'AgnesAI_error'}}")
|
|
230
|
+
assert p["limits"] == {"rpm": 20} and p["rate_limited"], p
|
|
231
|
+
assert "limits" not in parse("maximum 100 requests in 5 minutes") # a window no axis holds
|
|
208
232
|
assert parse("model not found") == {"rate_limited": False}
|
|
209
233
|
assert parse("context length of 8k exceeded").get("context_window") is None # "8" < 512
|
|
210
234
|
print("ok")
|
package/metrics/quota.py
CHANGED
|
@@ -21,7 +21,11 @@ its reset, so a connection is never held for want of traffic to observe one.
|
|
|
21
21
|
ONE HEADER NAME, DIFFERENT WINDOWS
|
|
22
22
|
----------------------------------
|
|
23
23
|
`x-ratelimit-limit-requests` is per minute at OpenAI and per day at Groq;
|
|
24
|
-
Cerebras names its windows (`-requests-day`, `-tokens-minute`)
|
|
24
|
+
Cerebras names its windows (`-requests-day`, `-tokens-minute`), Routeway only
|
|
25
|
+
its windows (`X-RateLimit-Remaining-Day`, requests, resets in seconds), and a
|
|
26
|
+
Cohere trial key its minute (`x-trial-endpoint-call-remaining`, no reset) and
|
|
27
|
+
its month's limit with nothing left said (`x-endpoint-monthly-call-limit`:
|
|
28
|
+
kept, never enforced, and quotaview counts the month from the ledger); OpenRouter
|
|
25
29
|
sends bare `X-RateLimit-*` only on a 429, inside the body's metadata as well,
|
|
26
30
|
and its message says which window ("free-models-per-day"); Anthropic's
|
|
27
31
|
`anthropic-ratelimit-*` are per minute; Gemini sends no headers but its 429
|
|
@@ -76,16 +80,25 @@ PROFILES = {
|
|
|
76
80
|
WINDOWS = {"minute": 60, "hour": 3600, "day": 86400}
|
|
77
81
|
OPENAI_STYLE = re.compile(r"^x-ratelimit-(limit|remaining|reset)-(requests|tokens)(?:-(minute|hour|day))?$")
|
|
78
82
|
BARE = re.compile(r"^x-ratelimit-(limit|remaining|reset)$")
|
|
83
|
+
WINDOWED = re.compile(r"^x-ratelimit-(limit|remaining|reset)-(minute|hour|day)$") # Routeway: requests
|
|
84
|
+
COHERE = re.compile(r"^x-trial-endpoint-call-(limit|remaining)$") # a trial key's minute
|
|
85
|
+
COHERE_MONTH = "x-endpoint-monthly-call-limit" # a limit with no remaining: quotaview counts it
|
|
79
86
|
ANTHROPIC = re.compile(r"^anthropic-ratelimit-(requests|tokens|input-tokens|output-tokens)-(limit|remaining|reset)$")
|
|
80
87
|
DURATION = re.compile(r"^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m(?!s))?(?:(\d+(?:\.\d+)?)s)?(?:(\d+(?:\.\d+)?)ms)?$")
|
|
81
88
|
|
|
82
89
|
|
|
83
|
-
def profile_for(base: str) -> tuple[str, dict]:
|
|
84
|
-
"""The profile for an endpoint's base URL
|
|
90
|
+
def profile_for(base: str, connection: str = "") -> tuple[str, dict]:
|
|
91
|
+
"""The profile for an endpoint's base URL, else for the connection (a hub
|
|
92
|
+
such as Open WebUI relays the provider's headers: `groq`, `api.groq.com`);
|
|
93
|
+
("generic", {}) for any other."""
|
|
85
94
|
host = (urllib.parse.urlsplit(base or "").hostname or "").lower()
|
|
95
|
+
conn = (connection or "").lower()
|
|
86
96
|
for known, prof in PROFILES.items():
|
|
87
97
|
if host == known or host.endswith("." + known):
|
|
88
98
|
return prof
|
|
99
|
+
for known, prof in PROFILES.items():
|
|
100
|
+
if conn in (known, prof[0]) or conn.endswith("." + known):
|
|
101
|
+
return prof
|
|
89
102
|
return "generic", {}
|
|
90
103
|
|
|
91
104
|
|
|
@@ -148,8 +161,15 @@ def from_headers(headers, profile: tuple[str, dict], now: float, status: int = 2
|
|
|
148
161
|
what, metric, window = m.group(1), m.group(2), m.group(3) or defaults.get(m.group(2), "unknown")
|
|
149
162
|
elif m := BARE.match(key):
|
|
150
163
|
what, metric, window = m.group(1), "requests", _window_hint(hint)
|
|
164
|
+
elif m := WINDOWED.match(key):
|
|
165
|
+
what, metric, window = m.group(1), "requests", m.group(2)
|
|
151
166
|
elif m := ANTHROPIC.match(key):
|
|
152
167
|
metric, what, window = m.group(1), m.group(2), "minute"
|
|
168
|
+
elif m := COHERE.match(key): # no reset sent: the minute is the wait
|
|
169
|
+
what, metric, window = m.group(1), "requests", "minute"
|
|
170
|
+
fields.setdefault((metric, window), {}).setdefault("reset", "60")
|
|
171
|
+
elif key == COHERE_MONTH: # no remaining, no reset: never enforced (live())
|
|
172
|
+
what, metric, window = "limit", "requests", "month"
|
|
153
173
|
else:
|
|
154
174
|
continue
|
|
155
175
|
fields.setdefault((metric, window), {})[what] = value
|
|
@@ -247,7 +267,7 @@ def store(connection: str, alias: str, records: list[dict], now: float | None =
|
|
|
247
267
|
def observe(alias: str, connection: str, base: str, status: int, headers, body=None) -> list[dict]:
|
|
248
268
|
"""Record what one response states; returns the records. Called by the
|
|
249
269
|
built-in client on every reply and every HTTP error."""
|
|
250
|
-
now, profile = time.time(), profile_for(base)
|
|
270
|
+
now, profile = time.time(), profile_for(base, connection)
|
|
251
271
|
hint = " ".join(str(e.get("message") or "") for e in _error_objects(body)) if status == 429 else ""
|
|
252
272
|
records = from_headers(headers, profile, now, status, hint)
|
|
253
273
|
if status == 429 and body is not None:
|
|
@@ -318,12 +338,29 @@ def _selftest() -> None:
|
|
|
318
338
|
"X-Ratelimit-Reset-Requests": "2m59.56s"}, profile_for("https://api.groq.com/openai/v1"), now)
|
|
319
339
|
g = one(r, "requests", "day")
|
|
320
340
|
assert g["remaining"] == 0 and g["reset_at"] == _iso(now + 179.56) and g["source"] == "headers (groq)", g
|
|
341
|
+
# Through a hub, the connection names the provider.
|
|
342
|
+
assert profile_for("http://owui.lan/api", "groq")[0] == "groq" == profile_for("http://owui.lan/api", "api.groq.com")[0]
|
|
343
|
+
assert profile_for("http://owui.lan/api", "api.cohere.ai") == ("generic", {})
|
|
321
344
|
# Cerebras names its windows.
|
|
322
345
|
r = from_headers({"x-ratelimit-limit-requests-day": "14400", "x-ratelimit-remaining-requests-day": "14399",
|
|
323
346
|
"x-ratelimit-reset-requests-day": "33011.38", "x-ratelimit-limit-tokens-minute": "60000",
|
|
324
347
|
"x-ratelimit-remaining-tokens-minute": "59000", "x-ratelimit-reset-tokens-minute": "11.4"},
|
|
325
348
|
profile_for("https://api.cerebras.ai/v1"), now)
|
|
326
349
|
assert one(r, "requests", "day")["limit"] == 14400 and one(r, "tokens", "minute")["remaining"] == 59000
|
|
350
|
+
# Routeway names only the window; requests, resets in seconds.
|
|
351
|
+
r = from_headers({"X-RateLimit-Limit-Day": "200", "X-RateLimit-Remaining-Day": "187", "X-RateLimit-Reset-Day": "3600",
|
|
352
|
+
"X-RateLimit-Limit-Minute": "5", "X-RateLimit-Remaining-Minute": "4", "X-RateLimit-Reset-Minute": "42"},
|
|
353
|
+
profile_for("http://owui.lan/api"), now)
|
|
354
|
+
d = one(r, "requests", "day")
|
|
355
|
+
assert d["limit"] == 200 and d["remaining"] == 187 and d["reset_at"] == _iso(now + 3600), d
|
|
356
|
+
assert one(r, "requests", "minute")["remaining"] == 4
|
|
357
|
+
# A Cohere trial key: its minute, with no reset (the minute is the wait); the month has no remaining.
|
|
358
|
+
r = from_headers({"x-trial-endpoint-call-limit": "20", "x-trial-endpoint-call-remaining": "19",
|
|
359
|
+
"x-endpoint-monthly-call-limit": "1000"}, profile_for("http://owui.lan/api"), now)
|
|
360
|
+
c = one(r, "requests", "minute")
|
|
361
|
+
assert c["limit"] == 20 and c["remaining"] == 19 and c["reset_at"] == _iso(now + 60) and len(r) == 2, r
|
|
362
|
+
mo = one(r, "requests", "month")
|
|
363
|
+
assert mo["limit"] == 1000 and mo["remaining"] is None and mo["reset_at"] is None, mo
|
|
327
364
|
# Anthropic: RFC 3339 resets, per minute.
|
|
328
365
|
r = from_headers({"anthropic-ratelimit-requests-limit": "50", "anthropic-ratelimit-requests-remaining": "0",
|
|
329
366
|
"anthropic-ratelimit-requests-reset": "2025-10-24T00:00:45Z"},
|