@wendellresearch-ai/benchwarmers 0.5.5 → 0.5.6

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.
@@ -1,6 +1,7 @@
1
- """One list of models worth adding, from FreeLLMAPI and models.dev, deduped.
1
+ """One list of models worth adding, from FreeLLMAPI, models.dev and the providers' own lists, deduped.
2
2
 
3
3
  python metrics/discover.py # summary: free models by reach, new this week
4
+ python metrics/discover.py --refresh # fetch every list now, as Discover's Refresh does
4
5
 
5
6
  The shopping list: Discover's Add (serve.py) puts a model on the bench by one
6
7
  of the routes below. A model offered by the same provider in both sources
@@ -34,7 +35,11 @@ ROUTES
34
35
  ------
35
36
  The ways this install can reach a provider, each a row's `routes`:
36
37
  openwebui a connection in Open WebUI, known because a bench model on the
37
- Open WebUI hub (endpoints.find_hub) is served from it. Adding
38
+ Open WebUI hub (endpoints.find_hub) is served from it, or from
39
+ the providers' lists (below). A row is that connection's when
40
+ the provider's API host is the connection's, or its id
41
+ (models.dev's or FreeLLMAPI's) is the connection's name or a
42
+ part of its host: FreeLLMAPI's xkiro is api.xkiro.com. Adding
38
43
  goes through metrics/owui.py, so the model also appears in the
39
44
  chats. None without an Open WebUI hub.
40
45
  direct an endpoint of its own in config/endpoints.json whose name or
@@ -50,9 +55,28 @@ The ways this install can reach a provider, each a row's `routes`:
50
55
  The same model offered by two providers (OpenRouter and NVIDIA) has a route
51
56
  on each; the dashboard's Discover popup lists them together by `norm`.
52
57
 
53
- NEW is the date a model was first seen in either list, kept in
58
+ THE PROVIDERS' OWN LISTS
59
+ ------------------------
60
+ The catalogs trail the providers by days, so a model a provider started
61
+ serving this morning is in neither. Open WebUI passes each connection's own
62
+ /models through (owui.lists), whatever its Model IDs list allows, so
63
+ refresh() keeps every connection's in runs/cache/provider-lists.json. A model
64
+ there that neither catalog has for that provider is a row of its own, source
65
+ "provider": its id, and what the list says of its name, context, price and
66
+ abilities (each provider says different things; many say nothing of price,
67
+ which is then unknown, not paid). Only through Open WebUI: the providers'
68
+ keys stay where they are, and nothing is sent anywhere new.
69
+
70
+ LINKS
71
+ -----
72
+ A row's `page` is its freellmapi.co page (where the sitemap has one), and
73
+ `md_page` its models.dev page: /models/<lab>/<id> by models.dev's canonical
74
+ id, else the provider's own page there.
75
+
76
+ NEW is the date a model was first seen in any list, kept in
54
77
  runs/discover-seen.json (gitignored). The first build records everything as a
