@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,189 @@
1
+ """The bench CLI (cli.py) against the real dashboard handler. No real files.
2
+
3
+ python scripts/test_cli.py
4
+
5
+ Read commands, write commands on a temp collections file and key store,
6
+ asking a collection (the panel stubbed, as in test_auth), a prompt built from
7
+ a file, the exit codes, and the admin token: the CLI sends it as a bearer,
8
+ the dashboard takes it and nothing else, and a gateway key never opens a
9
+ dashboard route.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import contextlib
15
+ import io
16
+ import json
17
+ import os
18
+ import pathlib
19
+ import sys
20
+ import tempfile
21
+ import threading
22
+ from http.server import ThreadingHTTPServer
23
+
24
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
25
+ sys.path.insert(0, str(ROOT))
26
+ import serve # noqa: E402
27
+ import gateway # noqa: E402
28
+ import guard # noqa: E402
29
+ import cli # noqa: E402
30
+
31
+ TOKEN = "test-admin-token-0123456789"
32
+
33
+
34
+ def bench(*argv: str, stdin: str = "") -> tuple[int, str, str]:
35
+ """(exit code, stdout, stderr) of one `bench` command, in process."""
36
+ out, err = io.StringIO(), io.StringIO()
37
+ real_in = sys.stdin
38
+ sys.stdin = io.StringIO(stdin)
39
+ try:
40
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
41
+ try:
42
+ code = cli.main(list(argv))
43
+ except SystemExit as e: # argparse's usage errors
44
+ code = e.code if isinstance(e.code, int) else 2
45
+ finally:
46
+ sys.stdin = real_in
47
+ return code, out.getvalue(), err.getvalue()
48
+
49
+
50
+ def ok(*argv: str, stdin: str = "") -> str:
51
+ code, out, err = bench(*argv, stdin=stdin)
52
+ assert code == 0, (argv, code, out, err)
53
+ return out
54
+
55
+
56
+ def main() -> int:
57
+ server = ThreadingHTTPServer(("127.0.0.1", 0), serve.Handler)
58
+ threading.Thread(target=server.serve_forever, daemon=True).start()
59
+ port = server.server_port
60
+ env_keys = ("BENCHWARMERS_ADMIN_TOKEN", "BENCHWARMERS_REQUIRE_KEYS", "BENCHWARMERS_KEY", "BENCHWARMERS_URL")
61
+ saved_env = {k: os.environ.pop(k, None) for k in env_keys}
62
+ saved = (guard.KEYS, serve.CHANGES_PATH, serve.PORT, serve.bench_collections.FILE, gateway.run_collection)
63
+ asked, real_env = [], cli.env
64
+ with tempfile.TemporaryDirectory() as tmp:
65
+ t = pathlib.Path(tmp)
66
+ try:
67
+ guard.KEYS, serve.CHANGES_PATH, serve.PORT = t / "gateway-keys.json", t / "changes.jsonl", port
68
+ serve.bench_collections.FILE = t / "collections.json"
69
+ serve.bench_collections.FILE.write_text(json.dumps({"core": {"members": [], "description": "x"}}),
70
+ encoding="utf-8")
71
+ gateway.run_collection = lambda name, messages, nominated, reply, who: (
72
+ asked.append((name, messages, nominated, reply)) or "## member — m\nthe answer", {"seats": []})
73
+ os.environ["BENCHWARMERS_URL"] = f"http://127.0.0.1:{port}/v1" # a /v1 on the end is fine
74
+
75
+ # Reading.
76
+ assert "Benchwarmers" in ok("status")
77
+ status = json.loads(ok("status", "--json"))
78
+ assert status["gateway"].endswith(f":{port}/v1") and status["collections"] == 1, status
79
+ assert set(status["scheduler_jobs"]) and "auth" in status, status
80
+ assert "core" in ok("collections")
81
+ json.loads(ok("models", "--json"))
82
+ json.loads(ok("endpoints", "--json"))
83
+ assert "no gateway keys" in ok("keys")
84
+
85
+ # Collections: save what is given, keep the rest, add and remove one at a time.
86
+ # Members must be chat models on the bench: three from this install's (or the fixtures').
87
+ chat = [m["alias"] for m in json.loads(ok("models", "--json"))["models"] if m["kind"] == "chat"]
88
+ assert len(chat) >= 3, chat
89
+ a, b, c = chat[:3]
90
+ ok("collection", "save", "bench/pair", "--members", f"{a},{b}", "--description", "two",
91
+ "--tags", "fast")
92
+ show = json.loads(ok("collection", "show", "pair", "--json"))
93
+ assert show["members"] == [a, b] and show["description"] == "two", show
94
+ data = json.loads(serve.bench_collections.FILE.read_text(encoding="utf-8"))
95
+ assert data["pair"]["members"] == [a, b] and data["pair"]["tags"] == ["fast"], data
96
+ ok("collection", "save", "pair", "--add", c, "--remove", a)
97
+ data = json.loads(serve.bench_collections.FILE.read_text(encoding="utf-8"))
98
+ assert data["pair"]["members"] == [b, c] and data["pair"]["description"] == "two", data
99
+ # Deleting asks; with no terminal to ask in, only --yes does it.
100
+ code, _, err = bench("collection", "rm", "pair")
101
+ assert code == 2 and "--yes" in err, (code, err)
102
+ assert "pair" in json.loads(serve.bench_collections.FILE.read_text(encoding="utf-8"))
103
+ ok("collection", "rm", "pair", "--yes")
104
+ assert "pair" not in json.loads(serve.bench_collections.FILE.read_text(encoding="utf-8"))
105
+ code, _, err = bench("collection", "show", "pair")
106
+ assert code == 1 and "no collection" in err, (code, err)
107
+
108
+ # Gateway keys: printed once, listed by name and hint only, revoked.
109
+ code, out, err = bench("key", "create", "cli-test", "--collections", "core", "--rpm", "3")
110
+ key = out.strip()
111
+ assert code == 0 and key.startswith(guard.PREFIX) and "shown this once" in err, (code, out, err)
112
+ listed = ok("keys", "--json")
113
+ assert "cli-test" in listed and key not in listed and "sha256" not in listed, listed
114
+ assert bench("key", "revoke", "cli-test")[0] == 2 # asks first
115
+ ok("key", "revoke", "cli-test", "-y")
116
+
117
+ # --json and --url before the command or after it; --url over BENCHWARMERS_URL.
118
+ assert json.loads(ok("--json", "status"))["ok"] and json.loads(ok("status", "--json"))["ok"]
119
+ url = os.environ["BENCHWARMERS_URL"]
120
+ os.environ["BENCHWARMERS_URL"] = "http://127.0.0.1:9" # nothing there
121
+ assert bench("status")[0] == 3
122
+ for argv in (("--url", url, "status", "--json"), ("status", "--url", url, "--json")):
123
+ assert json.loads(ok(*argv))["gateway"].endswith(f":{port}/v1"), argv
124
+ os.environ["BENCHWARMERS_URL"] = url
125
+
126
+ # A route this server lacks (an older server): said so, GET (the page) or POST (a 404).
127
+ for method in ("GET", "POST"):
128
+ try:
129
+ cli.call(method, "/api/not-yet", {} if method == "POST" else None)
130
+ raise AssertionError(f"{method} of a missing route passed")
131
+ except cli.Failed as e:
132
+ assert "older than this bench" in str(e), (method, str(e))
133
+
134
+ # Asking: the prompt from words, a file and stdin; the options on the wire.
135
+ f = t / "diff.txt"
136
+ f.write_text("- old\n+ new\n", encoding="utf-8")
137
+ out = ok("ask", "core", "review", "this", "-f", str(f), "--merged", "--with", "x1")
138
+ assert "the answer" in out, out
139
+ name, messages, nominated, reply = asked[-1]
140
+ assert name == "core" and reply == "merged" and nominated == ["x1"], asked[-1]
141
+ assert "review this" in messages[-1]["content"] and "+ new" in messages[-1]["content"], messages
142
+ ok("ask", "bench/core", "-", "--system", "be brief", stdin="from stdin")
143
+ assert asked[-1][1][0] == {"role": "system", "content": "be brief"}, asked[-1]
144
+ assert asked[-1][1][-1]["content"] == "from stdin", asked[-1]
145
+ code, _, err = bench("ask", "core")
146
+ assert code == 2 and "nothing to ask" in err, (code, err)
147
+ code, _, err = bench("verdict", "nope.member1", "--useful")
148
+ assert code == 1 and "nope.member1" in err, (code, err)
149
+
150
+ # The admin token: the server has one; what the CLI sends decides. The
151
+ # server reads os.environ, the CLI its own settings (cli.env), here apart.
152
+ os.environ["BENCHWARMERS_ADMIN_TOKEN"] = TOKEN
153
+ gw_key = guard.make_key("agent")
154
+ mine = {"BENCHWARMERS_URL": os.environ["BENCHWARMERS_URL"]}
155
+ cli.env = lambda name: mine.get(name, "")
156
+ code, _, err = bench("status") # none sent
157
+ assert code == 1 and "BENCHWARMERS_ADMIN_TOKEN" in err, (code, err)
158
+ mine["BENCHWARMERS_ADMIN_TOKEN"] = gw_key # a gateway key is not the admin token
159
+ code, _, err = bench("status")
160
+ assert code == 1 and "not the admin token" in err, (code, err)
161
+ mine["BENCHWARMERS_ADMIN_TOKEN"] = TOKEN
162
+ assert "Benchwarmers" in ok("status")
163
+ assert "admin token set" in ok("status")
164
+ code, _, err = bench("collections") # the gateway's routes take a key
165
+ assert code == 1 and "BENCHWARMERS_KEY" in err, (code, err)
166
+ mine["BENCHWARMERS_KEY"] = gw_key
167
+ assert "core" in ok("collections")
168
+ cli.env = real_env
169
+ del os.environ["BENCHWARMERS_ADMIN_TOKEN"]
170
+
171
+ # No server: exit 3, saying where it looked.
172
+ server.shutdown()
173
+ server.server_close()
174
+ code, _, err = bench("status")
175
+ assert code == 3 and "not running" in err and str(port) in err, (code, err)
176
+ finally:
177
+ (guard.KEYS, serve.CHANGES_PATH, serve.PORT, serve.bench_collections.FILE,
178
+ gateway.run_collection) = saved
179
+ cli.env = real_env
180
+ for k, v in saved_env.items():
181
+ os.environ.pop(k, None)
182
+ if v is not None:
183
+ os.environ[k] = v
184
+ print("ok")
185
+ return 0
186
+
187
+
188
+ if __name__ == "__main__":
189
+ sys.exit(main())