@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.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.
Files changed (103) hide show
  1. package/AGENTS.md +152 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +415 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +408 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +186 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +674 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +939 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +304 -295
  22. package/metrics/client.py +804 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +171 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/formats.py +1021 -0
  30. package/metrics/guard.py +647 -592
  31. package/metrics/learn.py +234 -221
  32. package/metrics/ledger.py +148 -0
  33. package/metrics/litellm_hub.py +206 -206
  34. package/metrics/model_roles.py +94 -84
  35. package/metrics/offload.py +166 -166
  36. package/metrics/owui.py +205 -205
  37. package/metrics/pacing.py +248 -0
  38. package/metrics/paths.py +60 -60
  39. package/metrics/quota.py +383 -0
  40. package/metrics/route.py +581 -579
  41. package/metrics/scheduler.py +275 -275
  42. package/metrics/sources.py +167 -167
  43. package/metrics/wrong.py +144 -144
  44. package/package.json +69 -65
  45. package/perf/README.md +89 -89
  46. package/perf/run.py +284 -261
  47. package/scripts/auto_retire.py +367 -367
  48. package/scripts/check.py +113 -106
  49. package/scripts/check_deprecations.py +378 -378
  50. package/scripts/check_themes.py +103 -103
  51. package/scripts/init.py +124 -124
  52. package/scripts/list_served.py +78 -78
  53. package/scripts/notice.py +78 -78
  54. package/scripts/onboard.py +410 -331
  55. package/scripts/provider_tags.py +129 -129
  56. package/scripts/reprefix.py +136 -136
  57. package/scripts/sync_config.py +105 -105
  58. package/scripts/test_auth.py +255 -255
  59. package/scripts/test_cli.py +189 -0
  60. package/scripts/test_client.py +277 -233
  61. package/scripts/test_connect.py +222 -222
  62. package/scripts/test_first_run.py +180 -180
  63. package/scripts/test_formats.py +549 -0
  64. package/scripts/test_hubs.py +328 -328
  65. package/scripts/test_launcher.py +67 -0
  66. package/scripts/test_llm_log.py +85 -81
  67. package/scripts/test_mcp.py +178 -178
  68. package/scripts/test_merged.py +215 -214
  69. package/scripts/test_no_owui.py +291 -291
  70. package/scripts/test_owui.py +90 -90
  71. package/scripts/test_quota.py +334 -0
  72. package/scripts/test_serve_edit.py +453 -438
  73. package/scripts/test_server_json.py +79 -0
  74. package/scripts/test_verdict.py +102 -98
  75. package/scripts/test_vision.py +231 -228
  76. package/serve.py +3972 -3764
  77. package/suite/mine.py +154 -154
  78. package/suite/pace.py +145 -145
  79. package/suite/run.py +1046 -1042
  80. package/suite/seed.py +421 -421
  81. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  82. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  83. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  84. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  85. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  86. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  87. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  88. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  89. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  90. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  91. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  92. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  93. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  94. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  95. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  96. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  97. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  98. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  99. package/ui/bench.css +430 -425
  100. package/ui/bench.js +1757 -1576
  101. package/ui/themes/dark.css +35 -35
  102. package/ui/themes/hardwood.css +52 -52
  103. package/ui/themes/rainforest.css +32 -32