55
- baseline, so only models that appear afterwards are badged new.
78
+ baseline, so only models that appear afterwards are badged new; so does the
79
+ first list of each connection.
56
80
  """
57
81
 
58
82
  from __future__ import annotations
@@ -61,6 +85,7 @@ import datetime as dt
61
85
  import json
62
86
  import pathlib
63
87
  import sys
88
+ from urllib.parse import urlsplit
64
89
 
65
90
  ROOT = pathlib.Path(__file__).resolve().parent.parent
66
91
  from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
@@ -74,6 +99,84 @@ FREELLMAPI_TO_CONNECTION = {"groq": "groq", "nvidia": "nvidia-api", "mistral": "
74
99
  "openrouter": "openrouter", "ollama": "ollama-cloud",
75
100
  "cerebras": "cerebras", "llm7": "llm7-api"}
76
101
  NEW_DAYS = 7
102
+ LISTS = "provider-lists" # runs/cache/provider-lists.json, written by refresh()
103
+
104
+
105
+ def refresh(force: bool = False) -> list[str]:
106
+ """Fetch the catalogs (sources.FEEDS) and the providers' own lists, each at
107
+ most once a day unless `force`. What went wrong, as sentences: a list that
108
+ could not be fetched keeps its cached copy."""
109
+ import endpoints
110
+ said = []
111
+ for name in sources.FEEDS:
112
+ try:
113
+ sources.fetch(name, force)
114
+ except Exception as exc: # noqa: BLE001
115
+ said.append(f"could not fetch {name} ({exc}); kept the cached copy")
116
+ age = sources.age_s(LISTS)
117
+ if not endpoints.find_hub("openwebui") or (not force and age is not None and age < sources.MAX_AGE_S):
118
+ return said
119
+ import owui
120
+ try:
121
+ got, failed = owui.lists()
122
+ except Exception as exc: # noqa: BLE001
123
+ return said + [f"could not ask Open WebUI for its providers' lists ({exc}); kept the cached copy"]
124
+ old = {c["names"][0]: c for c in sources.cached(LISTS).get("connections") or []}
125
+ conns = [{**c, "models": [s for s in map(_slim, c["models"]) if s]} for c in got]
126
+ sources.store(LISTS, {"at": dt.datetime.now().astimezone().isoformat(timespec="seconds"),
127
+ "connections": conns + [old[n] for n in failed if n in old]})
128
+ return said + [f"{n} would not list its models ({why})" + ("; kept its last list" if n in old else "")
129
+ for n, why in failed.items()]
130
+
131
+
132
+ def _slim(m: dict) -> dict | None:
133
+ """What Discover keeps of one model in a provider's own list; None when the
134
+ list says it is not a chat model (images, audio, embeddings, another API).
135
+ Each provider says it its own way: these are the ways seen 2026-11-07 in
136
+ OpenRouter's, Requesty's, xKiro's, Mistral's and UnoRouter's lists."""
137
+ arch = m.get("architecture") if isinstance(m.get("architecture"), dict) else {}
138
+ caps = m.get("capabilities") if isinstance(m.get("capabilities"), dict) else {}
139
+ outs = m.get("output_modalities") or arch.get("output_modalities")
140
+ ins = m.get("input_modalities") or arch.get("input_modalities")
141
+ apis = m.get("supported_endpoint_types")
142
+ if ((isinstance(outs, list) and outs and "text" not in outs) or m.get("modality") not in (None, "chat", "text")
143
+ or m.get("api") not in (None, "chat") or caps.get("completion_chat") is False
144
+ or (isinstance(apis, list) and apis and "openai" not in apis)):
145
+ return None
146
+ tier, price = m.get("access_tier"), m.get("pricing") if isinstance(m.get("pricing"), dict) else {}
147
+ nums = []
148
+ for v in (price.get("prompt"), price.get("completion"), price.get("input"), price.get("output"),
149
+ m.get("input_price"), m.get("output_price")):
150
+ try:
151
+ nums.append(float(v))
152
+ except (TypeError, ValueError):
153
+ pass
154
+ vision = caps.get("vision", m.get("supports_vision"))
155
+ if vision is None and isinstance(ins, list):
156
+ vision = "image" in ins
157
+ ctx = m.get("context_length") or m.get("context_window") or m.get("max_context_length")
158
+ return {"id": str(m["id"]), "name": m.get("display_name") or m.get("name"),
159
+ "context": ctx if isinstance(ctx, int) and ctx > 0 else None,
160
+ "free": (True if str(m["id"]).endswith(":free") or tier == "free" else
161
+ False if tier in ("paid", "premium") else all(x == 0 for x in nums) if nums else None),
162
+ "tools": caps.get("tools", caps.get("function_calling", m.get("supports_tool_calling"))),
163
+ "vision": vision, "reasoning": caps.get("reasoning", m.get("supports_reasoning"))}
164
+
165
+
166
+ def _host(base: str) -> str:
167
+ return urlsplit(base).hostname or "" if "//" in base else base
168
+
169
+
170
+ def _conn_for(ids: list[str], api: str | None, hosts: dict[str, str]) -> str | None:
171
+ """The Open WebUI connection (`hosts`: name -> its API host, the bench's
172
+ names first) that serves this provider: the provider's API host is its
173
+ host, or one of the provider's `ids` is its name or a part of its host."""
174
+ want = _host(api or "")
175
+ for name, host in hosts.items():
176
+ parts = host.split(".")[:-1] if "." in host else [host]
177
+ if (want and host == want) or any(i and (i == name or i in parts) for i in ids):
178
+ return name
179
+ return None
77
180
 
78
181
 
