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