@wendellresearch-ai/benchwarmers 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GATEWAY.md +9 -5
- package/PRIVACY.md +9 -3
- package/README.md +31 -8
- package/USAGE.md +81 -15
- package/VERSION +1 -1
- package/cli.py +17 -5
- package/gateway.py +34 -2
- package/launcher.py +2 -1
- package/metrics/aliases.py +24 -3
- package/metrics/discover.py +68 -18
- package/metrics/endpoints.py +36 -4
- package/metrics/model_roles.py +3 -1
- package/metrics/pacing.py +23 -3
- package/metrics/quotaview.py +460 -0
- package/metrics/route.py +3 -2
- package/metrics/scheduler.py +2 -1
- package/metrics/sources.py +29 -6
- package/metrics/wrong.py +3 -1
- package/package.json +1 -1
- package/scripts/check.py +3 -0
- package/scripts/onboard.py +12 -1
- package/scripts/test_hubs.py +10 -5
- package/scripts/test_no_owui.py +14 -2
- package/scripts/test_quota.py +12 -2
- package/scripts/test_serve_edit.py +58 -5
- package/scripts/test_server_json.py +22 -2
- package/serve.py +570 -128
- package/suite/README.md +33 -0
- package/suite/pace.py +8 -4
- package/suite/run.py +144 -16
- package/suite/tasks/vision-bars/fixtures/bars.png +0 -0
- package/suite/tasks/vision-bars/task.json +40 -0
- package/suite/tasks/vision-invoice/fixtures/invoice.png +0 -0
- package/suite/tasks/vision-invoice/task.json +40 -0
- package/suite/tasks/vision-label/fixtures/label.png +0 -0
- package/suite/tasks/vision-label/task.json +52 -0
- package/suite/tasks/vision-receipt/fixtures/receipt.png +0 -0
- package/suite/tasks/vision-receipt/task.json +44 -0
- package/suite/tasks/vision-screen/fixtures/screen.png +0 -0
- package/suite/tasks/vision-screen/task.json +48 -0
- package/suite/tasks/vision-shapes/fixtures/shapes.png +0 -0
- package/suite/tasks/vision-shapes/task.json +40 -0
- package/suite/tasks/vision-table/fixtures/table.png +0 -0
- package/suite/tasks/vision-table/task.json +40 -0
- package/suite/test_vision_tasks.py +201 -0
- package/suite/vision_keys.py +74 -0
- package/suite/vision_sample.py +232 -0
- package/suite/vision_tasks.py +338 -0
- package/ui/bench.css +71 -6
- package/ui/bench.js +543 -49
package/metrics/discover.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
"""One list of models worth adding, from models.dev
|
|
1
|
+
"""One list of models worth adding, from FreeLLMAPI and models.dev, deduped.
|
|
2
2
|
|
|
3
3
|
python metrics/discover.py # summary: free models by reach, new this week
|
|
4
4
|
|
|
5
5
|
The shopping list: Discover's Add (serve.py) puts a model on the bench by one
|
|
6
6
|
of the routes below. A model offered by the same provider in both sources
|
|
7
|
-
appears once, carrying both sources
|
|
7
|
+
appears once, carrying both sources. FreeLLMAPI leads where it lists the
|
|
8
|
+
model (its name, its provider's name, context, tools, limits, its smartest
|
|
9
|
+
and fastest ranks, size, monthly budget, quirks and freellmapi.co page);
|
|
10
|
+
models.dev fills the rest, and alone has the paid models and prices, API base
|
|
11
|
+
URLs (reach, direct routes), modalities, release and knowledge dates.
|
|
12
|
+
Image and audio generation in FreeLLMAPI's list are left out: the bench asks
|
|
13
|
+
for text.
|
|
8
14
|
|
|
9
15
|
FREE
|
|
10
16
|
----
|
|
@@ -149,7 +155,7 @@ def hub_routes(rows: list[dict]) -> tuple[list[list[dict]], list[str]]:
|
|
|
149
155
|
mine.append({"via": "hub", "hub": which, "connection": conn, "endpoint": ep,
|
|
150
156
|
"label": f"{r['provider_name']} via {label} ({conn} connection)",
|
|
151
157
|
"note": f"Also serves it to everything else that uses {label}.",
|
|
152
|
-
"steps": ["; ".join(said), "The bench: register
|
|
158
|
+
"steps": ["; ".join(said), "The bench: register it and onboard it as chosen below"],
|
|
153
159
|
"on_bench_as": next((a for e, _c, _n, a, s in bench if e == ep and s == served), None)})
|
|
154
160
|
out.append(mine)
|
|
155
161
|
return out, errors
|
|
@@ -158,6 +164,8 @@ def hub_routes(rows: list[dict]) -> tuple[list[list[dict]], list[str]]:
|
|
|
158
164
|
def build() -> list[dict]:
|
|
159
165
|
import endpoints
|
|
160
166
|
md, fl = sources.cached("modelsdev"), sources.cached("freellmapi")
|
|
167
|
+
pages = set(sources.cached("freellmapi-pages").get("slugs") or [])
|
|
168
|
+
platform_names = {p.get("id"): p.get("name") for p in fl.get("platforms") or []}
|
|
161
169
|
md_conn = {v: k for k, v in sources.connection_to_modelsdev().items()}
|
|
162
170
|
bench = _on_bench()
|
|
163
171
|
# Open WebUI's connections, as far as the bench can tell without asking it.
|
|
@@ -185,31 +193,37 @@ def build() -> list[dict]:
|
|
|
185
193
|
"vision": "image" in ((m.get("modalities") or {}).get("input") or []),
|
|
186
194
|
"released": m.get("release_date"), "knowledge": m.get("knowledge"),
|
|
187
195
|
"open_weights": m.get("open_weights"), "api": p.get("api"),
|
|
188
|
-
"openai_like": openai_like, "connection": md_conn.get(prov), "limits": None
|
|
196
|
+
"openai_like": openai_like, "connection": md_conn.get(prov), "limits": None,
|
|
197
|
+
"rank": None, "speed": None, "size": None, "budget": None, "quirks": [], "page": None}
|
|
189
198
|
|
|
190
199
|
for m in fl.get("models") or []:
|
|
200
|
+
if (m.get("modality") or "chat") != "chat":
|
|
201
|
+
continue # image or audio generation
|
|
191
202
|
plat = m.get("platform", "")
|
|
192
203
|
prov = sources.FREELLMAPI_TO_MODELSDEV.get(plat, plat)
|
|
193
204
|
key = (prov, sources.norm_model(m.get("modelId", "")))
|
|
194
|
-
|
|
205
|
+
lead = {"sources": ["freellmapi"], "free": True, # listed in a free-tier catalog
|
|
206
|
+
"name": sources.freellmapi_name(m.get("displayName") or "") or m.get("modelId"),
|
|
207
|
+
"provider_name": platform_names.get(plat) or prov,
|
|
208
|
+
"limits": {k: v for k, v in (m.get("limits") or {}).items() if v} or None,
|
|
209
|
+
"rank": m.get("intelligenceRank"), "speed": m.get("speedRank"),
|
|
210
|
+
"size": m.get("sizeLabel") or None, "budget": m.get("monthlyTokenBudget") or None,
|
|
211
|
+
"quirks": [{k: q.get(k) for k in ("title", "body", "severity")} for q in m.get("quirks") or []],
|
|
212
|
+
"page": sources.freellmapi_page(m.get("displayName") or "", pages)}
|
|
195
213
|
row = rows.get(key)
|
|
196
214
|
if row:
|
|
197
|
-
row
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
row["context"] = row["context"] or m.get("contextWindow")
|
|
201
|
-
row["tools"] = row["tools"] if row["tools"] is not None else m.get("supportsTools")
|
|
215
|
+
row.update(lead, sources=list(dict.fromkeys(["freellmapi", *row["sources"]])),
|
|
216
|
+
context=m.get("contextWindow") or row["context"], # the free tier's, where it says
|
|
217
|
+
tools=row["tools"] if m.get("supportsTools") is None else m.get("supportsTools"))
|
|
202
218
|
else:
|
|
203
219
|
rows[key] = {
|
|
204
|
-
"in_mods": ["text", "image"] if m.get("supportsVision") else ["text"], "out_mods": ["text"],
|
|
205
|
-
"provider": prov, "
|
|
206
|
-
"
|
|
207
|
-
"free": True, "cost": None, "context": m.get("contextWindow"), "output": None,
|
|
220
|
+
**lead, "in_mods": ["text", "image"] if m.get("supportsVision") else ["text"], "out_mods": ["text"],
|
|
221
|
+
"provider": prov, "model": m.get("modelId"),
|
|
222
|
+
"cost": None, "context": m.get("contextWindow"), "output": None,
|
|
208
223
|
"tools": m.get("supportsTools"), "reasoning": None,
|
|
209
224
|
"vision": m.get("supportsVision"), "released": None, "knowledge": None,
|
|
210
225
|
"open_weights": None, "api": None,
|
|
211
|
-
"openai_like": False, "connection": FREELLMAPI_TO_CONNECTION.get(plat)
|
|
212
|
-
"limits": lim}
|
|
226
|
+
"openai_like": False, "connection": FREELLMAPI_TO_CONNECTION.get(plat)}
|
|
213
227
|
|
|
214
228
|
# The same model through ANOTHER provider is fine to add, but worth knowing.
|
|
215
229
|
by_model: dict[str, list[str]] = {}
|
|
@@ -245,7 +259,11 @@ def build() -> list[dict]:
|
|
|
245
259
|
out.append(r)
|
|
246
260
|
_write_seen(seen)
|
|
247
261
|
order = {"connected": 0, "easy to add": 1, "on bench": 2, "other": 3}
|
|
248
|
-
|
|
262
|
+
# Within each reach, FreeLLMAPI's smartest first; the models it does not rank
|
|
263
|
+
# after, new ones first. Not new first overall: models.dev adds hundreds a
|
|
264
|
+
# week, which buried the ranked ones (the badge still marks them).
|
|
265
|
+
out.sort(key=lambda r: (not r["free"], order[r["reach"]], r["rank"] is None, r["rank"] or 0, not r["new"],
|
|
266
|
+
r["provider"], r["model"]))
|
|
249
267
|
return out
|
|
250
268
|
|
|
251
269
|
|
|
@@ -293,6 +311,38 @@ def _selftest() -> None:
|
|
|
293
311
|
over = sources.free_in_own_client_only({"opencode": False, "x": True})
|
|
294
312
|
assert over["opencode"] is False and over["x"] is True and over["opencode-go"] is True, over
|
|
295
313
|
assert sources.free_in_own_client_only(True) == sources.free_in_own_client_only({}) # not a map: ignored
|
|
314
|
+
# freellmapi.co names a page by the display name without its note; only its sitemap's pages exist.
|
|
315
|
+
assert sources.freellmapi_page("Llama 4 Scout (CF)", {"llama-4-scout"}) == "https://freellmapi.co/model/llama-4-scout"
|
|
316
|
+
assert sources.freellmapi_page("GLM-4.7 FlashX", {"llama-4-scout"}) is None
|
|
317
|
+
# FreeLLMAPI leads where it lists a model, models.dev fills the rest; image models out; smartest first.
|
|
318
|
+
md = {"acme": {"name": "Acme", "api": "https://acme.example/v1", "models": {
|
|
319
|
+
"acme/big": {"name": "Big", "cost": {"input": 1, "output": 2}, "limit": {"context": 8000},
|
|
320
|
+
"modalities": {"input": ["text", "image"], "output": ["text"]}, "release_date": "2026-01-01"},
|
|
321
|
+
"acme/small": {"name": "Small", "cost": {"input": 0, "output": 0}},
|
|
322
|
+
"acme/tiny": {"name": "Tiny", "cost": {"input": 0, "output": 0}}}}}
|
|
323
|
+
fl = {"platforms": [{"id": "acme", "name": "Acme Cloud"}], "models": [
|
|
324
|
+
{"platform": "acme", "modelId": "acme/big", "displayName": "Big One (Acme)", "intelligenceRank": 3,
|
|
325
|
+
"speedRank": 9, "sizeLabel": "Large", "monthlyTokenBudget": "~3M", "contextWindow": 4000,
|
|
326
|
+
"limits": {"rpm": 10, "rpd": None}, "quirks": [{"slug": "q", "title": "T", "body": "B", "severity": "info"}]},
|
|
327
|
+
{"platform": "acme", "modelId": "acme/painter", "displayName": "Painter", "modality": "image"},
|
|
328
|
+
{"platform": "acme", "modelId": "acme/tiny", "displayName": "Tiny", "intelligenceRank": 1}]}
|
|
329
|
+
feeds = {"modelsdev": md, "freellmapi": fl, "freellmapi-pages": {"slugs": ["big-one"]}}
|
|
330
|
+
import tempfile
|
|
331
|
+
kept = sources.cached, globals()["_on_bench"], globals()["SEEN"]
|
|
332
|
+
try:
|
|
333
|
+
sources.cached = lambda name: feeds.get(name, {})
|
|
334
|
+
globals().update(_on_bench=lambda: [], SEEN=pathlib.Path(tempfile.mkdtemp()) / "seen.json")
|
|
335
|
+
rows = [r for r in build() if r["provider"] == "acme"]
|
|
336
|
+
finally:
|
|
337
|
+
sources.cached = kept[0]
|
|
338
|
+
globals().update(_on_bench=kept[1], SEEN=kept[2])
|
|
339
|
+
big = next(r for r in rows if r["model"] == "acme/big")
|
|
340
|
+
assert (big["sources"], big["name"], big["provider_name"]) == (["freellmapi", "models.dev"], "Big One", "Acme Cloud"), big
|
|
341
|
+
assert big["free"] and big["context"] == 4000 and big["limits"] == {"rpm": 10} and big["released"] == "2026-01-01", big
|
|
342
|
+
assert (big["rank"], big["speed"], big["size"], big["budget"]) == (3, 9, "Large", "~3M"), big
|
|
343
|
+
assert big["page"] == "https://freellmapi.co/model/big-one" and big["quirks"] == [
|
|
344
|
+
{"title": "T", "body": "B", "severity": "info"}] and big["vision"], big
|
|
345
|
+
assert [r["model"] for r in rows] == ["acme/tiny", "acme/big", "acme/small"], [r["model"] for r in rows]
|
|
296
346
|
print("discover selftest ok")
|
|
297
347
|
|
|
298
348
|
|
|
@@ -306,7 +356,7 @@ def main() -> None:
|
|
|
306
356
|
by = {}
|
|
307
357
|
for r in free:
|
|
308
358
|
by[r["reach"]] = by.get(r["reach"], 0) + 1
|
|
309
|
-
print(f"{len(rows)} models ({len(free)} free) from models.dev
|
|
359
|
+
print(f"{len(rows)} models ({len(free)} free) from FreeLLMAPI + models.dev;"
|
|
310
360
|
f" free by reach: {by}; new this week: {sum(r['new'] for r in rows)}")
|
|
311
361
|
both = sum(1 for r in rows if len(r["sources"]) > 1)
|
|
312
362
|
print(f"{both} listed by both sources (shown once)")
|
package/metrics/endpoints.py
CHANGED
|
@@ -154,13 +154,29 @@ def connections() -> dict:
|
|
|
154
154
|
return data if isinstance(data, dict) else {}
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
_default: tuple = (None, "")
|
|
158
|
+
|
|
159
|
+
|
|
157
160
|
def default() -> str:
|
|
161
|
+
"""The default endpoint's name, read again only when endpoints.json
|
|
162
|
+
changes: snapshot_key asks once per model, hundreds of times a page."""
|
|
163
|
+
global _default
|
|
164
|
+
try:
|
|
165
|
+
st = FILE.stat()
|
|
166
|
+
key = (str(FILE), st.st_mtime_ns, st.st_size)
|
|
167
|
+
except OSError:
|
|
168
|
+
key = None
|
|
169
|
+
seen = _default
|
|
170
|
+
if key and seen[0] == key:
|
|
171
|
+
return seen[1]
|
|
158
172
|
data = load()
|
|
159
173
|
if not data:
|
|
160
174
|
raise RuntimeError(f"no endpoints configured: add one from the dashboard (python serve.py), run"
|
|
161
175
|
f" scripts/init.py --endpoint NAME BASE_URL, or create {FILE}"
|
|
162
176
|
' like {"name": {"base": "https://.../v1", "key": "name", "default": true}}')
|
|
163
|
-
|
|
177
|
+
name = next((n for n, e in data.items() if e.get("default")), next(iter(data)))
|
|
178
|
+
_default = (key, name)
|
|
179
|
+
return name
|
|
164
180
|
|
|
165
181
|
|
|
166
182
|
def kind(name: str, data: dict | None = None) -> str:
|
|
@@ -322,13 +338,29 @@ def endpoint_name(reg: dict, data: dict | None = None) -> str | None:
|
|
|
322
338
|
return next((n for n in same if data[n].get("key") == reg.get("api_key_name")), same[0] if same else None)
|
|
323
339
|
|
|
324
340
|
|
|
341
|
+
_parsed: tuple = (None, {})
|
|
342
|
+
|
|
343
|
+
|
|
325
344
|
def registrations() -> dict[str, dict]:
|
|
326
345
|
"""alias -> its yaml entry as written by yaml_block: model_name, api_base,
|
|
327
|
-
api_key_name and a `headers` dict. Not a YAML parser; the file is ours.
|
|
346
|
+
api_key_name and a `headers` dict. Not a YAML parser; the file is ours.
|
|
347
|
+
|
|
348
|
+
Parsed again only when the file changes (a dashboard render asks hundreds
|
|
349
|
+
of times); every caller gets its own copies to change."""
|
|
350
|
+
global _parsed
|
|
351
|
+
try:
|
|
352
|
+
st = YAML.stat()
|
|
353
|
+
except OSError: # a fresh install has registered nothing yet
|
|
354
|
+
return {}
|
|
355
|
+
key = (str(YAML), st.st_mtime_ns, st.st_size)
|
|
356
|
+
if _parsed[0] != key:
|
|
357
|
+
_parsed = (key, _parse_registrations())
|
|
358
|
+
return {a: dict(r, headers=dict(r["headers"])) for a, r in _parsed[1].items()}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_registrations() -> dict[str, dict]:
|
|
328
362
|
out: dict[str, dict] = {}
|
|
329
363
|
current = None
|
|
330
|
-
if not YAML.exists(): # a fresh install has registered nothing yet
|
|
331
|
-
return out
|
|
332
364
|
for line in YAML.read_text(encoding="utf-8").splitlines():
|
|
333
365
|
if (m := re.match(r"\s*-\s*model_id:\s*(.+)", line)):
|
|
334
366
|
current = out.setdefault(m.group(1).strip().strip("\"'"), {"headers": {}})
|
package/metrics/model_roles.py
CHANGED
|
@@ -20,7 +20,9 @@ roles: general reasoning / do-it-all: eligible for every shape
|
|
|
20
20
|
charts), weak at text-only work: admitted by the vision shape
|
|
21
21
|
alone, and a collection member with this role sits only when
|
|
22
22
|
the request carries images (gateway.py). A model that reads
|
|
23
|
-
images AND does everything else stays general.
|
|
23
|
+
images AND does everything else stays general. `bench onboard
|
|
24
|
+
--vision` (the picker's Vision model box) onboards one this
|
|
25
|
+
way; its suite is the vision tasks alone (suite/run.applies).
|
|
24
26
|
decision typed answers with probabilities through a Decisions API, not
|
|
25
27
|
chat; no shape admits them (config/decision-models.json)
|
|
26
28
|
|
package/metrics/pacing.py
CHANGED
|
@@ -177,6 +177,28 @@ def _ago(s: float) -> str:
|
|
|
177
177
|
return "just now" if s < 60 else f"{s / 60:.0f} min ago" if s < 7200 else f"{s / 3600:.0f} h ago"
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def _since(rs: list[dict], records: list[dict]) -> float:
|
|
181
|
+
"""The oldest ledger line any of these windows or quota records counts."""
|
|
182
|
+
spans = [AXES[r["axis"]][1] for r in rs]
|
|
183
|
+
return min([ledger.month_start(time.time()) if s == "month" else time.time() - s for s in spans]
|
|
184
|
+
+ [_ts(q["observed_at"]) for q in records if q.get("observed_at")])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def held(alias: str, connection: str, est_tokens: int = 1) -> tuple[float, str] | None:
|
|
188
|
+
"""(seconds until there is room, why) when a call to `alias` would have to
|
|
189
|
+
wait or be refused now, else None. Read only: no slot is taken. For
|
|
190
|
+
picking seats, so a model the bench knows will be refused is not seated
|
|
191
|
+
(gateway.refused_now); `admit` still decides each call."""
|
|
192
|
+
import quota
|
|
193
|
+
rs, records = rules(alias, connection), quota.live(connection, alias)
|
|
194
|
+
if not rs and not records:
|
|
195
|
+
return None
|
|
196
|
+
now = time.time()
|
|
197
|
+
block = _blocked(rs, records, alias, connection, est_tokens, now,
|
|
198
|
+
list(ledger.rows(_since(rs, records))), _inflight(now))
|
|
199
|
+
return None if block is None else (max(block[0] - now, 0.0), block[1])
|
|
200
|
+
|
|
201
|
+
|
|
180
202
|
def admit(alias: str, connection: str, est_tokens: int, left: float, sleep=time.sleep) -> tuple[str | None, str]:
|
|
181
203
|
"""Wait for room, or refuse. Returns (ticket, refusal): a ticket to pass
|
|
182
204
|
to `release` once the call's ledger line is written (None when no limit
|
|
@@ -189,9 +211,7 @@ def admit(alias: str, connection: str, est_tokens: int, left: float, sleep=time.
|
|
|
189
211
|
records = quota.live(connection, alias)
|
|
190
212
|
if not rs and not records:
|
|
191
213
|
return None, ""
|
|
192
|
-
|
|
193
|
-
since = min([ledger.month_start(time.time()) if s == "month" else time.time() - s for s in spans]
|
|
194
|
-
+ [_ts(q["observed_at"]) for q in records if q.get("observed_at")])
|
|
214
|
+
since = _since(rs, records)
|
|
195
215
|
with locked.lock(LOCK):
|
|
196
216
|
now = time.time()
|
|
197
217
|
rows = list(ledger.rows(since))
|