79
182
  def _on_bench() -> list[tuple[str, str, str, str, str]]:
@@ -168,14 +271,29 @@ def build() -> list[dict]:
168
271
  platform_names = {p.get("id"): p.get("name") for p in fl.get("platforms") or []}
169
272
  md_conn = {v: k for k, v in sources.connection_to_modelsdev().items()}
170
273
  bench = _on_bench()
171
- # Open WebUI's connections, as far as the bench can tell without asking it.
274
+ # Open WebUI's connections, as far as the bench can tell without asking it:
275
+ # its models' connections, then the providers' lists, with their API hosts.
172
276
  owui = endpoints.find_hub("openwebui")
173
- owui_conns = {c for ep, c, *_ in bench if ep == owui} - {"?"}
277
+ lists = (sources.cached(LISTS).get("connections") or []) if owui else []
278
+ hosts: dict[str, str] = dict.fromkeys(c for ep, c, *_ in bench if ep == owui and c != "?")
279
+ for c in lists:
280
+ for n in c["names"]:
281
+ hosts[n] = _host(c["base"])
282
+ hosts = {n: h or n for n, h in hosts.items()}
283
+ owui_conns = set(hosts)
284
+
285
+ def conn_of(ids: list[str], api: str | None, known: str | None) -> str | None:
286
+ return known if known in hosts else _conn_for(ids, api, hosts) or known
287
+
174
288
  direct = _direct(md)
175
289
  rows: dict[tuple[str, str], dict] = {}
290
+ # models.dev's own page for a model: by its canonical id, which names its lab.
291
+ labs = {str(m["canonical_model_id"]).split("/")[0] for p in md.values()
292
+ for m in (p.get("models") or {}).values() if m.get("canonical_model_id")}
176
293
 
177
294
  for prov, p in md.items():
178
295
  openai_like = bool(p.get("api")) or p.get("npm") == "@ai-sdk/openai-compatible"
296
+ conn = conn_of([prov], p.get("api"), md_conn.get(prov))
179
297
  for mid, m in (p.get("models") or {}).items():
180
298
  out_mods = (m.get("modalities") or {}).get("output") or ["text"]
181
299
  if "text" not in out_mods or m.get("status") == "deprecated":
@@ -193,8 +311,10 @@ def build() -> list[dict]:
193
311
  "vision": "image" in ((m.get("modalities") or {}).get("input") or []),
194
312
  "released": m.get("release_date"), "knowledge": m.get("knowledge"),
195
313
  "open_weights": m.get("open_weights"), "api": p.get("api"),
196
- "openai_like": openai_like, "connection": md_conn.get(prov), "limits": None,
197
- "rank": None, "speed": None, "size": None, "budget": None, "quirks": [], "page": None}
314
+ "openai_like": openai_like, "connection": conn, "limits": None,
315
+ "rank": None, "speed": None, "size": None, "budget": None, "quirks": [], "page": None,
316
+ "md_page": "https://models.dev/" + (f"models/{m['canonical_model_id']}" if m.get("canonical_model_id")
317
+ else f"models/{prov}/{mid}" if prov in labs else f"providers/{prov}")}
198
318
 
199
319
  for m in fl.get("models") or []:
200
320
  if (m.get("modality") or "chat") != "chat":
@@ -222,8 +342,30 @@ def build() -> list[dict]:
222
342
  "cost": None, "context": m.get("contextWindow"), "output": None,
223
343
  "tools": m.get("supportsTools"), "reasoning": None,
224
344
  "vision": m.get("supportsVision"), "released": None, "knowledge": None,
