@wendellresearch-ai/benchwarmers 0.2.1 → 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.
@@ -0,0 +1,334 @@
1
+ """The built-in client's Phase 2b, on a fake server: streaming, the usage
2
+ ledger, quota records, pacing and the free-tier guard.
3
+
4
+ python scripts/test_quota.py
5
+
6
+ No model is called and nothing of the install's is read or written:
7
+ BENCHWARMERS_HOME and LLM_USER_PATH name temp folders before any module
8
+ reads them. Each scenario is one model name the fake server answers in its
9
+ own way; the pacing ones sit on endpoints of their own, so their counts do
10
+ not mix. Covered:
11
+
12
+ - a streamed answer: text and reasoning apart, each piece heard by on_delta,
13
+ usage from the final chunk, the first token's time; and its ledger line;
14
+ - an upstream that answers plain JSON to a stream request, and one that
15
+ refuses stream_options (asked again without);
16
+ - a reply naming another model: the call succeeds, the swap is noted;
17
+ - an empty answer cut off by its budget: tried once more with twice the budget;
18
+ - a stream that breaks off, and STOP between chunks;
19
+ - quota records from rate-limit headers, the next call waiting out the reset;
20
+ a Gemini 429 body's daily quota learned as a limit;
21
+ - an rpm limit across six threads at once and across two processes (calls in
22
+ flight count), a monthly cap (rpmo), and both refusals classed as capacity;
23
+ - the free-tier guard holding a model that went paid, until it is allowed;
24
+ - metrics/cost.py reading the ledger.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import multiprocessing
31
+ import os
32
+ import pathlib
33
+ import sys
34
+ import tempfile
35
+ import threading
36
+ import time
37
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
38
+
39
+ # A child process (the cross-process check) re-runs this module: it must find the parent's folder.
40
+ TMP = pathlib.Path(os.environ.get("BENCH_QUOTA_TMP") or tempfile.mkdtemp(prefix="bench-quota-"))
41
+ os.environ.update(BENCHWARMERS_HOME=str(TMP), LLM_USER_PATH=str(TMP / "llm"), BENCH_CLIENT="builtin",
42
+ BENCH_QUOTA_TMP=str(TMP))
43
+ os.environ.pop("BENCH_ALLOW_PAID", None)
44
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
45
+ sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
46
+
47
+ import alerts # noqa: E402
48
+ import client # noqa: E402
49
+ import cost # noqa: E402
50
+ import learn # noqa: E402
51
+ import ledger # noqa: E402
52
+ import quota # noqa: E402
53
+ import run # noqa: E402
54
+
55
+ seen: dict[str, list[dict]] = {}
56
+ lock = threading.Lock()
57
+
58
+
59
+ class Fake(BaseHTTPRequestHandler):
60
+ def log_message(self, *_):
61
+ pass
62
+
63
+ def _send(self, code: int, body: bytes, ctype: str = "application/json", extra=None):
64
+ self.send_response(code)
65
+ self.send_header("Content-Type", ctype)
66
+ self.send_header("Content-Length", str(len(body)))
67
+ for k, v in (extra or {}).items():
68
+ self.send_header(k, v)
69
+ self.end_headers()
70
+ self.wfile.write(body)
71
+
72
+ def _stream(self, model: str, deltas: list[dict], finish: str = "stop", usage=None, served=None,
73
+ extra=None, gap: float = 0.0, close_early: bool = False):
74
+ self.send_response(200)
75
+ self.send_header("Content-Type", "text/event-stream")
76
+ for k, v in (extra or {}).items():
77
+ self.send_header(k, v)
78
+ self.end_headers()
79
+ head = {"id": "c1", "object": "chat.completion.chunk", "created": 0, "model": served or model}
80
+ for d in deltas:
81
+ self.wfile.write(f"data: {json.dumps({**head, 'choices': [{'index': 0, 'delta': d}]})}\n\n".encode())
82
+ self.wfile.flush()
83
+ time.sleep(gap)
84
+ if close_early:
85
+ return # HTTP/1.0: the connection closes, no finish, no [DONE]
86
+ tail = [{**head, "choices": [{"index": 0, "delta": {}, "finish_reason": finish}]},
87
+ {**head, "choices": [], "usage": usage or {"prompt_tokens": 7, "completion_tokens": 4,
88
+ "total_tokens": 11}}]
89
+ self.wfile.write(("".join(f"data: {json.dumps(c)}\n\n" for c in tail) + "data: [DONE]\n\n").encode())
90
+
91
+ def do_POST(self): # noqa: N802
92
+ req = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
93
+ model = req["model"]
94
+ with lock:
95
+ calls = seen.setdefault(model, [])
96
+ calls.append(req)
97
+ n = len(calls)
98
+ try:
99
+ if model == "plain" or (model == "nooptions" and "stream_options" in req):
100
+ if model == "nooptions":
101
+ return self._send(400, b'{"error": {"message": "Unrecognized request argument supplied:'
102
+ b' stream_options"}}')
103
+ return self._send(200, json.dumps({"model": model, "choices": [{"message": {
104
+ "content": "plain answer"}, "finish_reason": "stop"}],
105
+ "usage": {"prompt_tokens": 2, "completion_tokens": 2}}).encode())
106
+ if model == "overcap" and (req.get("max_tokens") or 0) >= 100:
107
+ return self._send(400, b'{"error": {"message": "max_tokens is too large: 128"}}')
108
+ if model in ("budget", "overcap") and (req.get("max_tokens") or 0) < 100:
109
+ return self._stream(model, [{"reasoning_content": "thinking and thinking"}], finish="length",
110
+ usage={"prompt_tokens": 5, "completion_tokens": req.get("max_tokens")})
111
+ if model == "ratelimited" and n <= 3:
112
+ return self._send(429, b'{"error": {"message": "slow down", "code": 429}}', extra={
113
+ "Retry-After": "0.1", "x-ratelimit-limit-requests": "3",
114
+ "x-ratelimit-remaining-requests": "0", "x-ratelimit-reset-requests": "2s"})
115
+ if model == "gemini":
116
+ return self._send(429, json.dumps([{"error": {"code": 429, "status": "RESOURCE_EXHAUSTED",
117
+ "message": "Quota exceeded", "details": [
118
+ {"@type": "type.googleapis.com/google.rpc.QuotaFailure", "violations": [
119
+ {"quotaId": "GenerateRequestsPerDayPerProjectPerModel-FreeTier",
120
+ "quotaValue": "50"}]},
121
+ {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.1s"}]}}]).encode())
122
+ if model == "paced":
123
+ time.sleep(0.4) # long enough that the seats overlap
124
+ if model == "dribble":
125
+ return self._stream(model, [{"content": "x"}] * 25, gap=0.2)
126
+ if model == "breakoff":
127
+ return self._stream(model, [{"content": "half an answer"}], close_early=True)
128
+ return self._stream(model, [{"reasoning_content": "let me "}, {"reasoning_content": "see"},
129
+ {"content": "The answer "}, {"content": "is ready."}],
130
+ served="someone-else-7b" if model == "swapped" else None,
131
+ extra={"x-ratelimit-remaining-requests": "99", "x-ratelimit-limit-requests": "100",
132
+ "x-ratelimit-reset-requests": "6m0s"} if model == "ok" else None)
133
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
134
+ pass
135
+
136
+
137
+ def setup(port: int) -> None:
138
+ base = f"http://127.0.0.1:{port}"
139
+ eps = {"fakeprov": f"{base}/v1", "pacedprov": f"{base}/p/v1", "procprov": f"{base}/q/v1",
140
+ "monthprov": f"{base}/m/v1"}
141
+ (TMP / "config").mkdir(parents=True, exist_ok=True)
142
+ (TMP / "llm").mkdir(parents=True, exist_ok=True)
143
+ (TMP / "config" / "endpoints.json").write_text(json.dumps(
144
+ {n: {"base": b, "key": "fake", "kind": "provider", **({"default": True} if n == "fakeprov" else {})}
145
+ for n, b in eps.items()}), encoding="utf-8")
146
+ where = {"paced": "pacedprov", "proc": "procprov", "month": "monthprov"}
147
+ yaml = []
148
+ for m in ("ok", "plain", "nooptions", "swapped", "budget", "overcap", "ratelimited", "gemini", "dribble", "breakoff",
149
+ "paced", "proc", "month", "paid"):
150
+ yaml += [f"- model_id: t-{m}", f" model_name: {'paced' if m in ('proc', 'month') else m}",
151
+ f' api_base: "{eps[where.get(m, "fakeprov")]}"', " api_key_name: fake"]
152
+ (TMP / "config" / "extra-openai-models.yaml").write_text("\n".join(yaml) + "\n", encoding="utf-8")
153
+ (TMP / "config" / "model-options.json").write_text('{"t-budget": {"max_tokens": 64}, "t-overcap": {"max_tokens": 64}}',
154
+ encoding="utf-8")
155
+ (TMP / "config" / "connection-limits.json").write_text(json.dumps(
156
+ {"pacedprov": {"rpm": 3}, "procprov": {"rpm": 4}, "monthprov": {"rpmo": 3}}), encoding="utf-8")
157
+ (TMP / "llm" / "keys.json").write_text('{"fake": "sk-test-not-a-key"}', encoding="utf-8")
158
+
159
+
160
+ def _child(n: int) -> None:
161
+ """One process's calls, for the cross-process pacing check."""
162
+ for _ in range(n):
163
+ client.chat("t-proc", "hi", timeout=5)
164
+
165
+
166
+ def main() -> int:
167
+ srv = ThreadingHTTPServer(("127.0.0.1", 0), Fake)
168
+ srv.daemon_threads = True
169
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
170
+ setup(srv.server_address[1])
171
+ failures = 0
172
+
173
+ def check(ok: bool, what: str, detail="") -> None:
174
+ nonlocal failures
175
+ print(("ok " if ok else "FAIL ") + what + ("" if ok else f"\n {detail}"))
176
+ failures += not ok
177
+
178
+ def last(alias: str) -> dict:
179
+ return [r for r in ledger.rows() if r["alias"] == alias][-1]
180
+
181
+ # Streaming: text and reasoning apart, every piece heard, usage from the last chunk.
182
+ heard = []
183
+ r = client.chat("t-ok", "Say something.", on_delta=lambda k, t: heard.append((k, t)), purpose="test")
184
+ req = seen["ok"][-1]
185
+ check(r["text"] == "The answer is ready." and r["reasoning"] == "let me see" and r["finish"] == "stop",
186
+ "a streamed answer, its reasoning kept apart", r)
187
+ check(req.get("stream") is True and req.get("stream_options") == {"include_usage": True},
188
+ "asked for a stream with usage", req)
189
+ check(heard == [("reasoning", "let me "), ("reasoning", "see"), ("text", "The answer "), ("text", "is ready.")],
190
+ "on_delta heard each piece in order", heard)
191
+ check(r["usage"].get("completion_tokens") == 4 and r["first_token_s"] is not None and r["attempts"] == 1,
192
+ "usage from the final chunk, first token timed", r)
193
+ row = last("t-ok")
194
+ check(row["status"] == "ok" and row["tokens_in"] == 7 and row["tokens_out"] == 4 and row["attempts"] == 1
195
+ and row["connection"] == "fakeprov" and row["caller"] == "scripts/test_quota" and row["purpose"] == "test",
196
+ "a ledger line with tokens, connection and caller", row)
197
+ recs = quota.live("fakeprov", "t-ok")
198
+ check(len(recs) == 1 and recs[0]["remaining"] == 99 and recs[0]["limit"] == 100,
199
+ "the reply's rate-limit headers kept as a quota record", recs)
200
+
201
+ # Upstreams that do not stream.
202
+ r = client.chat("t-plain", "hi")
203
+ check(r["text"] == "plain answer" and not r["error"], "plain JSON to a stream request is read as before", r)
204
+ r = client.chat("t-nooptions", "hi")
205
+ again = seen["nooptions"][-1]
206
+ check(r["text"] == "The answer is ready." and r["attempts"] == 2 and "stream_options" not in again
207
+ and again.get("stream") is True, "stream_options refused: asked once more without, still streamed", r)
208
+
209
+ # A silent swap: the call succeeds and is noted.
210
+ r = client.chat("t-swapped", "hi")
211
+ swap = (learn.learned().get("t-swapped") or {}).get("served_as") or {}
212
+ check(r["text"] and r["model"] == "someone-else-7b" and swap.get("served") == "someone-else-7b"
213
+ and last("t-swapped").get("swap") is True, "a swapped model is noted, the answer kept", (r, swap))
214
+
215
+ # An empty answer that ran out of budget: once more with twice the budget.
216
+ r = client.chat("t-budget", "hi")
217
+ tried = [c.get("max_tokens") for c in seen["budget"]]
218
+ check(r["text"] == "The answer is ready." and tried == [64, 128]
219
+ and r.get("budget_retry") == {"max_tokens": [64, 128], "answered": True}
220
+ and last("t-budget")["attempts"] == 2, "an empty reply cut by its budget is retried once, larger",
221
+ (tried, r))
222
+ r = client.chat("t-overcap", "hi")
223
+ check(r["text"] == "" and not r["error"] and r["budget_retry"]["answered"] is False
224
+ and "too large" in r["budget_retry"]["failed"] and run.failure_class(client.output(r, 30)) == "empty",
225
+ "a budget retry that fails leaves the empty answer, not an error", r)
226
+
227
+ # A stream that breaks off, and STOP between chunks.
228
+ r = client.chat("t-breakoff", "hi", timeout=10)
229
+ check("broke off before it finished" in r["error"] and run.failure_class(client.output(r, 10)) == "other",
230
+ "a stream that breaks off is an error, not a short answer", r["error"])
231
+ threading.Timer(0.6, client.STOP.set).start()
232
+ t = time.monotonic()
233
+ r = client.chat("t-dribble", "hi", timeout=30)
234
+ client.STOP.clear()
235
+ check(r["timed_out"] and time.monotonic() - t < 3, f"STOP ends a stream between chunks ({time.monotonic() - t:.1f}s)", r)
236
+
237
+ # Quota records: a 429's headers, then the next call waits for the reset.
238
+ r = client.chat("t-ratelimited", "hi", timeout=20)
239
+ # Read whole: the Retry-After record (0.1 s) has passed already, so live() leaves it out.
240
+ recs = (quota.locked.read_json(quota.FILE).get("fakeprov") or {}).get("t-ratelimited") or {}
241
+ check(r["status"] == 429 and r["attempts"] == 3 and recs.get("requests/unknown", {}).get("remaining") == 0
242
+ and "requests/retry-after" in recs, "a 429's rate-limit headers kept as quota records", (r, recs))
243
+ t = time.monotonic()
244
+ r = client.chat("t-ratelimited", "hi", timeout=20)
245
+ waited = time.monotonic() - t
246
+ check(r["text"] and r["attempts"] == 1 and 1.0 <= waited < 6,
247
+ f"the next call waited out the stated reset, then answered ({waited:.1f}s)", r)
248
+ r = client.chat("t-gemini", "hi", timeout=20)
249
+ rpd = ((learn.learned().get("t-gemini") or {}).get("limits") or {}).get("rpd") or {}
250
+ check(r["status"] == 429 and rpd.get("value") == 50 and rpd.get("where") == "quota",
251
+ "a Gemini 429 body's daily quota is learned as the model's rpd", rpd)
252
+
253
+ # Pacing across threads: rpm 3, six seats at once, the server slow enough that they overlap.
254
+ out: list[dict] = []
255
+ seats = [threading.Thread(target=lambda: out.append(client.chat("t-paced", "hi", timeout=5)))
256
+ for _ in range(6)]
257
+ for s in seats:
258
+ s.start()
259
+ for s in seats:
260
+ s.join()
261
+ answered = [x for x in out if x["text"]]
262
+ refused = [x for x in out if "rate limit (not sent" in x["error"]]
263
+ check(len(answered) == 3 and len(refused) == 3 and len(seen["paced"]) == 3,
264
+ f"rpm 3 across six threads: {len(answered)} sent, {len(refused)} refused", [x["error"][:80] for x in out])
265
+ if refused:
266
+ body = client.output(refused[0], 5)
267
+ check(run.failure_class(body) == "capacity" and run.outcome(body)[0] == "error"
268
+ and "try again in" in body and learn.parse(body).get("limits") is None,
269
+ "the refusal is a capacity failure, with a wait and no new learned limit", body)
270
+ check(last("t-paced")["status"] in ("refused", "ok") and
271
+ sum(1 for x in ledger.rows() if x["alias"] == "t-paced" and x["status"] == "refused") == 3,
272
+ "refused calls are in the ledger, sending nothing")
273
+
274
+ # Across processes: rpm 4 on procprov, two processes of three calls.
275
+ procs = [multiprocessing.Process(target=_child, args=(3,)) for _ in range(2)]
276
+ for p in procs:
277
+ p.start()
278
+ for p in procs:
279
+ p.join()
280
+ rows = [x for x in ledger.rows() if x["connection"] == "procprov"]
281
+ sent = sum(1 for x in rows if x["status"] == "ok")
282
+ check(all(p.exitcode == 0 for p in procs) and sent == 4 and len(rows) == 6,
283
+ f"rpm 4 across two processes: {sent} of {len(rows)} sent", rows)
284
+
285
+ # A monthly cap: three this month already, so the next waits for next month, which no deadline fits.
286
+ now = time.time()
287
+ for _ in range(3):
288
+ ledger.append({"t": now - 1, "alias": "t-month", "connection": "monthprov", "status": "ok", "attempts": 1})
289
+ r = client.chat("t-month", "hi", timeout=30)
290
+ check("rate limit (not sent" in r["error"] and "a calendar month" in r["error"] and "this month" in r["error"],
291
+ "rpmo 3: the fourth call this month is refused", r["error"])
292
+ check(client.chat("t-ok", "hi")["text"] != "", "another connection is not held by that cap")
293
+
294
+ # The free-tier guard: a model that went paid is held until allowed.
295
+ before = len(seen.get("paid", []))
296
+ alerts.raise_alert("model:paid:paid", "went paid", "t-paid is listed with a price now (models.dev)",
297
+ "check the provider", alias="t-paid")
298
+ r = client.chat("t-paid", "hi")
299
+ body = client.output(r, 30)
300
+ check("free-tier guard" in r["error"] and len(seen.get("paid", [])) == before
301
+ and run.failure_class(body) == "entitlement" and last("t-paid")["status"] == "refused",
302
+ "the free-tier guard holds a model that went paid, as entitlement", body)
303
+ (TMP / "config" / "connections.json").write_text('{"allow_paid": ["t-paid"]}', encoding="utf-8")
304
+ check(client.chat("t-paid", "hi")["text"] != "", "allow_paid lets it through")
305
+
306
+ # cost.py reads the ledger.
307
+ data = cost.collect()
308
+ ok = data.get("t-ok", {})
309
+ check(ok.get("calls", 0) >= 2 and ok.get("input", 0) >= 14 and ok.get("output", 0) >= 8,
310
+ "cost.py counts the ledger's calls and tokens", ok)
311
+
312
+ # The dashboard: the Endpoints dialog's limits take rpmo; the Quotas page shows records and the ledger.
313
+ sys.path.insert(0, str(ROOT))
314
+ import serve
315
+ saved = serve.save_connection_limits("monthprov", {"rpm": 20, "rpmo": 1000})
316
+ view = {c["name"]: c for c in serve.connections_view()["connections"]}
317
+ check(saved["ok"] and "rpmo" in serve.connections_view()["keys"]
318
+ and view.get("monthprov", {}).get("limits") == {"rpm": 20, "rpmo": 1000},
319
+ "the Endpoints dialog saves a monthly cap (rpmo)", (saved, view.get("monthprov")))
320
+ bad = serve.save_connection_limits("monthprov", {"rpmo": -1})
321
+ check(not bad["ok"] and serve.save_model_facts("t-ok", {"limits": {"rpmo": 5}}).get("ok") is not None
322
+ and "rpmo" not in ((serve.catalog.overrides().get("t-ok") or {}).get("limits") or {}),
323
+ "a bad rpmo is refused; a model's own limits have none", bad)
324
+ page = serve.quotas_page()
325
+ check("What providers said is left" in page and "fakeprov" in page and "What the bench sent" in page,
326
+ "the Quotas page shows the records and the ledger's counts")
327
+
328
+ srv.shutdown()
329
+ print("\nthe ledger, quotas and pacing hold" if not failures else f"\n{failures} FAILED")
330
+ return 1 if failures else 0
331
+
332
+
333
+ if __name__ == "__main__":
334
+ sys.exit(main())
@@ -358,8 +358,11 @@ def main() -> int:
358
358
  hold.wait(10)