@@ -0,0 +1,79 @@
1
+ """server.json, the MCP Registry's entry: it says what the packages really are.
2
+
3
+ python scripts/test_server_json.py
4
+
5
+ A release publishes server.json to the registry after npm and PyPI
6
+ (.github/workflows/release.yml), and the registry refuses it, after the
7
+ packages are already out, when the entry and the packages disagree. So this
8
+ checks what the registry checks, and what it cannot: the name is in the
9
+ io.github.<owner> namespace GitHub's OIDC token proves (the owner as the
10
+ repository URL spells it, since the match is case-sensitive); every version is
11
+ VERSION; package.json carries the name as mcpName (npm's proof) and the README
12
+ PyPI shows carries `mcp-name: <name>` (PyPI's); the identifiers are the real
13
+ package names; the arguments start the MCP server through launcher.py; the
14
+ environment variables are the ones mcp_server.py reads. Standard library
15
+ only: the full schema is validated by mcp-publisher in the release itself.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import pathlib
22
+ import re
23
+ import sys
24
+ import tomllib
25
+
26
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
27
+ sys.path.insert(0, str(ROOT))
28
+ import launcher # noqa: E402
29
+
30
+ SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"
31
+
32
+
33
+ def main() -> int:
34
+ server = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
35
+ npm = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
36
+ pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]
37
+ version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
38
+
39
+ assert server["$schema"] == SCHEMA, server["$schema"]
40
+ name = server["name"]
41
+ owner = re.fullmatch(r"git\+https://github\.com/([^/]+)/[^/]+?\.git", npm["repository"]["url"]).group(1)
42
+ assert name == f"io.github.{owner}/benchwarmers", f"{name}: not in io.github.{owner}, the OIDC namespace"
43
+ assert re.fullmatch(r"[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+", name) and 3 <= len(name) <= 200, name
44
+ assert 1 <= len(server["description"]) <= 100, f"description is {len(server['description'])} chars, max 100"
45
+ assert 1 <= len(server.get("title", "x")) <= 100, server["title"]
46
+ assert server["repository"] == {"url": f"https://github.com/{owner}/benchwarmers", "source": "github"}
47
+
48
+ assert npm["version"] == version, f"package.json says {npm['version']}, VERSION says {version}"
49
+ assert server["version"] == version, f"server.json says {server['version']}, VERSION says {version}"
50
+ for p in server["packages"]:
51
+ assert p["version"] == version, f"server.json's {p['registryType']} package says {p['version']}"
52
+
53
+ # Ownership: npm's package.json and PyPI's README name the server.
54
+ assert npm.get("mcpName") == name, f"package.json mcpName is {npm.get('mcpName')!r}, not {name}"
55
+ assert pyproject["readme"] == "README.md", pyproject["readme"]
56
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
57
+ assert re.search(r"mcp-name: " + re.escape(name) + r"(?=\s|<|-->|$)", readme), \
58
+ f"README.md has no 'mcp-name: {name}' followed by a space, a newline or -->"
59
+
60
+ kinds = {p["registryType"]: p for p in server["packages"]}
61
+ assert sorted(kinds) == ["npm", "pypi"] and len(kinds) == len(server["packages"]), list(kinds)
62
+ assert kinds["npm"]["identifier"] == npm["name"], kinds["npm"]["identifier"]
63
+ assert kinds["pypi"]["identifier"] == pyproject["name"], kinds["pypi"]["identifier"]
64
+
65
+ read = set(re.findall(r'environ\.get\("(\w+)"', (ROOT / "mcp_server.py").read_text(encoding="utf-8")))
66
+ for kind, p in kinds.items():
67
+ assert p["transport"] == {"type": "stdio"}, (kind, p["transport"])
68
+ args = [a["value"] for a in p["packageArguments"] if a["type"] == "positional"]
69
+ assert launcher.plan(args).get("script") == "mcp_server.py", f"{kind}: {args} does not start mcp_server.py"
70
+ names = [e["name"] for e in p["environmentVariables"]]
71
+ assert set(names) == read and len(names) == len(read), f"{kind}: lists {names}, mcp_server.py reads {read}"
72
+ for e in p["environmentVariables"]:
73
+ assert e.get("isSecret", False) == (e["name"] == "BENCHWARMERS_KEY"), e
74
+ print(f"server.json ok: {name} {version}, npm and pypi")
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main())
@@ -1,98 +1,102 @@
1
- """Verdicts and the gateway's `bench` field, against a throwaway log. No model or
2
- endpoint is called and the real offloads.jsonl and runs/gateway/ are never
3
- written. The collection comes from the data folder (in CI, tests/fixtures/data).
4
-
5
- python scripts/test_verdict.py
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import pathlib
12
- import sys
13
- import tempfile
14
- import threading
15
- import urllib.request
16
- from http.server import ThreadingHTTPServer
17
-
18
- ROOT = pathlib.Path(__file__).resolve().parent.parent
19
- sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite")]
20
- import gateway # noqa: E402
21
- import offload # noqa: E402
22
- import run # noqa: E402
23
- import serve # noqa: E402
24
-
25
- tmp = pathlib.Path(tempfile.mkdtemp(prefix="bench-verdict-"))
26
- offload.LOG = tmp / "offloads.jsonl"
27
- gateway.LOG_DIR = tmp
28
-
29
-
30
- def verdicts() -> None:
31
- offload.add("m1", "task", None, "member; ok in 3s", "pX.member1")
32
- assert not serve.record_verdict("pX.member1", None, "", [], [])["ok"] # no pick
33
- assert not serve.record_verdict("pX.member1", True, "", "x", [])["ok"] # claims not a list
34
- assert not serve.record_verdict("nope.member1", True, "", [], [])["ok"] # unknown call
35
- assert serve.record_verdict("pX.member1", True, " found it ", ["real one", " "], ["wrong one"])["ok"]
36
- e = offload.entries()[-1]
37
- assert (e["useful"], e["note"], e["real"], e["false"]) == (True, "found it", ["real one"], ["wrong one"]), e
38
- n = len(offload.LOG.read_text(encoding="utf-8").splitlines())
39
- assert serve.record_verdict("pX.member1", True, "found it", ["real one"], ["wrong one"])["ok"]
40
- assert len(offload.LOG.read_text(encoding="utf-8").splitlines()) == n, "the same verdict again wrote a line"
41
- # The dashboard form shows note and claims for editing: blank means cleared.
42
- assert serve.record_verdict("pX.member1", False, "", [], [])["ok"]
43
- e = offload.entries()[-1]
44
- assert e["useful"] is False and e["note"] == "" and "real" not in e and "false" not in e, e
45
- # The CLI: a blank note or claim list keeps the earlier one.
46
- offload.verdict("pX.member1", False, "", ["cli claim"])
47
- offload.verdict("pX.member1", True, "")
48
- assert offload.entries()[-1]["real"] == ["cli claim"], offload.entries()[-1]
49
- assert len(offload.entries()) == 1 # superseded by call id
50
-
51
-
52
- def bench_field() -> None:
53
- gateway.pick_shape = lambda text: ("general", "test")
54
- gateway.ask_seat = lambda alias, system, prompt, messages=None: {
55
- "status": "ok", "answer": f"answer from {alias}", "error": "", "seconds": 0.1}
56
- # The explorer seats ask every endpoint's /models; None is its own "cannot
57
- # be asked, do not filter", so no request leaves the machine.
58
- run._served_model_names = lambda cache=None: None
59
- srv = ThreadingHTTPServer(("127.0.0.1", 0), gateway.Handler)
60
- gateway.PORT = srv.server_address[1] # the Host check expects the port it runs on
61
- threading.Thread(target=srv.serve_forever, daemon=True).start()
62
- url = f"http://127.0.0.1:{srv.server_address[1]}/v1/chat/completions"
63
-
64
- def post(body, **headers):
65
- req = urllib.request.Request(url, json.dumps(body).encode(),
66
- {"Content-Type": "application/json", **headers})
67
- with urllib.request.urlopen(req) as r:
68
- return r.read().decode()
69
-
70
- # metrics/guard.py, end to end: a rebinding Host, a page's text/plain
71
- # form and a foreign Origin are refused before any seat is called.
72
- for headers, code in (({"Host": "evil.example"}, 403), ({"Content-Type": "text/plain"}, 415),
73
- ({"Origin": "http://evil.example"}, 403)):
74
- try:
75
- post({}, **headers)
76
- raise AssertionError(f"{headers} was served")
77
- except urllib.error.HTTPError as exc:
78
- assert exc.code == code, (headers, exc.code)
79
-
80
- name = next(iter(gateway.bench_collections.load()))
81
- msg = {"model": f"bench/{name}", "messages": [{"role": "user", "content": "say hi"}]}
82
- r = json.loads(post(msg))
83
- b = r["bench"]
84
- assert b["collection"] == f"bench/{name}" and b["seats"], b
85
- assert all(s["answer"] == f"answer from {s['alias']}" and s["call"].startswith(b["panel"] + ".")
86
- for s in b["seats"])
87
- assert all(s["call"] in r["choices"][0]["message"]["content"] for s in b["seats"])
88
- assert "prompt" not in b and "system" not in b
89
- chunks = [json.loads(line[6:]) for line in post({**msg, "stream": True}).splitlines()
90
- if line.startswith("data: {")]
91
- assert "bench" in chunks[0] and "bench" not in chunks[1], chunks
92
- srv.shutdown()
93
-
94
-
95
- if __name__ == "__main__":
96
- verdicts()
97
- bench_field()
98
- print("verdict and bench-field checks pass")
1
+ """Verdicts and the gateway's `bench` field, against a throwaway log. No model or
2
+ endpoint is called and the real offloads.jsonl and runs/gateway/ are never
3
+ written. The collection comes from the data folder (in CI, tests/fixtures/data).
4
+
5
+ python scripts/test_verdict.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import pathlib
12
+ import sys
13
+ import tempfile
14
+ import threading
15
+ import urllib.request
16
+ from http.server import ThreadingHTTPServer
17
+
18
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
19
+ sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite")]
20
+ import gateway # noqa: E402
21
+ import offload # noqa: E402
22
+ import run # noqa: E402
23
+ import serve # noqa: E402
24
+
25
+ tmp = pathlib.Path(tempfile.mkdtemp(prefix="bench-verdict-"))
26
+ offload.LOG = tmp / "offloads.jsonl"
27
+ gateway.LOG_DIR = tmp
28
+
29
+
30
+ def verdicts() -> None:
31
+ offload.add("m1", "task", None, "member; ok in 3s", "pX.member1")
32
+ assert not serve.record_verdict("pX.member1", None, "", [], [])["ok"] # no pick
33
+ assert not serve.record_verdict("pX.member1", True, "", "x", [])["ok"] # claims not a list
34
+ assert not serve.record_verdict("nope.member1", True, "", [], [])["ok"] # unknown call
35
+ assert serve.record_verdict("pX.member1", True, " found it ", ["real one", " "], ["wrong one"])["ok"]
36
+ e = offload.entries()[-1]
37
+ assert (e["useful"], e["note"], e["real"], e["false"]) == (True, "found it", ["real one"], ["wrong one"]), e
38
+ n = len(offload.LOG.read_text(encoding="utf-8").splitlines())
39
+ assert serve.record_verdict("pX.member1", True, "found it", ["real one"], ["wrong one"])["ok"]
40
+ assert len(offload.LOG.read_text(encoding="utf-8").splitlines()) == n, "the same verdict again wrote a line"
41
+ # The dashboard form shows note and claims for editing: blank means cleared.
42
+ assert serve.record_verdict("pX.member1", False, "", [], [])["ok"]
43
+ e = offload.entries()[-1]
44
+ assert e["useful"] is False and e["note"] == "" and "real" not in e and "false" not in e, e
45
+ # The CLI: a blank note or claim list keeps the earlier one.
46
+ offload.verdict("pX.member1", False, "", ["cli claim"])
47
+ offload.verdict("pX.member1", True, "")
48
+ assert offload.entries()[-1]["real"] == ["cli claim"], offload.entries()[-1]
49
+ assert len(offload.entries()) == 1 # superseded by call id
50
+
51
+
52
+ def bench_field() -> None:
53
+ gateway.pick_shape = lambda text: ("general", "test")
54
+ gateway.ask_seat = lambda alias, system, prompt, messages=None: {
55
+ "status": "ok", "answer": f"answer from {alias}", "error": "", "seconds": 0.1}
56
+ # The explorer seats ask every endpoint's /models; None is its own "cannot
57
+ # be asked, do not filter", so no request leaves the machine.
58
+ run._served_model_names = lambda cache=None: None
59
+ srv = ThreadingHTTPServer(("127.0.0.1", 0), gateway.Handler)
60
+ gateway.PORT = srv.server_address[1] # the Host check expects the port it runs on
61
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
62
+ url = f"http://127.0.0.1:{srv.server_address[1]}/v1/chat/completions"
63
+
64
+ def post(body, **headers):
65
+ req = urllib.request.Request(url, json.dumps(body).encode(),
66
+ {"Content-Type": "application/json", **headers})
67
+ with urllib.request.urlopen(req) as r:
68
+ return r.read().decode()
69
+
70
+ # metrics/guard.py, end to end: a rebinding Host, a page's text/plain
71
+ # form and a foreign Origin are refused before any seat is called.
72
+ for headers, code in (({"Host": "evil.example"}, 403), ({"Content-Type": "text/plain"}, 415),
73
+ ({"Origin": "http://evil.example"}, 403)):
74
+ try:
75
+ post({}, **headers)
76
+ raise AssertionError(f"{headers} was served")
77
+ except urllib.error.HTTPError as exc:
78
+ assert exc.code == code, (headers, exc.code)
79
+
80
+ name = next(iter(gateway.bench_collections.load()))
81
+ msg = {"model": f"bench/{name}", "messages": [{"role": "user", "content": "say hi"}]}
82
+ r = json.loads(post(msg))
83
+ b = r["bench"]
84
+ assert b["collection"] == f"bench/{name}" and b["seats"], b
85
+ assert all(s["answer"] == f"answer from {s['alias']}" and s["call"].startswith(b["panel"] + ".")
86
+ for s in b["seats"])
87
+ assert all(s["call"] in r["choices"][0]["message"]["content"] for s in b["seats"])
88
+ assert "prompt" not in b and "system" not in b
89
+ chunks = [json.loads(line[6:]) for line in post({**msg, "stream": True}).splitlines()
90
+ if line.startswith("data: {")]
91
+ # Seat by seat as each finishes; the bench rides on the last chunk, with the finish_reason.
92
+ assert "bench" in chunks[-1] and chunks[-1]["choices"][0]["finish_reason"] == "stop", chunks
93
+ assert not any("bench" in c for c in chunks[:-1]), chunks
94
+ said = "".join(c["choices"][0]["delta"].get("content", "") for c in chunks)
95
+ assert all(s["call"] in said for s in chunks[-1]["bench"]["seats"]), said
96
+ srv.shutdown()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ verdicts()
101
+ bench_field()
102
+ print("verdict and bench-field checks pass")