225
- "open_weights": None, "api": None,
226
- "openai_like": False, "connection": FREELLMAPI_TO_CONNECTION.get(plat)}
345
+ "open_weights": None, "api": None, "md_page": None,
346
+ "openai_like": False, "connection": conn_of([prov, plat], None, FREELLMAPI_TO_CONNECTION.get(plat))}
347
+
348
+ # What each connection's provider lists that neither catalog has for it.
349
+ listed = set()
350
+ for c in lists:
351
+ conn = c["names"][0] # the bench's name for it, as conn_of picks
352
+ provs = sorted({r["provider"] for r in rows.values() if r["connection"] in c["names"]}) or [conn]
353
+ have = {k[1] for k in rows if k[0] in provs}
354
+ name = next((r["provider_name"] for r in rows.values() if r["provider"] == provs[0]), c["names"][-1])
355
+ listed.add(conn)
356
+ for m in c["models"]:
357
+ key = (provs[0], sources.norm_model(m["id"]))
358
+ if key[1] in have:
359
+ continue
360
+ have.add(key[1])
361
+ rows[key] = {
362
+ "in_mods": ["text", "image"] if m.get("vision") else ["text"], "out_mods": ["text"],
363
+ "provider": provs[0], "provider_name": name, "model": m["id"], "name": m.get("name") or m["id"],
364
+ "sources": ["provider"], "free": m.get("free"), "cost": None, "context": m.get("context"),
365
+ "output": None, "tools": m.get("tools"), "reasoning": m.get("reasoning"), "vision": m.get("vision"),
366
+ "released": None, "knowledge": None, "open_weights": None, "api": c["base"], "openai_like": True,
367
+ "connection": conn, "limits": None, "rank": None, "speed": None, "size": None, "budget": None,
368
+ "quirks": [], "page": None, "md_page": None}
227
369
 
228
370
  # The same model through ANOTHER provider is fine to add, but worth knowing.
229
371
  by_model: dict[str, list[str]] = {}
@@ -234,12 +376,14 @@ def build() -> list[dict]:
234
376
  seen = _read_seen()
235
377
  today = dt.date.today().isoformat()
236
378
  baseline = not seen
379
+ first_list = {c for c in listed if f"list|{c}" not in seen} # a connection's first list is its baseline
237
380
  walled = {p for p, on in sources.free_in_own_client_only().items() if on}
238
381
  out = []
239
382
  for key, r in rows.items():
240
383
  k = "|".join(key)
241
384
  if k not in seen:
242
- seen[k] = "baseline" if baseline else today
385
+ seen[k] = ("baseline" if baseline or (r["sources"] == ["provider"] and r["connection"] in first_list)
386
+ else today)
243
387
  r["first_seen"] = seen[k]
244
388
  r["new"] = seen[k] not in ("baseline",) and seen[k] >= (dt.date.today() - dt.timedelta(days=NEW_DAYS)).isoformat()
245
389
  if r["free"] and r["provider"] in walled:
@@ -257,6 +401,7 @@ def build() -> list[dict]:
257
401
  "connected" if r["routes"] else
258
402
  "easy to add" if r["openai_like"] else "other")
259
403
  out.append(r)
404
+ seen.update({f"list|{c}": today for c in first_list})
260
405
  _write_seen(seen)
261
406
  order = {"connected": 0, "easy to add": 1, "on bench": 2, "other": 3}
262
407
  # Within each reach, FreeLLMAPI's smartest first; the models it does not rank
@@ -343,6 +488,38 @@ def _selftest() -> None:
343
488
  assert big["page"] == "https://freellmapi.co/model/big-one" and big["quirks"] == [
344
489
  {"title": "T", "body": "B", "severity": "info"}] and big["vision"], big
345
490
  assert [r["model"] for r in rows] == ["acme/tiny", "acme/big", "acme/small"], [r["model"] for r in rows]