359
359
  return outcome[self.served][0]
360
360
 
361
- def fake_spawn(served, endpoint="", who="ui"):
361
+ modes = {}
362
+
363
+ def fake_spawn(served, endpoint="", who="ui", mode="full"):
362
364
  order.append(served)
365
+ modes[served] = mode
363
366
  (runs / f"onboard-{served}.log").write_text(f"onboarding {served} from e -> alias {served}-x\n"
364
367
  + outcome[served][1], encoding="utf-8")
365
368
  return {"ok": True, "errors": [], "log": f"onboard-{served}.log"}, Proc(served)
@@ -371,7 +374,7 @@ def main() -> int:
371
374
  time.sleep(0.05)
372
375
 
373
376
  kept = (serve._spawn_onboard, serve._onboard_refusal, serve.DATA)
374
- serve._spawn_onboard, serve._onboard_refusal, serve.DATA = fake_spawn, lambda s, e: [], tmp
377
+ serve._spawn_onboard, serve._onboard_refusal, serve.DATA = fake_spawn, lambda s, e, m="full": [], tmp
375
378
  try:
376
379
  first = serve.queue_onboard([{"served": "a", "endpoint": "e"}, {"served": "ok", "endpoint": "e"}])
377
380
  twice = serve.queue_onboard([{"served": "ok", "endpoint": "e"}])["ok"]
