@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
@@ -1,255 +1,255 @@
1
- """Login, gateway keys and one port, through the real handlers. No real files.
2
-
3
- python scripts/test_auth.py
4
-
5
- metrics/guard.py's self-check covers the pieces (the cookie, key hashing,
6
- the startup refusal); this covers what the servers do with them, route by
7
- route: the dashboard behind the login cookie, the gateway's routes behind a
8
- gateway key on both ports, the open routes (health, login, Connect's
9
- callback), no auth at all without an admin token, the access log, and both
10
- servers refusing to listen beyond this machine without a token. Keys, the
11
- change log and collections point at a temp folder.
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import io
17
- import json
18
- import os
19
- import pathlib
20
- import subprocess
21
- import sys
22
- import tempfile
23
- import threading
24
- import urllib.error
25
- import urllib.request
26
- from http.server import ThreadingHTTPServer
27
-
28
- ROOT = pathlib.Path(__file__).resolve().parent.parent
29
- sys.path.insert(0, str(ROOT))
30
- import serve # noqa: E402
31
- import gateway # noqa: E402
32
- import guard # noqa: E402
33
-
34
- TOKEN = "test-admin-token-0123456789"
35
-
36
-
37
- class NoRedirect(urllib.request.HTTPRedirectHandler):
38
- def redirect_request(self, *args, **kwargs):
39
- return None
40
-
41
-
42
- OPENER = urllib.request.build_opener(NoRedirect)
43
-
44
-
45
- def serve_on(handler) -> ThreadingHTTPServer:
46
- server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
47
- threading.Thread(target=server.serve_forever, daemon=True).start()
48
- return server
49
-
50
-
51
- def call(port: int, path: str, body: dict | None = None, method: str | None = None,
52
- **headers) -> tuple[int, dict, str]:
53
- """(status, headers, body text) for one request; HTTP errors are answers too."""
54
- headers = {"Host": f"127.0.0.1:{port}", **headers}
55
- data = None
56
- if body is not None:
57
- data, headers["Content-Type"] = json.dumps(body).encode(), "application/json"
58
- req = urllib.request.Request(f"http://127.0.0.1:{port}{path}", data, headers, method=method)
59
- try:
60
- with OPENER.open(req, timeout=10) as r:
61
- return r.status, dict(r.headers), r.read().decode()
62
- except urllib.error.HTTPError as e:
63
- return e.code, dict(e.headers), e.read().decode()
64
-
65
-
66
- def refuses_to_start(script: str) -> str:
67
- """The startup message when `script` is told to listen beyond this machine without a token."""
68
- env = {k: v for k, v in os.environ.items() if k != "BENCHWARMERS_ADMIN_TOKEN"}
69
- env.update(BENCH_BIND="0.0.0.0", BENCH_PORT="8199", BENCH_GATEWAY_PORT="8198", PYTHONIOENCODING="utf-8")
70
- done = subprocess.run([sys.executable, script], cwd=ROOT, env=env, capture_output=True, text=True,
71
- encoding="utf-8", errors="replace", timeout=120)
72
- assert done.returncode != 0, f"{script} started on 0.0.0.0 without a token"
73
- return done.stderr
74
-
75
-
76
- def main() -> int:
77
- for script in ("serve.py", "gateway.py"):
78
- assert "BENCHWARMERS_ADMIN_TOKEN" in refuses_to_start(script), script
79
-
80
- dash, gw = serve_on(serve.Handler), serve_on(gateway.Handler)
81
- d, g = dash.server_port, gw.server_port
82
- saved = (guard.KEYS, serve.CHANGES_PATH, serve.PORT, gateway.bench_collections.FILE,
83
- os.environ.pop("BENCHWARMERS_ADMIN_TOKEN", None), sys.stderr, gateway.run_collection,
84
- os.environ.pop("BENCHWARMERS_REQUIRE_KEYS", None), os.environ.pop("BENCH_ALLOWED_ORIGINS", None))
85
- with tempfile.TemporaryDirectory() as tmp:
86
- t = pathlib.Path(tmp)
87
- try:
88
- guard.KEYS, serve.CHANGES_PATH, serve.PORT = t / "gateway-keys.json", t / "changes.jsonl", d
89
- gateway.bench_collections.FILE = t / "collections.json"
90
- gateway.bench_collections.FILE.write_text(json.dumps({"core": {"members": [], "description": "x"},
91
- "vision": {"members": [], "description": "x"}}),
92
- encoding="utf-8")
93
- # The panel is test_merged's and test_vision's; here only who asked, and whether it ran.
94
- ran = []
95
- gateway.run_collection = lambda name, messages, nominated, reply, who: (
96
- ran.append((name, who)) or "ok", {"seats": []})
97
-
98
- # NO TOKEN: everything as before -- no login, no key, any bearer ignored.
99
- assert call(d, "/api/stamp")[0] == 200
100
- assert call(d, "/login")[:2][0] == 303
101
- for port in (d, g):
102
- status, _, text = call(port, "/v1/models", Authorization="Bearer sk-anything")
103
- assert status == 200 and "bench/core" in text, (port, status, text)
104
-
105
- # TOKEN SET: the dashboard asks for the login cookie...
106
- os.environ["BENCHWARMERS_ADMIN_TOKEN"] = TOKEN
107
- status, headers, _ = call(d, "/")
108
- assert status == 303 and headers["Location"] == "/login", (status, headers)
109
- assert call(d, "/api/stamp")[0] == 401
110
- assert call(d, "/api/theme", {"theme": "nope"})[0] == 401 # writes too
111
- # Not even which routes exist: the Host, then the login, then the 404.
112
- assert call(d, "/api/nope", {}, Host="evil.example")[0] == 403
113
- assert call(d, "/api/nope", {})[0] == 401
114
- assert call(d, "/fonts/chivo-latin-400-normal.woff2")[0] == 303
115
- status, _, page = call(d, "/login")
116
- assert status == 200 and "Admin token" in page
117
- # ...which only the admin token gets, through the same guard as any write.
118
- assert call(d, "/login", {"token": "wrong"})[0] == 401
119
- assert call(d, "/login", {"token": TOKEN}, Origin="http://evil.example")[0] == 403
120
- status, headers, _ = call(d, "/login", {"token": TOKEN})
121
- cookie = headers.get("Set-Cookie", "")
122
- assert status == 200 and "HttpOnly" in cookie and "SameSite=Strict" in cookie, headers
123
- assert TOKEN not in cookie
124
- jar = {"Cookie": cookie.split(";", 1)[0]}
125
- assert call(d, "/api/stamp", **jar)[0] == 200
126
- assert call(d, "/api/theme", {"theme": "nope"}, **jar)[0] == 400 # in, then the theme rule
127
- assert call(d, "/api/nope", {}, **jar)[0] == 404
128
-
129
- # Open without either: health (any Host), and Connect's callback.
130
- assert call(d, "/health", Host="platform.internal:9")[:3:2] == (200, '{"ok": true}')
131
- status, _, page = call(d, "/connect/callback?state=nope&code=x")
132
- assert status == 200 and "unknown or has expired" in page, page
133
- # ...but not a path merely beginning like it.
134
- assert call(d, "/connect/callbackx")[0] == 303
135
-
136
- # Gateway keys, made from the dashboard and shown once.
137
- assert call(d, "/api/gateway-key", {"name": "chat-ui"})[0] == 401
138
- status, _, text = call(d, "/api/gateway-key", {"name": "chat-ui"}, **jar)
139
- key = json.loads(text)["key"]
140
- assert status == 200 and key not in guard.KEYS.read_text(encoding="utf-8")
141
- assert key not in serve.CHANGES_PATH.read_text(encoding="utf-8")
142
- assert key not in serve.settings_page() and "chat-ui" in serve.settings_page()
143
- bearer = {"Authorization": f"Bearer {key}"}
144
-
145
- # The gateway's routes, on both ports: the key, never the cookie.
146
- for port in (d, g):
147
- status, headers, _ = call(port, "/v1/models")
148
- assert status == 401 and headers["WWW-Authenticate"].startswith("Bearer"), (port, headers)
149
- assert call(port, "/v1/models", **jar)[0] == 401, port
150
- assert call(port, "/v1/models", Authorization="Bearer sk-dummy")[0] == 401, port
151
- status, _, text = call(port, "/v1/models", **bearer)
152
- assert status == 200 and json.loads(text)["data"][0]["id"] == "bench/core", (port, text)
153
- # One port: /bench/verdict reaches the gateway's own validation.
154
- status, _, text = call(port, "/bench/verdict", {"call": "x"}, **bearer)
155
- assert status == 400 and "useful" in json.loads(text)["error"]["message"], (port, text)
156
- assert call(port, "/v1/models", Host="evil.example", **bearer)[0] == 403, port # Host still checked
157
- # A key never opens the dashboard.
158
- assert call(d, "/api/stamp", **bearer)[0] == 401
159
- assert call(d, "/", **bearer)[0] == 303
160
-
161
- # The access log: route, status, who; never a key, a query or a callback's code.
162
- log = io.StringIO()
163
- sys.stderr = log
164
- call(d, "/v1/models", **bearer)
165
- call(d, "/api/stamp", **jar)
166
- call(d, "/connect/callback?state=nope&code=secret-code")
167
- call(d, "/connect/callback/sid-in-path?code=secret-code")
168
- sys.stderr = saved[5]
169
- lines = log.getvalue()
170
- assert "GET /v1/models 200 chat-ui" in lines and "GET /api/stamp 200 admin" in lines, lines
171
- assert key not in lines and "secret-code" not in lines and "sid-in-path" not in lines, lines
172
-
173
- # A key limited to collections and a rate, made on Settings: it sees and calls only
174
- # those, its name rides on the call, and past its cap it waits.
175
- status, _, text = call(d, "/api/gateway-key", {"name": "estate", "collections": ["vision"], "rpm": "1"},
176
- **jar)
177
- assert status == 200, text
178
- estate = {"Authorization": f"Bearer {json.loads(text)['key']}"}
179
- assert call(d, "/api/gateway-key", {"name": "other", "collections": ["nope"]}, **jar)[0] == 400
180
- assert call(d, "/api/gateway-key", {"name": "other", "rpm": "lots"}, **jar)[0] == 400
181
- status, _, text = call(g, "/v1/models", **estate)
182
- assert [m["id"] for m in json.loads(text)["data"]] == ["bench/vision", "bench/vision:merged"], text
183
- ask = {"messages": [{"role": "user", "content": "hi"}]}
184
- status, _, text = call(g, "/v1/chat/completions", {"model": "bench/core", **ask}, **estate)
185
- assert status == 403 and "may call bench/vision" in text, text
186
- assert call(g, "/v1/chat/completions", {"model": "bench/vision", **ask}, **estate)[0] == 200
187
- assert call(g, "/v1/chat/completions", {"model": "bench/core", **ask}, **bearer)[0] == 200
188
- assert ran == [("vision", "estate"), ("core", "chat-ui")], ran
189
- status, headers, _ = call(g, "/v1/chat/completions", {"model": "bench/vision", **ask}, **estate)
190
- assert status == 429 and int(headers["Retry-After"]) >= 1, (status, headers)
191
- page = serve.settings_page()
192
- assert "estate" in page and "bwk_…" in page and ">vision<" in page, page
193
-
194
- # CORS for a browser app on an allowed origin, on the gateway's routes only.
195
- os.environ["BENCH_ALLOWED_ORIGINS"] = "https://estate.example"
196
- app, evil = {"Origin": "https://estate.example"}, {"Origin": "https://evil.example"}
197
- asks = {"Access-Control-Request-Method": "POST",
198
- "Access-Control-Request-Headers": "authorization, content-type, x-stainless-os"}
199
- for port in (d, g):
200
- status, headers, _ = call(port, "/v1/chat/completions", method="OPTIONS", **app, **asks)
201
- assert status == 204 and headers["Access-Control-Allow-Origin"] == "https://estate.example", headers
202
- assert "Authorization" in headers["Access-Control-Allow-Headers"] and headers["Vary"] == "Origin"
203
- assert "x-stainless-os" in headers["Access-Control-Allow-Headers"] # the OpenAI JS SDK's
204
- assert "Access-Control-Allow-Credentials" not in headers
205
- status, headers, _ = call(port, "/v1/models", method="OPTIONS", **app,
206
- **{"Access-Control-Request-Headers": "x-ok; not a name"})
207
- assert status == 204 and headers["Access-Control-Allow-Headers"] == "Authorization, Content-Type"
208
- status, headers, _ = call(port, "/v1/chat/completions", method="OPTIONS", **evil, **asks)
209
- assert status == 403 and "Access-Control-Allow-Origin" not in headers, (port, headers)
210
- status, headers, _ = call(port, "/v1/models", **bearer, **app)
211
- assert status == 200 and headers["Access-Control-Allow-Origin"] == "https://estate.example"
212
- assert "Access-Control-Allow-Origin" not in call(port, "/v1/models", **bearer, **evil)[1]
213
- # The app reads a refusal too; cross-site is fine for it, for nobody else.
214
- assert call(port, "/v1/models", **app)[1]["Access-Control-Allow-Origin"] == "https://estate.example"
215
- status, _, text = call(port, "/bench/verdict", {"call": "x"}, **bearer, **app,
216
- **{"Sec-Fetch-Site": "cross-site"})
217
- assert status == 400 and "useful" in text, (port, text)
218
- assert call(port, "/bench/verdict", {"call": "x"}, **bearer, **evil,
219
- **{"Sec-Fetch-Site": "cross-site"})[0] == 403
220
- # The dashboard: no preflight, no CORS, same-origin as before.
221
- assert call(d, "/api/stamp", method="OPTIONS", **app, **asks)[0] == 501
222
- status, headers, _ = call(d, "/api/theme", {"theme": "nope"}, **jar, **app, **{"Sec-Fetch-Site": "cross-site"})
223
- assert status == 403 and "Access-Control-Allow-Origin" not in headers, headers
224
- assert "Access-Control-Allow-Origin" not in call(d, "/api/stamp", **jar, **app)[1]
225
- del os.environ["BENCH_ALLOWED_ORIGINS"]
226
-
227
- # Revoked: refused at once.
228
- assert call(d, "/api/gateway-key/remove", {"name": "chat-ui"}, **jar)[0] == 200
229
- assert call(d, "/v1/models", **bearer)[0] == 401
230
- assert call(d, "/api/gateway-key/remove", {"name": "chat-ui"}, **jar)[0] == 400
231
-
232
- # No admin token, keys required on this machine: the gateway asks, the dashboard does not.
233
- del os.environ["BENCHWARMERS_ADMIN_TOKEN"]
234
- os.environ["BENCHWARMERS_REQUIRE_KEYS"] = "1"
235
- for port in (d, g):
236
- assert call(port, "/v1/models", Authorization="Bearer sk-anything")[0] == 401, port
237
- assert call(port, "/v1/models", **estate)[0] == 200, port
238
- assert call(d, "/api/stamp")[0] == 200
239
- finally:
240
- (guard.KEYS, serve.CHANGES_PATH, serve.PORT, gateway.bench_collections.FILE) = saved[:4]
241
- gateway.run_collection = saved[6]
242
- for name, value in (("BENCHWARMERS_ADMIN_TOKEN", saved[4]), ("BENCHWARMERS_REQUIRE_KEYS", saved[7]),
243
- ("BENCH_ALLOWED_ORIGINS", saved[8])):
244
- os.environ.pop(name, None)
245
- if value is not None:
246
- os.environ[name] = value
247
- sys.stderr = saved[5]
248
- dash.shutdown()
249
- gw.shutdown()
250
- print("ok")
251
- return 0
252
-
253
-
254
- if __name__ == "__main__":
255
- sys.exit(main())
1
+ """Login, gateway keys and one port, through the real handlers. No real files.
2
+
3
+ python scripts/test_auth.py
4
+
5
+ metrics/guard.py's self-check covers the pieces (the cookie, key hashing,
6
+ the startup refusal); this covers what the servers do with them, route by
7
+ route: the dashboard behind the login cookie, the gateway's routes behind a
8
+ gateway key on both ports, the open routes (health, login, Connect's
9
+ callback), no auth at all without an admin token, the access log, and both
10
+ servers refusing to listen beyond this machine without a token. Keys, the
11
+ change log and collections point at a temp folder.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import io
17
+ import json
18
+ import os
19
+ import pathlib
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import threading
24
+ import urllib.error
25
+ import urllib.request
26
+ from http.server import ThreadingHTTPServer
27
+
28
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
29
+ sys.path.insert(0, str(ROOT))
30
+ import serve # noqa: E402
31
+ import gateway # noqa: E402
32
+ import guard # noqa: E402
33
+
34
+ TOKEN = "test-admin-token-0123456789"
35
+
36
+
37
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
38
+ def redirect_request(self, *args, **kwargs):
39
+ return None
40
+
41
+
42
+ OPENER = urllib.request.build_opener(NoRedirect)
43
+
44
+
45
+ def serve_on(handler) -> ThreadingHTTPServer:
46
+ server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
47
+ threading.Thread(target=server.serve_forever, daemon=True).start()
48
+ return server
49
+
50
+
51
+ def call(port: int, path: str, body: dict | None = None, method: str | None = None,
52
+ **headers) -> tuple[int, dict, str]:
53
+ """(status, headers, body text) for one request; HTTP errors are answers too."""
54
+ headers = {"Host": f"127.0.0.1:{port}", **headers}
55
+ data = None
56
+ if body is not None:
57
+ data, headers["Content-Type"] = json.dumps(body).encode(), "application/json"
58
+ req = urllib.request.Request(f"http://127.0.0.1:{port}{path}", data, headers, method=method)
59
+ try:
60
+ with OPENER.open(req, timeout=10) as r:
61
+ return r.status, dict(r.headers), r.read().decode()
62
+ except urllib.error.HTTPError as e:
63
+ return e.code, dict(e.headers), e.read().decode()
64
+
65
+
66
+ def refuses_to_start(script: str) -> str:
67
+ """The startup message when `script` is told to listen beyond this machine without a token."""
68
+ env = {k: v for k, v in os.environ.items() if k != "BENCHWARMERS_ADMIN_TOKEN"}
69
+ env.update(BENCH_BIND="0.0.0.0", BENCH_PORT="8199", BENCH_GATEWAY_PORT="8198", PYTHONIOENCODING="utf-8")
70
+ done = subprocess.run([sys.executable, script], cwd=ROOT, env=env, capture_output=True, text=True,
71
+ encoding="utf-8", errors="replace", timeout=120)
72
+ assert done.returncode != 0, f"{script} started on 0.0.0.0 without a token"
73
+ return done.stderr
74
+
75
+
76
+ def main() -> int:
77
+ for script in ("serve.py", "gateway.py"):
78
+ assert "BENCHWARMERS_ADMIN_TOKEN" in refuses_to_start(script), script
79
+
80
+ dash, gw = serve_on(serve.Handler), serve_on(gateway.Handler)
81
+ d, g = dash.server_port, gw.server_port
82
+ saved = (guard.KEYS, serve.CHANGES_PATH, serve.PORT, gateway.bench_collections.FILE,
83
+ os.environ.pop("BENCHWARMERS_ADMIN_TOKEN", None), sys.stderr, gateway.run_collection,
84
+ os.environ.pop("BENCHWARMERS_REQUIRE_KEYS", None), os.environ.pop("BENCH_ALLOWED_ORIGINS", None))
85
+ with tempfile.TemporaryDirectory() as tmp:
86
+ t = pathlib.Path(tmp)
87
+ try:
88
+ guard.KEYS, serve.CHANGES_PATH, serve.PORT = t / "gateway-keys.json", t / "changes.jsonl", d
89
+ gateway.bench_collections.FILE = t / "collections.json"
90
+ gateway.bench_collections.FILE.write_text(json.dumps({"core": {"members": [], "description": "x"},
91
+ "vision": {"members": [], "description": "x"}}),
92
+ encoding="utf-8")
93
+ # The panel is test_merged's and test_vision's; here only who asked, and whether it ran.
94
+ ran = []
95
+ gateway.run_collection = lambda name, messages, nominated, reply, who: (
96
+ ran.append((name, who)) or "ok", {"seats": []})
97
+
98
+ # NO TOKEN: everything as before -- no login, no key, any bearer ignored.
99
+ assert call(d, "/api/stamp")[0] == 200
100
+ assert call(d, "/login")[:2][0] == 303
101
+ for port in (d, g):
102
+ status, _, text = call(port, "/v1/models", Authorization="Bearer sk-anything")
103
+ assert status == 200 and "bench/core" in text, (port, status, text)
104
+
105
+ # TOKEN SET: the dashboard asks for the login cookie...
106
+ os.environ["BENCHWARMERS_ADMIN_TOKEN"] = TOKEN
107
+ status, headers, _ = call(d, "/")
108
+ assert status == 303 and headers["Location"] == "/login", (status, headers)
109
+ assert call(d, "/api/stamp")[0] == 401
110
+ assert call(d, "/api/theme", {"theme": "nope"})[0] == 401 # writes too
111
+ # Not even which routes exist: the Host, then the login, then the 404.
112
+ assert call(d, "/api/nope", {}, Host="evil.example")[0] == 403
113
+ assert call(d, "/api/nope", {})[0] == 401
114
+ assert call(d, "/fonts/chivo-latin-400-normal.woff2")[0] == 303
115
+ status, _, page = call(d, "/login")
116
+ assert status == 200 and "Admin token" in page
117
+ # ...which only the admin token gets, through the same guard as any write.
118
+ assert call(d, "/login", {"token": "wrong"})[0] == 401
119
+ assert call(d, "/login", {"token": TOKEN}, Origin="http://evil.example")[0] == 403
120
+ status, headers, _ = call(d, "/login", {"token": TOKEN})
121
+ cookie = headers.get("Set-Cookie", "")
122
+ assert status == 200 and "HttpOnly" in cookie and "SameSite=Strict" in cookie, headers
123
+ assert TOKEN not in cookie
124
+ jar = {"Cookie": cookie.split(";", 1)[0]}
125
+ assert call(d, "/api/stamp", **jar)[0] == 200
126
+ assert call(d, "/api/theme", {"theme": "nope"}, **jar)[0] == 400 # in, then the theme rule
127
+ assert call(d, "/api/nope", {}, **jar)[0] == 404
128
+
129
+ # Open without either: health (any Host), and Connect's callback.
130
+ assert call(d, "/health", Host="platform.internal:9")[:3:2] == (200, '{"ok": true}')
131
+ status, _, page = call(d, "/connect/callback?state=nope&code=x")
132
+ assert status == 200 and "unknown or has expired" in page, page
133
+ # ...but not a path merely beginning like it.
134
+ assert call(d, "/connect/callbackx")[0] == 303
135
+
136
+ # Gateway keys, made from the dashboard and shown once.
137
+ assert call(d, "/api/gateway-key", {"name": "chat-ui"})[0] == 401
138
+ status, _, text = call(d, "/api/gateway-key", {"name": "chat-ui"}, **jar)
139
+ key = json.loads(text)["key"]
140
+ assert status == 200 and key not in guard.KEYS.read_text(encoding="utf-8")
141
+ assert key not in serve.CHANGES_PATH.read_text(encoding="utf-8")
142
+ assert key not in serve.settings_page() and "chat-ui" in serve.settings_page()
143
+ bearer = {"Authorization": f"Bearer {key}"}
144
+
145
+ # The gateway's routes, on both ports: the key, never the cookie.
146
+ for port in (d, g):
147
+ status, headers, _ = call(port, "/v1/models")
148
+ assert status == 401 and headers["WWW-Authenticate"].startswith("Bearer"), (port, headers)
149
+ assert call(port, "/v1/models", **jar)[0] == 401, port
150
+ assert call(port, "/v1/models", Authorization="Bearer sk-dummy")[0] == 401, port
151
+ status, _, text = call(port, "/v1/models", **bearer)
152
+ assert status == 200 and json.loads(text)["data"][0]["id"] == "bench/core", (port, text)
153
+ # One port: /bench/verdict reaches the gateway's own validation.
154
+ status, _, text = call(port, "/bench/verdict", {"call": "x"}, **bearer)
155
+ assert status == 400 and "useful" in json.loads(text)["error"]["message"], (port, text)
156
+ assert call(port, "/v1/models", Host="evil.example", **bearer)[0] == 403, port # Host still checked
157
+ # A key never opens the dashboard.
158
+ assert call(d, "/api/stamp", **bearer)[0] == 401
159
+ assert call(d, "/", **bearer)[0] == 303
160
+
161
+ # The access log: route, status, who; never a key, a query or a callback's code.
162
+ log = io.StringIO()
163
+ sys.stderr = log
164
+ call(d, "/v1/models", **bearer)
165
+ call(d, "/api/stamp", **jar)
166
+ call(d, "/connect/callback?state=nope&code=secret-code")
167
+ call(d, "/connect/callback/sid-in-path?code=secret-code")
168
+ sys.stderr = saved[5]
169
+ lines = log.getvalue()
170
+ assert "GET /v1/models 200 chat-ui" in lines and "GET /api/stamp 200 admin" in lines, lines
171
+ assert key not in lines and "secret-code" not in lines and "sid-in-path" not in lines, lines
172
+
173
+ # A key limited to collections and a rate, made on Settings: it sees and calls only
174
+ # those, its name rides on the call, and past its cap it waits.
175
+ status, _, text = call(d, "/api/gateway-key", {"name": "estate", "collections": ["vision"], "rpm": "1"},
176
+ **jar)
177
+ assert status == 200, text
178
+ estate = {"Authorization": f"Bearer {json.loads(text)['key']}"}
179
+ assert call(d, "/api/gateway-key", {"name": "other", "collections": ["nope"]}, **jar)[0] == 400
180
+ assert call(d, "/api/gateway-key", {"name": "other", "rpm": "lots"}, **jar)[0] == 400
181
+ status, _, text = call(g, "/v1/models", **estate)
182
+ assert [m["id"] for m in json.loads(text)["data"]] == ["bench/vision", "bench/vision:merged"], text
183
+ ask = {"messages": [{"role": "user", "content": "hi"}]}
184
+ status, _, text = call(g, "/v1/chat/completions", {"model": "bench/core", **ask}, **estate)
185
+ assert status == 403 and "may call bench/vision" in text, text
186
+ assert call(g, "/v1/chat/completions", {"model": "bench/vision", **ask}, **estate)[0] == 200
187
+ assert call(g, "/v1/chat/completions", {"model": "bench/core", **ask}, **bearer)[0] == 200
188
+ assert ran == [("vision", "estate"), ("core", "chat-ui")], ran
189
+ status, headers, _ = call(g, "/v1/chat/completions", {"model": "bench/vision", **ask}, **estate)
190
+ assert status == 429 and int(headers["Retry-After"]) >= 1, (status, headers)
191
+ page = serve.settings_page()
192
+ assert "estate" in page and "bwk_…" in page and ">vision<" in page, page
193
+
194
+ # CORS for a browser app on an allowed origin, on the gateway's routes only.
195
+ os.environ["BENCH_ALLOWED_ORIGINS"] = "https://estate.example"
196
+ app, evil = {"Origin": "https://estate.example"}, {"Origin": "https://evil.example"}
197
+ asks = {"Access-Control-Request-Method": "POST",
198
+ "Access-Control-Request-Headers": "authorization, content-type, x-stainless-os"}
199
+ for port in (d, g):
200
+ status, headers, _ = call(port, "/v1/chat/completions", method="OPTIONS", **app, **asks)
201
+ assert status == 204 and headers["Access-Control-Allow-Origin"] == "https://estate.example", headers
202
+ assert "Authorization" in headers["Access-Control-Allow-Headers"] and headers["Vary"] == "Origin"
203
+ assert "x-stainless-os" in headers["Access-Control-Allow-Headers"] # the OpenAI JS SDK's
204
+ assert "Access-Control-Allow-Credentials" not in headers
205
+ status, headers, _ = call(port, "/v1/models", method="OPTIONS", **app,
206
+ **{"Access-Control-Request-Headers": "x-ok; not a name"})
207
+ assert status == 204 and headers["Access-Control-Allow-Headers"] == "Authorization, Content-Type"
208
+ status, headers, _ = call(port, "/v1/chat/completions", method="OPTIONS", **evil, **asks)
209
+ assert status == 403 and "Access-Control-Allow-Origin" not in headers, (port, headers)
210
+ status, headers, _ = call(port, "/v1/models", **bearer, **app)
211
+ assert status == 200 and headers["Access-Control-Allow-Origin"] == "https://estate.example"
212
+ assert "Access-Control-Allow-Origin" not in call(port, "/v1/models", **bearer, **evil)[1]
213
+ # The app reads a refusal too; cross-site is fine for it, for nobody else.
214
+ assert call(port, "/v1/models", **app)[1]["Access-Control-Allow-Origin"] == "https://estate.example"
215
+ status, _, text = call(port, "/bench/verdict", {"call": "x"}, **bearer, **app,
216
+ **{"Sec-Fetch-Site": "cross-site"})
217
+ assert status == 400 and "useful" in text, (port, text)
218
+ assert call(port, "/bench/verdict", {"call": "x"}, **bearer, **evil,
219
+ **{"Sec-Fetch-Site": "cross-site"})[0] == 403
220
+ # The dashboard: no preflight, no CORS, same-origin as before.
221
+ assert call(d, "/api/stamp", method="OPTIONS", **app, **asks)[0] == 501
222
+ status, headers, _ = call(d, "/api/theme", {"theme": "nope"}, **jar, **app, **{"Sec-Fetch-Site": "cross-site"})
223
+ assert status == 403 and "Access-Control-Allow-Origin" not in headers, headers
224
+ assert "Access-Control-Allow-Origin" not in call(d, "/api/stamp", **jar, **app)[1]
225
+ del os.environ["BENCH_ALLOWED_ORIGINS"]
226
+
227
+ # Revoked: refused at once.
228
+ assert call(d, "/api/gateway-key/remove", {"name": "chat-ui"}, **jar)[0] == 200
229
+ assert call(d, "/v1/models", **bearer)[0] == 401
230
+ assert call(d, "/api/gateway-key/remove", {"name": "chat-ui"}, **jar)[0] == 400
231
+
232
+ # No admin token, keys required on this machine: the gateway asks, the dashboard does not.
233
+ del os.environ["BENCHWARMERS_ADMIN_TOKEN"]
234
+ os.environ["BENCHWARMERS_REQUIRE_KEYS"] = "1"
235
+ for port in (d, g):
236
+ assert call(port, "/v1/models", Authorization="Bearer sk-anything")[0] == 401, port
237
+ assert call(port, "/v1/models", **estate)[0] == 200, port
238
+ assert call(d, "/api/stamp")[0] == 200
239
+ finally:
240
+ (guard.KEYS, serve.CHANGES_PATH, serve.PORT, gateway.bench_collections.FILE) = saved[:4]
241
+ gateway.run_collection = saved[6]
242
+ for name, value in (("BENCHWARMERS_ADMIN_TOKEN", saved[4]), ("BENCHWARMERS_REQUIRE_KEYS", saved[7]),
243
+ ("BENCH_ALLOWED_ORIGINS", saved[8])):
244
+ os.environ.pop(name, None)
245
+ if value is not None:
246
+ os.environ[name] = value
247
+ sys.stderr = saved[5]
248
+ dash.shutdown()
249
+ gw.shutdown()
250
+ print("ok")
251
+ return 0
252
+
253
+
254
+ if __name__ == "__main__":
255
+ sys.exit(main())