491
+ # A connection is found by its API host or by the provider's id in its host; the bench's name wins.
492
+ hosts = {"xk": "api.xkiro.com", "api.xkiro.com": "api.xkiro.com", "router.requesty.ai": "router.requesty.ai",
493
+ "cli-proxy-api": "203.0.113.7"}
494
+ assert _conn_for(["xkiro"], None, hosts) == "xk"
495
+ assert _conn_for(["requesty"], "https://router.requesty.ai/v1", hosts) == "router.requesty.ai"
496
+ assert _conn_for(["com"], None, hosts) is None and _conn_for(["acme"], "https://acme.example/v1", hosts) is None
497
+ # A provider's own list: chat models only, price where it says one, unknown where it does not.
498
+ assert _slim({"id": "a", "architecture": {"output_modalities": ["image"]}}) is None
499
+ assert _slim({"id": "b", "supported_endpoint_types": ["aihorde"]}) is None
500
+ assert _slim({"id": "c", "capabilities": {"completion_chat": False}}) is None
501
+ assert _slim({"id": "d:free"})["free"] is True and _slim({"id": "e"})["free"] is None
502
+ assert _slim({"id": "f", "pricing": {"prompt": "0", "completion": "0"}})["free"] is True
503
+ assert _slim({"id": "g", "input_price": 2.5e-07, "output_price": 1e-06})["free"] is False
504
+ assert _slim({"id": "h", "access_tier": "premium", "capabilities": {"vision": True}, "context_length": 9})[
505
+ "free"] is False and _slim({"id": "h", "capabilities": {"vision": True}})["vision"] is True
506
+ # With a list cached: acme's connection is found by its host, the list's new model is a row of its
507
+ # own (the first list of a connection is a baseline, so not new), and models.dev links each model.
508
+ feeds["provider-lists"] = {"connections": [{"names": ["acme-conn"], "base": "https://acme.example/v1",
509
+ "models": [_slim({"id": "acme/big"}), _slim({"id": "acme/fresh"})]}]}
510
+ import endpoints
511
+ try:
512
+ sources.cached = lambda name: feeds.get(name, {})
513
+ globals().update(_on_bench=lambda: [], SEEN=pathlib.Path(tempfile.mkdtemp()) / "seen.json")
514
+ endpoints_hub, endpoints.find_hub = endpoints.find_hub, lambda which: "owui-ep" if which == "openwebui" else None
515
+ rows = {r["model"]: r for r in build() if r["provider"] == "acme"}
516
+ finally:
517
+ sources.cached, endpoints.find_hub = kept[0], endpoints_hub
518
+ globals().update(_on_bench=kept[1], SEEN=kept[2])
519
+ assert set(rows) == {"acme/big", "acme/small", "acme/tiny", "acme/fresh"}, sorted(rows)
520
+ assert rows["acme/big"]["connection"] == "acme-conn" and rows["acme/big"]["reach"] == "connected", rows["acme/big"]
521
+ assert rows["acme/fresh"]["sources"] == ["provider"] and not rows["acme/fresh"]["new"], rows["acme/fresh"]
522
+ assert rows["acme/big"]["md_page"] == "https://models.dev/providers/acme", rows["acme/big"]["md_page"]
346
523
  print("discover selftest ok")
347
524
 
348
525
 
@@ -351,6 +528,9 @@ def main() -> None:
351
528
  if "--selftest" in sys.argv:
352
529
  _selftest()
353
530
  return
531
+ if "--refresh" in sys.argv:
532
+ for line in refresh(force=True) or ["every list fetched"]:
533
+ print(" ", line)
354
534
  rows = build()
355
535
  free = [r for r in rows if r["free"]]
356
536
  by = {}
package/metrics/owui.py CHANGED
@@ -114,6 +114,32 @@ def connections() -> dict[str, tuple[int, str]]:
114
114
  return out
115
115
 
116
116
 
117
+ def lists() -> tuple[list[dict], dict[str, str]]:
118
+ """What each connection's provider serves now, by its own /models, which
119
+ Open WebUI passes through at /openai/models/<index> whatever the
120
+ connection's Model IDs list allows: [{"names", "base", "models"}], names
121
+ as connections() has them (the bench's first), and {name: why} for the
122
+ connections that would not say. Switched-off connections are left out."""
123
+ names: dict[int, list[str]] = collections.defaultdict(list)
124
+ for name, (idx, _tag) in connections().items():
125
+ names[idx].append(name)
126
+ cfg = _call("/openai/config")
127
+ bases = cfg.get("OPENAI_API_BASE_URLS") or []
128
+ out, failed = [], {}
129
+ for idx, ns in sorted(names.items()):
130
+ if idx >= len(bases) or (cfg.get("OPENAI_API_CONFIGS") or {}).get(str(idx), {}).get("enable") is False:
131
+ continue
132
+ try:
133
+ r = _call(f"/openai/models/{idx}")
134
+ except Exception as exc: # noqa: BLE001
135
+ failed[ns[0]] = str(exc)
136
+ continue
137
+ data = r.get("data") if isinstance(r, dict) else r
138
+ out.append({"names": ns, "base": bases[idx],
139
+ "models": [m for m in data or [] if isinstance(m, dict) and m.get("id")]})
140
+ return out, failed
141
+
142
+
117
143
  def plan(connection: str, model_id: str) -> dict:
118
144
  """What adding would change. Raises ValueError when it cannot be done here."""