@@ -394,7 +397,8 @@ def main() -> int:
394
397
  settle()
395
398
  stopped = {i["served"]: i["state"] for i in serve.onboard_queue()["items"]}
396
399
  order.remove("quiet")
397
- serve.queue_onboard([{"served": s, "endpoint": "e"} for s in ("ok", "quiet", "bad")])
400
+ serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m} for s, m in
401
+ (("ok", "full"), ("quiet", "quick"), ("bad", "quick-adhoc"))])
398
402
  settle()
399
403
  batch = {i["served"]: (i["state"], i["alias"], bool(i["note"])) for i in serve.onboard_queue()["items"]}
400
404
  cleared = serve.clear_onboard_queue()["items"]
@@ -407,8 +411,9 @@ def main() -> int:
407
411
  and stopped == {"a": "done", "ok": "skipped", "quiet": "no answer"} and order == ["a", "ok", "quiet", "bad"]
408
412
  and batch == {"ok": ("done", "ok-x", False), "quiet": ("no answer", "quiet-x", True),
409
413
  "bad": ("failed", "bad-x", True)}
410
- and cleared == [] and bad_input == (False, False, False))
411
- print(("ok " if ok else "FAIL ") + "onboarding queue" + ("" if ok else f" -> {stopped} {order} {batch}"))
414
+ and cleared == [] and bad_input == (False, False, False)
415
+ and modes == {"a": "full", "ok": "full", "quiet": "quick", "bad": "quick-adhoc"})
416
+ print(("ok " if ok else "FAIL ") + "onboarding queue" + ("" if ok else f" -> {stopped} {order} {batch} {modes}"))
412
417
  failures += 0 if ok else 1
