@wendellresearch-ai/benchwarmers 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +147 -147
- package/BENCHMARKS.md +262 -262
- package/GATEWAY.md +272 -271
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -275
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -155
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -72
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -712
- package/launcher.py +120 -0
- package/mcp_server.py +266 -0
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +295 -295
- package/metrics/client.py +394 -373
- package/metrics/cliproxy.py +191 -191
- package/metrics/connect.py +691 -691
- package/metrics/cost.py +152 -152
- package/metrics/decide.py +227 -227
- package/metrics/discover.py +318 -318
- package/metrics/endpoints.py +436 -436
- package/metrics/guard.py +617 -592
- package/metrics/learn.py +221 -221
- package/metrics/litellm_hub.py +206 -206
- package/metrics/model_roles.py +84 -84
- package/metrics/offload.py +166 -166
- package/metrics/owui.py +205 -205
- package/metrics/paths.py +60 -60
- package/metrics/route.py +579 -574
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -64
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -104
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +401 -331
- package/scripts/provider_tags.py +129 -129
- package/scripts/reprefix.py +136 -136
- package/scripts/sync_config.py +105 -105
- package/scripts/test_auth.py +255 -255
- package/scripts/test_cli.py +189 -0
- package/scripts/test_client.py +233 -233
- package/scripts/test_connect.py +222 -222
- package/scripts/test_first_run.py +180 -180
- package/scripts/test_hubs.py +328 -328
- package/scripts/test_launcher.py +67 -0
- package/scripts/test_llm_log.py +85 -0
- package/scripts/test_mcp.py +178 -0
- package/scripts/test_merged.py +214 -193
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -386
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3662
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1040
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -424
- package/ui/bench.js +1756 -1555
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""launcher.py: what each command line runs, and that it really runs.
|
|
2
|
+
|
|
3
|
+
python scripts/test_launcher.py
|
|
4
|
+
|
|
5
|
+
The launcher is what `benchwarmers` and `bench` are on every install (pip's
|
|
6
|
+
entry point, npm's bin/benchwarmers.js hands over to it, a checkout runs it
|
|
7
|
+
directly), so each route is checked: the version, the help, an unknown
|
|
8
|
+
command, the servers (with the first run where it belongs) and the client
|
|
9
|
+
(in the caller's folder, its own --help). Then two real runs: --version, and
|
|
10
|
+
mcp answering initialize with nothing else on stdout.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
22
|
+
sys.path.insert(0, str(ROOT))
|
|
23
|
+
import launcher # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
v = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
|
28
|
+
assert launcher.plan(["--version"]) == {"say": v, "code": 0}
|
|
29
|
+
for argv in ([], ["serve"], ["--port-is-not-a-flag"]): # no command: the dashboard
|
|
30
|
+
p = launcher.plan(argv)
|
|
31
|
+
assert p["script"] == "serve.py" and p["first_run"], (argv, p)
|
|
32
|
+
assert launcher.plan(["serve", "--x"])["args"] == ["--x"]
|
|
33
|
+
for argv in (["--help"], ["serve", "-h"], ["gateway", "--help"]):
|
|
34
|
+
p = launcher.plan(argv)
|
|
35
|
+
assert p["code"] == 0 and "ask <collection>" in p["say"], argv
|
|
36
|
+
p = launcher.plan(["nope"])
|
|
37
|
+
assert p["code"] == 2 and p.get("stderr") and 'no command "nope"' in p["say"], p
|
|
38
|
+
p = launcher.plan(["init", "--help"]) # init's own help
|
|
39
|
+
assert p["script"].endswith("init.py") and p["args"] == ["--help"] and not p["first_run"], p
|
|
40
|
+
p = launcher.plan(["mcp"])
|
|
41
|
+
assert p["script"] == "mcp_server.py" and not p["first_run"], p # stdout is the protocol's
|
|
42
|
+
for argv in (["status"], ["ask", "core", "hi", "--help"], ["collection", "rm", "x", "--yes"]):
|
|
43
|
+
p = launcher.plan(argv)
|
|
44
|
+
assert p["script"] == "cli.py" and p["args"] == argv, (argv, p)
|
|
45
|
+
assert p["cwd"] == os.getcwd() and not p["first_run"], p # the caller's folder: -f is theirs
|
|
46
|
+
assert launcher.plan(["gateway"])["cwd"] == str(ROOT)
|
|
47
|
+
|
|
48
|
+
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
|
49
|
+
done = subprocess.run([sys.executable, str(ROOT / "launcher.py"), "--version"], capture_output=True,
|
|
50
|
+
text=True, env=env, timeout=60)
|
|
51
|
+
assert done.returncode == 0 and done.stdout.strip() == v, done
|
|
52
|
+
done = subprocess.run([sys.executable, str(ROOT / "launcher.py"), "nope"], capture_output=True,
|
|
53
|
+
text=True, env=env, timeout=60)
|
|
54
|
+
assert done.returncode == 2 and not done.stdout and "nope" in done.stderr, done
|
|
55
|
+
hello = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
56
|
+
"params": {"protocolVersion": "2025-06-18"}}) + "\n"
|
|
57
|
+
done = subprocess.run([sys.executable, str(ROOT / "launcher.py"), "mcp"], input=hello, capture_output=True,
|
|
58
|
+
text=True, env=env, timeout=60)
|
|
59
|
+
lines = done.stdout.splitlines()
|
|
60
|
+
assert done.returncode == 0 and len(lines) == 1, done
|
|
61
|
+
assert json.loads(lines[0])["result"]["serverInfo"]["version"] == v, lines
|
|
62
|
+
print("ok")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""An answer stays an answer when the `llm` CLI fails to write its own log.
|
|
2
|
+
|
|
3
|
+
python scripts/test_llm_log.py
|
|
4
|
+
|
|
5
|
+
2026-10-22, panel p20261022-160401-6ca2: seat inkling-small-free printed
|
|
6
|
+
"pong" and was recorded as an error, because `llm` then raised
|
|
7
|
+
"sqlite3.OperationalError: database is locked" writing logs.db, which the
|
|
8
|
+
other seats' `llm` processes held. A fake `llm` here prints an answer and
|
|
9
|
+
fails that way, through the two places the bench runs `llm -m`: the gateway's
|
|
10
|
+
seat call and suite/run.py's ask_once (the perf probe, the suite, repoqa,
|
|
11
|
+
retries, auto-retire). The same lock before any answer, and a stream cut
|
|
12
|
+
mid-answer, stay errors. BENCHWARMERS_HOME and LLM_USER_PATH name temp
|
|
13
|
+
folders before anything is imported.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import pathlib
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
24
|
+
TMP = pathlib.Path(tempfile.mkdtemp(prefix="bench-llmlog-"))
|
|
25
|
+
(TMP / "config").mkdir()
|
|
26
|
+
(TMP / "llm").mkdir()
|
|
27
|
+
os.environ.update(BENCHWARMERS_HOME=str(TMP), LLM_USER_PATH=str(TMP / "llm"), BENCH_CLIENT="llm")
|
|
28
|
+
sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite")]
|
|
29
|
+
|
|
30
|
+
FAKE = '''import os, sys
|
|
31
|
+
sys.stdin.read()
|
|
32
|
+
mode = os.environ["FAKE_LLM_MODE"]
|
|
33
|
+
if mode in ("locked", "cut"):
|
|
34
|
+
sys.stdout.write("pong\\n" if mode == "locked" else "half an ans")
|
|
35
|
+
sys.stdout.flush()
|
|
36
|
+
if mode == "cut":
|
|
37
|
+
sys.stderr.write("Error: Connection error.\\n")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
if mode != "ok":
|
|
40
|
+
sys.stderr.write("Traceback (most recent call last):\\n"
|
|
41
|
+
" File \\"llm/models.py\\", line 1, in log_to_db\\n"
|
|
42
|
+
"sqlite3.OperationalError: database is locked\\n")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
sys.stdout.write("pong\\n")
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fake_llm() -> str:
|
|
49
|
+
"""A path that runs like an `llm` executable on Windows, macOS and Linux."""
|
|
50
|
+
# Its own folder: TMP/llm is llm's user folder (LLM_USER_PATH), so a shim
|
|
51
|
+
# named llm beside it would be that folder on macOS and Linux.
|
|
52
|
+
bin_dir = TMP / "bin"
|
|
53
|
+
bin_dir.mkdir(exist_ok=True)
|
|
54
|
+
script = bin_dir / "fake_llm.py"
|
|
55
|
+
script.write_text(FAKE, encoding="utf-8")
|
|
56
|
+
if os.name == "nt":
|
|
57
|
+
shim = bin_dir / "llm.cmd"
|
|
58
|
+
shim.write_text(f'@"{sys.executable}" "{script}" %*\r\n@exit /b %ERRORLEVEL%\r\n', encoding="utf-8")
|
|
59
|
+
else:
|
|
60
|
+
shim = bin_dir / "llm"
|
|
61
|
+
shim.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{script}" "$@"\n', encoding="utf-8")
|
|
62
|
+
shim.chmod(0o755)
|
|
63
|
+
return str(shim)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> int:
|
|
67
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
68
|
+
import gateway
|
|
69
|
+
import run
|
|
70
|
+
binary = fake_llm()
|
|
71
|
+
for mode, answered in (("ok", True), ("locked", True), ("locked-early", False), ("cut", False)):
|
|
72
|
+
os.environ["FAKE_LLM_MODE"] = mode
|
|
73
|
+
answer, error = gateway._ask_llm([binary, "-m", "m1"], "ping")
|
|
74
|
+
assert (answer, error == "") == (("pong", True) if answered else (answer, False)), (mode, answer, error)
|
|
75
|
+
body, _ = run.ask_once(binary, "m1", "ping", 60)
|
|
76
|
+
kind = (run.outcome(body, min_length=1) or ("answered", ""))[0]
|
|
77
|
+
assert (kind == "answered") == answered and (body.strip() == "pong") == answered, (mode, kind, body)
|
|
78
|
+
if mode == "locked-early":
|
|
79
|
+
assert "database is locked" in error and "database is locked" in body, (error, body)
|
|
80
|
+
print("ok")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
sys.exit(main())
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""The MCP server (mcp_server.py) as a client starts it: a subprocess on stdio, against a stub server.
|
|
2
|
+
|
|
3
|
+
python scripts/test_mcp.py
|
|
4
|
+
|
|
5
|
+
initialize and its protocol version negotiation, ping, tools/list, a call of
|
|
6
|
+
each tool (what reaches the server, gateway key included, and what comes
|
|
7
|
+
back), a refusal passed through as a tool error, a ping answered while a
|
|
8
|
+
panel is still running, protocol errors, and the error when no Benchwarmers
|
|
9
|
+
is running. Nothing but protocol messages may reach stdout.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import pathlib
|
|
17
|
+
import socket
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
23
|
+
|
|
24
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
25
|
+
SEEN: list[tuple[str, str, dict, str]] = [] # (method, path, body, Authorization)
|
|
26
|
+
|
|
27
|
+
MODELS = {"object": "list", "data": [
|
|
28
|
+
{"id": "bench/core", "description": "second opinions", "tags": ["general"], "members": ["m1", "m2"],
|
|
29
|
+
"explorers": 2, "shape": "", "reply": "all", "merger": "",
|
|
30
|
+
"together": {"context_window": 128000, "capabilities": ["tools"], "connections": ["x"], "limits": {}}},
|
|
31
|
+
{"id": "bench/core:merged", "description": "second opinions", "reply": "merged"}]}
|
|
32
|
+
SEATS = [{"call": "p1.member1", "seat": "member", "alias": "m1", "status": "ok", "seconds": 1.0,
|
|
33
|
+
"answer": "answer one", "error": ""},
|
|
34
|
+
{"call": "p1.explorer", "seat": "explorer", "alias": "x1", "status": "empty", "seconds": 2.0,
|
|
35
|
+
"answer": "", "error": ""}]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Stub(BaseHTTPRequestHandler):
|
|
39
|
+
def do_GET(self): # noqa: N802
|
|
40
|
+
SEEN.append(("GET", self.path, {}, self.headers.get("Authorization", "")))
|
|
41
|
+
self._send(200, MODELS)
|
|
42
|
+
|
|
43
|
+
def do_POST(self): # noqa: N802
|
|
44
|
+
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
45
|
+
SEEN.append(("POST", self.path, body, self.headers.get("Authorization", "")))
|
|
46
|
+
if self.path == "/bench/verdict":
|
|
47
|
+
return self._send(200, {"ok": True, "recorded": {"call": body["call"]}})
|
|
48
|
+
if body["model"] == "bench/nope":
|
|
49
|
+
return self._send(404, {"error": {"message": "no collection nope; choose one of bench/core"}})
|
|
50
|
+
time.sleep(1) # a panel takes a while
|
|
51
|
+
merged = (body.get("bench") or {}).get("reply") == "merged"
|
|
52
|
+
self._send(200, {"choices": [{"message": {"role": "assistant", "content": "merged answer" if merged
|
|
53
|
+
else "## member — m1\nanswer one"}}],
|
|
54
|
+
"bench": {"panel": "p1", "collection": body["model"], "shape": "general",
|
|
55
|
+
"seats": SEATS, "reply": "merged" if merged else "all"}})
|
|
56
|
+
|
|
57
|
+
def _send(self, status, data):
|
|
58
|
+
raw = json.dumps(data).encode()
|
|
59
|
+
self.send_response(status)
|
|
60
|
+
self.send_header("Content-Type", "application/json")
|
|
61
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
62
|
+
self.end_headers()
|
|
63
|
+
self.wfile.write(raw)
|
|
64
|
+
|
|
65
|
+
def log_message(self, *_a):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Client:
|
|
70
|
+
def __init__(self, url: str, key: str = ""):
|
|
71
|
+
env = {**os.environ, "BENCHWARMERS_URL": url, "BENCHWARMERS_KEY": key, "BENCHWARMERS_TIMEOUT": "30"}
|
|
72
|
+
self.p = subprocess.Popen([sys.executable, str(ROOT / "mcp_server.py")], stdin=subprocess.PIPE,
|
|
73
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
|
74
|
+
self.next = 0
|
|
75
|
+
|
|
76
|
+
def send(self, msg) -> None:
|
|
77
|
+
self.p.stdin.write((msg if isinstance(msg, str) else json.dumps(msg)).encode() + b"\n")
|
|
78
|
+
self.p.stdin.flush()
|
|
79
|
+
|
|
80
|
+
def read(self) -> dict:
|
|
81
|
+
line = self.p.stdout.readline()
|
|
82
|
+
assert line, "the server closed stdout: " + self.p.stderr.read().decode()
|
|
83
|
+
return json.loads(line) # every stdout line must be one JSON-RPC message
|
|
84
|
+
|
|
85
|
+
def request(self, method: str, params: dict | None = None) -> dict:
|
|
86
|
+
self.next += 1
|
|
87
|
+
self.send({"jsonrpc": "2.0", "id": self.next, "method": method, **({"params": params} if params else {})})
|
|
88
|
+
reply = self.read()
|
|
89
|
+
assert reply["id"] == self.next and reply["jsonrpc"] == "2.0", reply
|
|
90
|
+
return reply
|
|
91
|
+
|
|
92
|
+
def tool(self, name: str, **arguments) -> dict:
|
|
93
|
+
return self.request("tools/call", {"name": name, "arguments": arguments})["result"]
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
self.p.stdin.close()
|
|
97
|
+
self.p.wait(timeout=10)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
stub = ThreadingHTTPServer(("127.0.0.1", 0), Stub)
|
|
102
|
+
threading.Thread(target=stub.serve_forever, daemon=True).start()
|
|
103
|
+
url = f"http://127.0.0.1:{stub.server_port}"
|
|
104
|
+
c = Client(url + "/v1", key="bwk_test") # a pasted /v1 is fine
|
|
105
|
+
|
|
106
|
+
init = c.request("initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
|
|
107
|
+
"clientInfo": {"name": "test", "version": "0"}})["result"]
|
|
108
|
+
assert init["protocolVersion"] == "2025-06-18", init
|
|
109
|
+
assert init["capabilities"] == {"tools": {"listChanged": False}}, init
|
|
110
|
+
assert init["serverInfo"]["name"] == "benchwarmers" and "verdict" in init["instructions"], init
|
|
111
|
+
c.send({"jsonrpc": "2.0", "method": "notifications/initialized"}) # no answer to a notification
|
|
112
|
+
assert c.request("ping")["result"] == {}
|
|
113
|
+
|
|
114
|
+
tools = {t["name"]: t for t in c.request("tools/list")["result"]["tools"]}
|
|
115
|
+
assert set(tools) == {"list_collections", "ask", "verdict"}, tools
|
|
116
|
+
assert tools["ask"]["inputSchema"]["required"] == ["collection", "prompt"]
|
|
117
|
+
|
|
118
|
+
r = c.tool("list_collections")
|
|
119
|
+
assert not r["isError"], r
|
|
120
|
+
rows = json.loads(r["content"][0]["text"])
|
|
121
|
+
assert [x["name"] for x in rows] == ["bench/core"], rows # the :merged twin is folded in
|
|
122
|
+
assert rows[0]["context_window"] == 128000 and rows[0]["members"] == ["m1", "m2"], rows
|
|
123
|
+
assert SEEN[-1][:2] == ("GET", "/v1/models") and SEEN[-1][3] == "Bearer bwk_test", SEEN[-1]
|
|
124
|
+
|
|
125
|
+
# A ping sent while the panel runs is answered first.
|
|
126
|
+
c.send({"jsonrpc": "2.0", "id": "a", "method": "tools/call",
|
|
127
|
+
"params": {"name": "ask", "arguments": {"collection": "core", "prompt": "hi", "system": "be brief",
|
|
128
|
+
"with": ["x1"]}}})
|
|
129
|
+
c.send({"jsonrpc": "2.0", "id": "p", "method": "ping"})
|
|
130
|
+
first, second = c.read(), c.read()
|
|
131
|
+
assert (first["id"], second["id"]) == ("p", "a"), (first, second)
|
|
132
|
+
r = second["result"]
|
|
133
|
+
assert not r["isError"] and "answer one" in r["content"][0]["text"], r
|
|
134
|
+
seats = json.loads(r["content"][1]["text"].split("\n", 1)[1])["seats"]
|
|
135
|
+
assert [s["call"] for s in seats] == ["p1.member1", "p1.explorer"] and "answer" not in seats[0], seats
|
|
136
|
+
method, path, body, _ = SEEN[-1]
|
|
137
|
+
assert (method, path) == ("POST", "/v1/chat/completions"), SEEN[-1]
|
|
138
|
+
assert body["model"] == "bench/core" and body["with"] == ["x1"] and "bench" not in body, body
|
|
139
|
+
assert body["messages"] == [{"role": "system", "content": "be brief"}, {"role": "user", "content": "hi"}]
|
|
140
|
+
|
|
141
|
+
r = c.tool("ask", collection="bench/core", prompt="hi", merged=True)
|
|
142
|
+
assert SEEN[-1][2]["bench"] == {"reply": "merged"}, SEEN[-1]
|
|
143
|
+
assert r["content"][0]["text"] == "merged answer" and "answer one" in r["content"][1]["text"], r
|
|
144
|
+
|
|
145
|
+
r = c.tool("ask", collection="nope", prompt="hi") # the server's refusal, passed on
|
|
146
|
+
assert r["isError"] and "(404)" in r["content"][0]["text"] and "choose one" in r["content"][0]["text"], r
|
|
147
|
+
r = c.tool("ask", collection="core")
|
|
148
|
+
assert r["isError"] and "prompt" in r["content"][0]["text"], r
|
|
149
|
+
|
|
150
|
+
r = c.tool("verdict", call="p1.member1", useful=True, note="checked", real=["a true claim"], false=["x"])
|
|
151
|
+
assert not r["isError"] and "1 real, 1 false" in r["content"][0]["text"], r
|
|
152
|
+
assert SEEN[-1][1:3] == ("/bench/verdict", {"call": "p1.member1", "useful": True, "note": "checked",
|
|
153
|
+
"real": ["a true claim"], "false": ["x"]}), SEEN[-1]
|
|
154
|
+
assert c.tool("verdict", call="p1.member1")["isError"] # useful is required
|
|
155
|
+
|
|
156
|
+
assert c.request("tools/call", {"name": "nope"})["error"]["code"] == -32602
|
|
157
|
+
assert c.request("resources/list")["error"]["code"] == -32601
|
|
158
|
+
c.send("{not json")
|
|
159
|
+
assert c.read()["error"]["code"] == -32700
|
|
160
|
+
c.close()
|
|
161
|
+
|
|
162
|
+
# An unknown protocol version gets the newest; no server running is a clear tool error.
|
|
163
|
+
with socket.socket() as s:
|
|
164
|
+
s.bind(("127.0.0.1", 0))
|
|
165
|
+
dead = s.getsockname()[1]
|
|
166
|
+
c = Client(f"http://127.0.0.1:{dead}")
|
|
167
|
+
init = c.request("initialize", {"protocolVersion": "1999-01-01", "capabilities": {}})["result"]
|
|
168
|
+
assert init["protocolVersion"] == "2025-11-25", init
|
|
169
|
+
r = c.tool("list_collections")
|
|
170
|
+
assert r["isError"] and "not running" in r["content"][0]["text"], r
|
|
171
|
+
c.close()
|
|
172
|
+
stub.shutdown()
|
|
173
|
+
print("mcp server ok: initialize, ping, tools/list, each tool, refusals, a ping mid-panel, not running")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
sys.exit(main())
|