119
145
  release, note = version()
package/metrics/route.py CHANGED
@@ -273,7 +273,7 @@ def suite_recall() -> dict[str, tuple[int, int, int]]:
273
273
  def panel(shape: str | None = None, large: bool = True,
274
274
  nominated: list[str] | None = None,
275
275
  members: list[str] | None = None, explorers: int = 2,
276
- avoid: set[str] | None = None) -> dict:
276
+ avoid: set[str] | None = None, stand_ins: int = 0) -> dict:
277
277
  """`large=True` means the task is a big one — most of ours are.
278
278
 
279
279
  `members` is a bench-gateway COLLECTION: models the caller always runs. With
@@ -282,7 +282,8 @@ def panel(shape: str | None = None, large: bool = True,
282
282
  never with a member or a member's family. `avoid` keeps models out of
283
283
  those automatic picks (the gateway passes the ones on a retiring alert, so
284
284
  a seat is not spent on a model already known to be failing); a nomination
285
- still seats one.
285
+ still seats one. `stand_ins` more are picked the same way, for members out
286
+ of quota (the gateway); fewer come back if the pool runs out.
286
287
 
287
288
  `nominated` names models the operator wants on the panel today, and they
288
289
  take EXPLORER seats — never the primary or the second.
@@ -469,6 +470,12 @@ def panel(shape: str | None = None, large: bool = True,
469
470
 
470
471
  explorer, explorer2 = seats[0], seats[1]
471
472
 
473
+ standing_in = []
474
+ while len(standing_in) < stand_ins and (pick := stalest()):
475
+ chosen.add(pick["model"])
476
+ seated_families.add(pick["family"])
477
+ standing_in.append(pick)
478
+
472
479
  # RESERVE: the fallback when the panel meets a rate limit.
473
480
  #
474
481
  # This replaces the hard rule that seats must sit on different
@@ -489,7 +496,7 @@ def panel(shape: str | None = None, large: bool = True,
489
496
  key=lambda r: (r["found"], paid(r)), default=None)
490
497
 
491
498
  return {"primary": primary, "second": second, "explorer": explorer,
492
- "explorer2": explorer2, "reserve": reserve,
499
+ "explorer2": explorer2, "reserve": reserve, "stand_ins": standing_in,
493
500
  "considered": len(rows),
494
501
  "untried": sum(1 for r in rows if not r["offloads"]),
495
502
  "contenders": len(contenders),
@@ -93,17 +93,30 @@ FREELLMAPI_TO_MODELSDEV = {
93
93
  def fetch(name: str, force: bool = False) -> dict:
94
94
  """A feed, from the cache when it is under a day old."""
95
95
  path = CACHE / f"{name}.json"
96
- if not force and path.exists() and time.time() - path.stat().st_mtime < MAX_AGE_S:
96
+ age = age_s(name)
97
+ if not force and age is not None and age < MAX_AGE_S:
97
98
  return json.loads(path.read_text(encoding="utf-8"))
98
99
  req = urllib.request.Request(FEEDS[name], headers={"User-Agent": "benchwarmers"})
99
100
  with urllib.request.urlopen(req, timeout=90) as r:
100
101
  data = (json.load(r) if not FEEDS[name].endswith(".xml") else
101
102
  {"slugs": sorted(set(re.findall(r"/model/([^<\"/]+)<", r.read().decode("utf-8"))))})
103
+ store(name, data)
104
+ return data
105
+
106
+
107
+ def store(name: str, data: dict) -> None:
108
+ """Write one cached list whole, never half."""
102
109
  CACHE.mkdir(parents=True, exist_ok=True)
110
+ path = CACHE / f"{name}.json"
103
111
  tmp = path.with_suffix(".part")
104
112
  tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
105
113
  tmp.replace(path)
106
- return data
114
+
115
+
116
+ def age_s(name: str) -> float | None:
117
+ """Seconds since a cached list was written; None if it never was."""
118
+ path = CACHE / f"{name}.json"
119
+ return time.time() - path.stat().st_mtime if path.exists() else None
107
120
 
108
121
 
109
122
  def cached(name: str) -> dict:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendellresearch-ai/benchwarmers",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
5
5
  "mcpName": "io.github.WendellResearch-AI/benchwarmers",
6
6
  "license": "MIT",
@@ -274,7 +274,8 @@ def gateway_routes() -> None:
274
274
  "merge": {"members": ["m1", "m2"], "reply": "merged", "merger": "mg", "description": "x"},
275
275
  "broken": {"members": ["m1", "boom"], "description": "x"},
276
276
  "small": {"members": ["tiny"], "description": "x"},
277
- "other": {"members": ["m2"], "description": "x"}})
277
+ "other": {"members": ["m2"], "description": "x"},
278
+ "dear": {"members": ["m1"], "description": "held back", "ad_hoc": True}})
278
279
  import gateway
279
280
  import guard
280
281
  import serve
@@ -493,6 +494,65 @@ def gateway_routes() -> None:
493
494
  one = json.loads(ok("/v1beta/models/bench/core")[0])
494
495
  assert one["name"] == "models/bench/core" and "generateContent" in one["supportedGenerationMethods"]
495
496
  assert call("/v1beta/models/bench/nope")[0] == 404
497
+ # Ad hoc: on no list and in no refusal, so nobody picks it; named, it runs and can be looked up.
498
+ for listing in (ok("/v1/models")[0], ok("/api/tags")[0], ok("/v1beta/models")[0]):
499
+ assert "bench/dear" not in listing and "bench/core" in listing, listing
500
+ assert "bench/dear" not in call("/v1/chat/completions", {"model": "bench/nope", "messages": [
501
+ {"role": "user", "content": user}]})[2]
502
+ assert ok("/api/show", {"model": "bench/dear"}) and ok("/v1beta/models/bench/dear")
503
+ asked = json.loads(ok("/v1/chat/completions", {"model": "bench/dear", "messages": [
504
+ {"role": "user", "content": user}]})[0])
505
+ assert asked["model"] == "bench/dear" and [s["alias"] for s in asked["bench"]["seats"]] == ["m1"], asked
506
+ seats.asked.clear()
507
+ made = call("/api/collection", {"name": "dear", "members": ["m1"], "ad_hoc": True}, on=dash.server_port)
508
+ assert json.loads(made[2])["ok"] and serve.bench_collections.load()["dear"]["ad_hoc"] is True, made
509
+ made = call("/api/collection", {"name": "dear", "members": ["m1"]}, on=dash.server_port)
510
+ assert json.loads(made[2])["ok"] and "bench/dear" in ok("/v1/models")[0], made # unticked: on offer again
511
+ serve.delete_collection("dear")
512
+
513
+ # A COLLECTION'S OWN LIMITS: above the gateway's 3 and 0 here, checked, undone by null.
514
+ def save(name, members, **own):
515
+ made = call("/api/collection", {"name": name, "members": members, "shape": "general", **own}, on=dash.server_port)
516
+ return json.loads(made[2])
517
+ def ask_it(name):
518
+ return json.loads(ok("/v1/chat/completions", {"model": f"bench/{name}", "messages": [
519
+ {"role": "user", "content": "x"}], "stream": False})[0])
520
+ for bad in ({"max_calls": 11}, {"max_calls": 0}, {"max_explorers": 3}, {"max_calls": "4"}, {"max_explorers": True}):
521
+ assert not save("own", ["m1"], **bad)["ok"], bad
522
+ assert not save("own", ["m1", "m2", "mg"], max_calls=2)["ok"] # its own cap, not the gateway's
523
+ assert save("wide", ["m1", "m2", "mg", "boom"], max_calls=4)["ok"] # above the gateway's
524
+ assert "wide" in serve.bench_collections.load() and not serve.save_gateway_settings(1, 0)["errors"][0].count("wide")
525
+ assert save("own", ["m1", "m2"], max_calls=4, max_explorers=1)["ok"]
526
+ listed_own = [m for m in json.loads(ok("/v1/models")[0])["data"] if m["id"] == "bench/own"][0]
527
+ assert listed_own["explorers"] == 1 and listed_own["max_calls"] == 4, listed_own
528
+ # (boom held throughout: the stub raises for it, and an explorer seat could pick it.)
529
+ real_refused = gateway.refused_now
530
+ gateway.refused_now = lambda aliases, *_: {a: "out of quota" for a in aliases if a == "boom"}
531
+ assert len(ask_it("own")["bench"]["seats"]) == 3 # two members, its own explorer
532
+ # STAND-INS: a held member stays on the collection, named, with a seat in its place.
533
+ gateway.refused_now = lambda aliases, *_: {a: "out of quota" for a in aliases if a in ("m1", "boom")}
534
+ b = ask_it("own")["bench"]
535
+ by_seat = {s["seat"]: s["alias"] for s in b["seats"]}
536
+ assert [k["alias"] for k in b["skipped"]] == ["m1"] and "m1" not in by_seat.values(), b
537
+ assert len(b["seats"]) == 3 and "stand-in for m1" in by_seat and by_seat["stand-in for m1"] != "m2", b
538
+ assert "m1" in serve.bench_collections.load()["own"]["members"]
539
+ # Max explorers 0: only the members, here and when the gateway's own 0 applies.
540
+ assert save("own", ["m1", "m2"], max_calls=4, max_explorers=0)["ok"]
541
+ assert [s["alias"] for s in ask_it("own")["bench"]["seats"]] == ["m2"]
542
+ assert save("own", ["m1", "m2"], max_calls=None, max_explorers=None)["ok"]
543
+ assert "max_calls" not in serve.bench_collections.load()["own"]
544
+ assert [s["alias"] for s in ask_it("own")["bench"]["seats"]] == ["m2"]
545
+ gateway.refused_now = lambda aliases, *_: {a: "out of quota" for a in aliases}
546
+ assert call("/v1/chat/completions", {"model": "bench/own", "messages": [{"role": "user", "content": "x"}]})[0] == 429
547
+ gateway.refused_now = real_refused
548
+ # A connection the alerts say has stopped answering counts as held.
549
+ import alerts
550
+ real_active, alerts.active = alerts.active, lambda: {"c": {"kind": "connection", "message": "not answering", "aliases": ["m1"]}}
551
+ assert gateway.refused_now(["m1", "m2"], 10, 1) == {"m1": "not answering"}
552
+ alerts.active = real_active
553
+ serve.delete_collection("own")
554
+ serve.delete_collection("wide")
555
+ seats.asked.clear()
496
556
  # Token counts are estimates, without a model call; an image counts IMAGE_TOKENS.
497
557
  counted = {"model": "bench/core", "messages": [
498
558
  {"role": "user", "content": [{"type": "text", "text": "x" * 400},
@@ -514,6 +514,25 @@ def main() -> int:
514
514
  print(("ok " if ok else "FAIL ") + "provider chip names" + ("" if ok else f" -> {got}"))
515
515
  failures += 0 if ok else 1
516
516
 
517
+ # An answer clears every alert it disproves (2026-11-02: big-pickle answered a
518
+ # Retry and stayed listed, its unrecognised-failure alert open), never a price;
519
+ # a held connection loses the model, and goes with its last one.
520
+ alerts.raise_alert("model:unclassified:t/served", "registration", "t fails oddly")
521
+ alerts.raise_alert("model:pending:t/served", "retiring", "t retires", alias="t")
522
+ alerts.raise_alert("model:paid:t/served", "went paid", "t has a price", alias="t")
523
+ alerts.raise_alert("model:pending:t/served-too", "retiring", "another retires", alias="t2")
524
+ alerts.raise_alert("connection:held:x", "connection", "all fail", aliases=["t", "t2"])
525
+ alerts.raise_alert("connection:held:y", "connection", "all fail", aliases=["t"])
526
+ cleared = alerts.answered("t", "t/served")
527
+ left = alerts.active()
528
+ ok = (set(cleared) == {"model:unclassified:t/served", "model:pending:t/served", "connection:held:y"}
529
+ and "model:paid:t/served" in left and "model:pending:t/served-too" in left
530
+ and left["connection:held:x"]["aliases"] == ["t2"])
531
+ for k in ("model:paid:t/served", "model:pending:t/served-too", "connection:held:x"):
532
+ alerts.clear(k)
533
+ print(("ok " if ok else "FAIL ") + "an answer clears what it disproves, not a price" + ("" if ok else f" -> {cleared}"))
534
+ failures += 0 if ok else 1
535
+
517
536
  # Got it clears a notice (NVIDIA changed its figures), never a state still wrong.
518
537
  alerts.raise_alert("test:notice", "limits", "a notice", dismiss=True)
519
538
  alerts.raise_alert("test:state", "limits", "a state")