413
418
 
414
419
  # Dismiss: one picker key or many in one write, a change-log line each;
@@ -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())
@@ -88,7 +88,11 @@ def bench_field() -> None:
88
88
  assert "prompt" not in b and "system" not in b
89
89
  chunks = [json.loads(line[6:]) for line in post({**msg, "stream": True}).splitlines()
90
90
  if line.startswith("data: {")]
91
- assert "bench" in chunks[0] and "bench" not in chunks[1], chunks
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
92
96
  srv.shutdown()
93
97
 
94
98
 
@@ -97,13 +97,14 @@ def main() -> int:
97
97
  write("model-overrides.json", {"eye": {"vision": True}, "eye2": {"vision": True}, "echo": {"vision": True},
98
98
  "blind": {"vision": False}, "planned": {"vision": True},
99
99
  "tiny": {"vision": True, "context_window": 3000}})
100
+ write("model-roles.json", {"models": {"eye2": "vision"}}) # a specialist: sits only for pictures
100
101
  write("gateway.json", {"max_calls": 5, "max_explorers": 2})
101
102
  write("collections.json", {
102
103
  "vision": {"members": ["eye", "blind", "unknown", "echo"], "description": "photos"},
103
104
  "merged": {"members": ["eye", "eye2"], "reply": "merged", "merger": "mg", "description": "x"},
104
105
  "blindonly": {"members": ["blind", "unknown", "planned"], "description": "x"},
105
106
  "small": {"members": ["tiny"], "description": "x"},
106
- "text": {"members": ["eye"], "description": "x"}})
107
+ "text": {"members": ["eye", "eye2"], "description": "x"}})
107
108
 
