@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.
Files changed (50) hide show
  1. package/GATEWAY.md +9 -5
  2. package/PRIVACY.md +9 -3
  3. package/README.md +31 -8
  4. package/USAGE.md +81 -15
  5. package/VERSION +1 -1
  6. package/cli.py +17 -5
  7. package/gateway.py +34 -2
  8. package/launcher.py +2 -1
  9. package/metrics/aliases.py +24 -3
  10. package/metrics/discover.py +68 -18
  11. package/metrics/endpoints.py +36 -4
  12. package/metrics/model_roles.py +3 -1
  13. package/metrics/pacing.py +23 -3
  14. package/metrics/quotaview.py +460 -0
  15. package/metrics/route.py +3 -2
  16. package/metrics/scheduler.py +2 -1
  17. package/metrics/sources.py +29 -6
  18. package/metrics/wrong.py +3 -1
  19. package/package.json +1 -1
  20. package/scripts/check.py +3 -0
  21. package/scripts/onboard.py +12 -1
  22. package/scripts/test_hubs.py +10 -5
  23. package/scripts/test_no_owui.py +14 -2
  24. package/scripts/test_quota.py +12 -2
  25. package/scripts/test_serve_edit.py +58 -5
  26. package/scripts/test_server_json.py +22 -2
  27. package/serve.py +570 -128
  28. package/suite/README.md +33 -0
  29. package/suite/pace.py +8 -4
  30. package/suite/run.py +144 -16
  31. package/suite/tasks/vision-bars/fixtures/bars.png +0 -0
  32. package/suite/tasks/vision-bars/task.json +40 -0
  33. package/suite/tasks/vision-invoice/fixtures/invoice.png +0 -0
  34. package/suite/tasks/vision-invoice/task.json +40 -0
  35. package/suite/tasks/vision-label/fixtures/label.png +0 -0
  36. package/suite/tasks/vision-label/task.json +52 -0
  37. package/suite/tasks/vision-receipt/fixtures/receipt.png +0 -0
  38. package/suite/tasks/vision-receipt/task.json +44 -0
  39. package/suite/tasks/vision-screen/fixtures/screen.png +0 -0
  40. package/suite/tasks/vision-screen/task.json +48 -0
  41. package/suite/tasks/vision-shapes/fixtures/shapes.png +0 -0
  42. package/suite/tasks/vision-shapes/task.json +40 -0
  43. package/suite/tasks/vision-table/fixtures/table.png +0 -0
  44. package/suite/tasks/vision-table/task.json +40 -0
  45. package/suite/test_vision_tasks.py +201 -0
  46. package/suite/vision_keys.py +74 -0
  47. package/suite/vision_sample.py +232 -0
  48. package/suite/vision_tasks.py +338 -0
  49. package/ui/bench.css +71 -6
  50. package/ui/bench.js +543 -49
@@ -1,10 +1,16 @@
1
- """One list of models worth adding, from models.dev and FreeLLMAPI, deduped.
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 and the better-filled fields.
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, a 4-call probe, then the suite"],
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
- lim = {k: v for k, v in (m.get("limits") or {}).items() if v} or None
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["sources"].append("freellmapi")
198
- row["free"] = True # listed in a free-tier catalog
199
- row["limits"] = lim
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, "provider_name": prov, "model": m.get("modelId"),
206
- "name": m.get("displayName") or m.get("modelId"), "sources": ["freellmapi"],
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
- out.sort(key=lambda r: (not r["free"], not r["new"], order[r["reach"]], r["provider"], r["model"]))
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 + FreeLLMAPI;"
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)")
@@ -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
- return next((n for n, e in data.items() if e.get("default")), next(iter(data)))
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": {}})
@@ -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
- spans = [AXES[r["axis"]][1] for r in rs]
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))