@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/scripts/test_hubs.py
CHANGED
|
@@ -278,16 +278,21 @@ def discover_add() -> None:
|
|
|
278
278
|
os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "mgmt-test"
|
|
279
279
|
|
|
280
280
|
onboarded = []
|
|
281
|
-
serve.queue_onboard = lambda items, who="ui": onboarded.append(
|
|
281
|
+
serve.queue_onboard = lambda items, who="ui": onboarded.append(
|
|
282
|
+
(items[0]["served"], items[0]["endpoint"], items[0]["mode"], items[0]["vision"])) or {
|
|
282
283
|
"ok": True, "errors": [], "items": []}
|
|
283
284
|
owui.add = lambda c, m: (m, [f"owui added {m} on {c}"])
|
|
284
285
|
r = serve.discover_add("groq", "vendor/new", endpoint="lite")
|
|
285
|
-
assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite"), r
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite", "full", False), r
|
|
287
|
+
# The confirm's Onboard as and Vision box reach the queue; a bad mode is refused before the hub has it.
|
|
288
|
+
r = serve.discover_add("openrouter", "vendor/y:free", endpoint="cpa", mode="quick-adhoc", vision=True)
|
|
289
|
+
assert r["ok"] and onboarded[-1] == ("vendor/y:free", "cpa", "quick-adhoc", True), r
|
|
288
290
|
assert [m for m, p, _b in SEEN if "management" in p and m == "PATCH"] == ["PATCH"]
|
|
291
|
+
before = len(SEEN)
|
|
292
|
+
assert "mode must be" in serve.discover_add("openrouter", "vendor/q", endpoint="cpa", mode="slow")["errors"][0]
|
|
293
|
+
assert len(SEEN) == before, SEEN[before:]
|
|
289
294
|
r = serve.discover_add("groq-api", "vendor/z") # no endpoint: Open WebUI, as before
|
|
290
|
-
assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui"), r
|
|
295
|
+
assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui", "full", False), r
|
|
291
296
|
assert serve.discover_add("groq-api", "vendor/z", endpoint="owui")["ok"]
|
|
292
297
|
assert "not a hub" in serve.discover_add("groq", "v/z", endpoint="plain")["errors"][0]
|
|
293
298
|
changes = [json.loads(line)["field"] for line in serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()]
|
package/scripts/test_no_owui.py
CHANGED
|
@@ -132,6 +132,15 @@ def stranger(fake: str) -> None:
|
|
|
132
132
|
assert c["ok"] and not c["errors"], c
|
|
133
133
|
assert sorted(x["served"] for x in c["candidates"]) == ["vendor/c", "vendor/d", "vendor/e", "vendor/f"], c
|
|
134
134
|
assert {x["group"] for x in c["candidates"]} == {"openrouter (direct)"} and not any(x["new"] for x in c["candidates"])
|
|
135
|
+
# Queued or onboarding now: left out until the queue is done with it.
|
|
136
|
+
kept_items = serve._OQ["items"]
|
|
137
|
+
serve._OQ["items"] = [{"served": "vendor/c", "endpoint": "openrouter", "state": "running"},
|
|
138
|
+
{"served": "vendor/d", "endpoint": "openrouter", "state": "done"}]
|
|
139
|
+
try:
|
|
140
|
+
busy = sorted(x["served"] for x in serve.onboard_candidates()["candidates"])
|
|
141
|
+
finally:
|
|
142
|
+
serve._OQ["items"] = kept_items
|
|
143
|
+
assert busy == ["vendor/d", "vendor/e", "vendor/f"], busy
|
|
135
144
|
|
|
136
145
|
# The served check: the registered models only, not the aggregator's catalogue.
|
|
137
146
|
assert check_deprecations.served() == {"vendor/a:free", "vendor/b:free"}
|
|
@@ -241,12 +250,15 @@ def owner(fake: str) -> None:
|
|
|
241
250
|
OWUI_MODELS.append({"id": "just-added", "urlIdx": 1})
|
|
242
251
|
real_names = provider_tags.connection_names
|
|
243
252
|
try:
|
|
244
|
-
|
|
253
|
+
# The picker's list is kept a few minutes (serve.LISTED_FOR); Refresh asks again.
|
|
254
|
+
kept = serve.onboard_candidates()
|
|
255
|
+
assert "just-added" not in {x["served"] for x in kept["candidates"]}, kept
|
|
256
|
+
live = {x["served"]: x["group"] for x in serve.onboard_candidates(fresh=True)["candidates"]}
|
|
245
257
|
|
|
246
258
|
def refused(hub):
|
|
247
259
|
raise OSError("401")
|
|
248
260
|
provider_tags.connection_names = refused
|
|
249
|
-
c = serve.onboard_candidates()
|
|
261
|
+
c = serve.onboard_candidates(fresh=True)
|
|
250
262
|
finally:
|
|
251
263
|
provider_tags.connection_names = real_names
|
|
252
264
|
OWUI_MODELS.pop()
|
package/scripts/test_quota.py
CHANGED
|
@@ -290,6 +290,16 @@ def main() -> int:
|
|
|
290
290
|
check("rate limit (not sent" in r["error"] and "a calendar month" in r["error"] and "this month" in r["error"],
|
|
291
291
|
"rpmo 3: the fourth call this month is refused", r["error"])
|
|
292
292
|
check(client.chat("t-ok", "hi")["text"] != "", "another connection is not held by that cap")
|
|
293
|
+
# Seating asks the same question first (gateway.refused_now): held past any
|
|
294
|
+
# seat's wait, so not seated; a model with room is.
|
|
295
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
296
|
+
import gateway
|
|
297
|
+
import pacing
|
|
298
|
+
h = pacing.held("t-month", "monthprov")
|
|
299
|
+
check(h is not None and h[0] > gateway.SEAT_TIMEOUT and "this month" in h[1], "held() sees the monthly cap", h)
|
|
300
|
+
check(pacing.held("t-ok", gateway.route.connection_of("t-ok")) is None, "held() is None with room")
|
|
301
|
+
out = gateway.refused_now(["t-month", "t-ok"], 10, gateway.SEAT_TIMEOUT)
|
|
302
|
+
check(list(out) == ["t-month"] and "rate limit" in out["t-month"], "refused_now names only the held model", out)
|
|
293
303
|
|
|
294
304
|
# The free-tier guard: a model that went paid is held until allowed.
|
|
295
305
|
before = len(seen.get("paid", []))
|
|
@@ -322,8 +332,8 @@ def main() -> int:
|
|
|
322
332
|
and "rpmo" not in ((serve.catalog.overrides().get("t-ok") or {}).get("limits") or {}),
|
|
323
333
|
"a bad rpmo is refused; a model's own limits have none", bad)
|
|
324
334
|
page = serve.quotas_page()
|
|
325
|
-
check("
|
|
326
|
-
"the Quotas page
|
|
335
|
+
check("id='quota-data'" in page and "fakeprov" in page and "What the bench sent" in page,
|
|
336
|
+
"the Quotas page carries its tiles and the ledger's counts")
|
|
327
337
|
|
|
328
338
|
srv.shutdown()
|
|
329
339
|
print("\nthe ledger, quotas and pacing hold" if not failures else f"\n{failures} FAILED")
|
|
@@ -360,9 +360,9 @@ def main() -> int:
|
|
|
360
360
|
|
|
361
361
|
modes = {}
|
|
362
362
|
|
|
363
|
-
def fake_spawn(served, endpoint="", who="ui", mode="full"):
|
|
363
|
+
def fake_spawn(served, endpoint="", who="ui", mode="full", vision=False):
|
|
364
364
|
order.append(served)
|
|
365
|
-
modes[served] = mode
|
|
365
|
+
modes[served] = mode + (" vision" if vision else "")
|
|
366
366
|
(runs / f"onboard-{served}.log").write_text(f"onboarding {served} from e -> alias {served}-x\n"
|
|
367
367
|
+ outcome[served][1], encoding="utf-8")
|
|
368
368
|
return {"ok": True, "errors": [], "log": f"onboard-{served}.log"}, Proc(served)
|
|
@@ -397,8 +397,8 @@ def main() -> int:
|
|
|
397
397
|
settle()
|
|
398
398
|
stopped = {i["served"]: i["state"] for i in serve.onboard_queue()["items"]}
|
|
399
399
|
order.remove("quiet")
|
|
400
|
-
serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m} for s, m in
|
|
401
|
-
(("ok", "full"), ("quiet", "quick"), ("bad", "quick-adhoc"))])
|
|
400
|
+
serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m, "vision": s == "quiet"} for s, m in
|
|
401
|
+
(("ok", "full-adhoc"), ("quiet", "quick"), ("bad", "quick-adhoc"))])
|
|
402
402
|
settle()
|
|
403
403
|
batch = {i["served"]: (i["state"], i["alias"], bool(i["note"])) for i in serve.onboard_queue()["items"]}
|
|
404
404
|
cleared = serve.clear_onboard_queue()["items"]
|
|
@@ -412,10 +412,55 @@ def main() -> int:
|
|
|
412
412
|
and batch == {"ok": ("done", "ok-x", False), "quiet": ("no answer", "quiet-x", True),
|
|
413
413
|
"bad": ("failed", "bad-x", True)}
|
|
414
414
|
and cleared == [] and bad_input == (False, False, False)
|
|
415
|
-
and modes == {"a": "full", "ok": "full", "quiet": "quick", "bad": "quick-adhoc"}
|
|
415
|
+
and modes == {"a": "full", "ok": "full-adhoc", "quiet": "quick vision", "bad": "quick-adhoc"}
|
|
416
|
+
and serve.ONBOARD_MODES["full-adhoc"] == ["--ad-hoc"]) # the suite now, then ad hoc
|
|
416
417
|
print(("ok " if ok else "FAIL ") + "onboarding queue" + ("" if ok else f" -> {stopped} {order} {batch} {modes}"))
|
|
417
418
|
failures += 0 if ok else 1
|
|
418
419
|
|
|
420
|
+
# Restart: refused unless serve.py's parent started it (nothing would start it
|
|
421
|
+
# again); refused while a job runs, unless forced; then the server shuts down.
|
|
422
|
+
import scheduler
|
|
423
|
+
kept = os.environ.get("BENCH_SERVED"), serve.SERVER
|
|
424
|
+
shut = []
|
|
425
|
+
try:
|
|
426
|
+
os.environ.pop("BENCH_SERVED", None)
|
|
427
|
+
alone = serve.restart()
|
|
428
|
+
os.environ["BENCH_SERVED"] = "1"
|
|
429
|
+
serve.SERVER = type("Server", (), {"shutdown": lambda self: shut.append(1)})()
|
|
430
|
+
scheduler.RUNNING["suite"] = True
|
|
431
|
+
busy, forced = serve.restart(), serve.restart(force=True)
|
|
432
|
+
time.sleep(0.8) # the shutdown waits half a second
|
|
433
|
+
finally:
|
|
434
|
+
scheduler.RUNNING.pop("suite", None)
|
|
435
|
+
serve.SERVER = kept[1]
|
|
436
|
+
os.environ.pop("BENCH_SERVED", None) if kept[0] is None else os.environ.update(BENCH_SERVED=kept[0])
|
|
437
|
+
ok = (not alone["ok"] and busy.get("busy") and not busy["ok"] and "suite" in busy["errors"][0]
|
|
438
|
+
and forced["ok"] and forced["cut"] and shut == [1])
|
|
439
|
+
print(("ok " if ok else "FAIL ") + "restart" + ("" if ok else f" -> {alone} {busy} {forced} {shut}"))
|
|
440
|
+
failures += 0 if ok else 1
|
|
441
|
+
|
|
442
|
+
# Action needed: an alert whose models have all left the bench is not shown,
|
|
443
|
+
# before the daily check rewrites the file (2026-10-28: a kilo connection
|
|
444
|
+
# alert stayed after Retire all 4).
|
|
445
|
+
kept_data, kept_reg = serve.DATA, serve.registered
|
|
446
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
447
|
+
(pathlib.Path(tmp) / "metrics").mkdir()
|
|
448
|
+
(pathlib.Path(tmp) / "metrics" / "alerts.json").write_text(json.dumps({
|
|
449
|
+
"connection:held:gone.example": {"kind": "connection", "message": "every model on gone.example is failing",
|
|
450
|
+
"aliases": ["g1", "g2"], "first_seen": "2026-10-28"},
|
|
451
|
+
"connection:held:here.example": {"kind": "connection", "message": "every model on here.example is failing",
|
|
452
|
+
"aliases": ["h1", "g1"], "first_seen": "2026-10-28"},
|
|
453
|
+
"retiring:x:old": {"kind": "retiring", "alias": "old", "message": "old retires", "first_seen": "2026-10-28"}}),
|
|
454
|
+
encoding="utf-8")
|
|
455
|
+
serve.DATA, serve.registered = pathlib.Path(tmp), lambda: {"h1-served": "h1"}
|
|
456
|
+
try:
|
|
457
|
+
shown = serve.problems()
|
|
458
|
+
finally:
|
|
459
|
+
serve.DATA, serve.registered = kept_data, kept_reg
|
|
460
|
+
ok = "here.example" in shown and "gone.example" not in shown and "old retires" not in shown
|
|
461
|
+
print(("ok " if ok else "FAIL ") + "action needed drops alerts on retired models")
|
|
462
|
+
failures += 0 if ok else 1
|
|
463
|
+
|
|
419
464
|
# Dismiss: one picker key or many in one write, a change-log line each;
|
|
420
465
|
# undo brings back only the ones named; nothing named is refused.
|
|
421
466
|
serve.DISMISSED_PATH = cfg / "dismissed.json"
|
|
@@ -433,6 +478,14 @@ def main() -> int:
|
|
|
433
478
|
print(("ok " if ok else "FAIL ") + "dismiss in bulk, and undo" + ("" if ok else f" -> {gone} {left} {log} {refused}"))
|
|
434
479
|
failures += 0 if ok else 1
|
|
435
480
|
|
|
481
|
+
# The member picker's provider chips: a proxy's vendor, else the bare host.
|
|
482
|
+
names = {"nvidia-api": "nvidia", "api.unorouter.com": "unorouter", "router.requesty.ai": "requesty",
|
|
483
|
+
"cli-proxy-api/codex": "codex", "openrouter": "openrouter", "apihub.agnes-ai.com": "agnes-ai"}
|
|
484
|
+
got = {t: serve.provider_label(t) for t in names}
|
|
485
|
+
ok = got == names
|
|
486
|
+
print(("ok " if ok else "FAIL ") + "provider chip names" + ("" if ok else f" -> {got}"))
|
|
487
|
+
failures += 0 if ok else 1
|
|
488
|
+
|
|
436
489
|
# The owner's files are as they were. alerts.json can change under a
|
|
437
490
|
# running suite, so only what this test could do counts: the victim's
|
|
438
491
|
# alerts cleared from the real file.
|
|
@@ -11,7 +11,10 @@ repository URL spells it, since the match is case-sensitive); every version is
|
|
|
11
11
|
VERSION; package.json carries the name as mcpName (npm's proof) and the README
|
|
12
12
|
PyPI shows carries `mcp-name: <name>` (PyPI's); the identifiers are the real
|
|
13
13
|
package names; the arguments start the MCP server through launcher.py; the
|
|
14
|
-
environment variables are the ones mcp_server.py reads.
|
|
14
|
+
environment variables are the ones mcp_server.py reads. The root's mcp.json,
|
|
15
|
+
mcp_config.json (Antigravity's name for it), plugin.json, .cursor-plugin and
|
|
16
|
+
skills/ (cursor.directory, Glama and Antigravity read the repo root, not
|
|
17
|
+
plugin/) start the same server and carry the plugin's skill. Standard library
|
|
15
18
|
only: the full schema is validated by mcp-publisher in the release itself.
|
|
16
19
|
"""
|
|
17
20
|
|
|
@@ -71,7 +74,24 @@ def main() -> int:
|
|
|
71
74
|
assert set(names) == read and len(names) == len(read), f"{kind}: lists {names}, mcp_server.py reads {read}"
|
|
72
75
|
for e in p["environmentVariables"]:
|
|
73
76
|
assert e.get("isSecret", False) == (e["name"] == "BENCHWARMERS_KEY"), e
|
|
74
|
-
|
|
77
|
+
|
|
78
|
+
# The repo root as cursor.directory and Glama read it: mcp.json starts the npm
|
|
79
|
+
# package's server, and skills/ is the Claude Code plugin's skill, word for word.
|
|
80
|
+
root_mcp = json.loads((ROOT / "mcp.json").read_text(encoding="utf-8"))["mcpServers"]["benchwarmers"]
|
|
81
|
+
assert root_mcp["command"] == "npx" and root_mcp["args"][:2] == ["-y", npm["name"]], root_mcp
|
|
82
|
+
assert launcher.plan(root_mcp["args"][2:]).get("script") == "mcp_server.py", root_mcp["args"]
|
|
83
|
+
agy = json.loads((ROOT / "mcp_config.json").read_text(encoding="utf-8"))["mcpServers"]["benchwarmers"]
|
|
84
|
+
assert agy == root_mcp, f"mcp_config.json (Antigravity's) {agy} differs from mcp.json {root_mcp}"
|
|
85
|
+
assert json.loads((ROOT / "plugin.json").read_text(encoding="utf-8"))["name"] == "benchwarmers"
|
|
86
|
+
cursor = json.loads((ROOT / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
|
|
87
|
+
assert (cursor["skills"], cursor["mcpServers"]) == ("./skills/", "./mcp.json"), cursor
|
|
88
|
+
lf = lambda p: p.read_text(encoding="utf-8").replace("\r\n", "\n") # noqa: E731
|
|
89
|
+
copies = sorted(p.name for p in (ROOT / "skills").iterdir())
|
|
90
|
+
assert copies == sorted(p.name for p in (ROOT / "plugin" / "skills").iterdir()), copies
|
|
91
|
+
for skill in copies:
|
|
92
|
+
assert lf(ROOT / "skills" / skill / "SKILL.md") == lf(ROOT / "plugin" / "skills" / skill / "SKILL.md"), \
|
|
93
|
+
f"skills/{skill}/SKILL.md differs from plugin/skills/{skill}/SKILL.md: copy one over the other"
|
|
94
|
+
print(f"server.json ok: {name} {version}, npm and pypi; mcp.json, .cursor-plugin and skills/ match")
|
|
75
95
|
return 0
|
|
76
96
|
|
|
77
97
|
|