108
109
  import client
109
110
  import gateway
@@ -164,12 +165,14 @@ def main() -> int:
164
165
  where = [text[max(0, text.find(secret) - 300):text.find(secret) + 50] for text in shown if secret in text]
165
166
  assert not where, f"{secret!r} was written down: {where}"
166
167
 
167
- # 2. A text request takes the llm path, as before, with the same bench keys.
168
+ # 2. A text request takes the llm path, as before, with the same bench keys;
169
+ # the vision specialist among the members is not asked, and is named.
168
170
  write("gateway.json", {"max_calls": 5, "max_explorers": 0}) # members only from here
169
171
  ASKED.clear()
170
172
  status, r = post({"model": "bench/text", "messages": [{"role": "user", "content": "say hi"}]})
171
173
  assert status == 200 and LLM_CALLS == [("eye", "say hi")] and not ASKED, (status, LLM_CALLS, ASKED)
172
- assert set(r["bench"]) == {"panel", "collection", "shape", "shape_by", "seats", "verdict"}, r["bench"]
174
+ assert set(r["bench"]) == {"panel", "collection", "shape", "shape_by", "seats", "verdict", "skipped"}, r["bench"]
175
+ assert [(k["alias"], "vision specialist" in k["why"]) for k in r["bench"]["skipped"]] == [("eye2", True)]
173
176
 
174
177
  # 3. Merged: the seats see the images; the merger gets text, placeholders and answers.
175
178
  LLM_CALLS.clear()