@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
package/suite/mine.py
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
"""Turn judged bench-gateway calls into DRAFT claim-verification tasks.
|
|
2
|
-
|
|
3
|
-
python suite/mine.py # judged calls with claims not yet mined
|
|
4
|
-
python suite/mine.py p20261016-... # draft tasks from one call
|
|
5
|
-
|
|
6
|
-
THE SUITE GROWS FROM REAL WORK
|
|
7
|
-
------------------------------
|
|
8
|
-
Every bench-gateway call logs its prompt — including the files, exactly as the
|
|
9
|
-
models saw them — and a verdict can record verified claims:
|
|
10
|
-
|
|
11
|
-
python metrics/offload.py verdict <call id> --useful \\
|
|
12
|
-
--real "<a finding it got right>" --false "<a claim it made that is untrue>"
|
|
13
|
-
|
|
14
|
-
Each claim becomes one task: the files as fixtures, the claim as a neutral
|
|
15
|
-
hypothesis, `VERDICT: TRUE` or `VERDICT: FALSE` on the first line. A --real
|
|
16
|
-
claim is true, a --false one is not, so both kinds accumulate. The code is the
|
|
17
|
-
version that was reviewed, frozen: a claim task tests verification, not
|
|
18
|
-
whether today's code still has the bug.
|
|
19
|
-
|
|
20
|
-
DRAFTS, NOT TASKS
|
|
21
|
-
-----------------
|
|
22
|
-
A mined task is written with "draft": true, and suite/run.py skips drafts
|
|
23
|
-
unless asked for by --task. Read it before removing the flag: is the claim
|
|
24
|
-
stated so the files alone decide it, with no model's name and no line number
|
|
25
|
-
that gives the answer away? Is the fixture small enough for free-tier models?
|
|
26
|
-
A claim the attached files cannot settle is a bad task, however true.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
from __future__ import annotations
|
|
30
|
-
|
|
31
|
-
import argparse
|
|
32
|
-
import json
|
|
33
|
-
import pathlib
|
|
34
|
-
import re
|
|
35
|
-
import sys
|
|
36
|
-
|
|
37
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
38
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
39
|
-
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
40
|
-
TASKS = DATA / "suite" / "tasks"
|
|
41
|
-
CALLS = DATA / "runs" / "gateway" / "calls.jsonl"
|
|
42
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
43
|
-
import offload # noqa: E402
|
|
44
|
-
|
|
45
|
-
PROMPT = ("Hypothesis about the attached code:\n\n{claim}\n\n"
|
|
46
|
-
"Decide whether the hypothesis is TRUE or FALSE for the code exactly as attached, using"
|
|
47
|
-
" standard Python and standard-library behaviour. Your first line must be exactly"
|
|
48
|
-
" `VERDICT: TRUE` or `VERDICT: FALSE`. Then quote the line or lines that decide it and"
|
|
49
|
-
" explain in at most five sentences. Plain ASCII.")
|
|
50
|
-
HEADER = re.compile(r"^===== (?:FILE: )?(.+?)(?: =====)?$", re.M)
|
|
51
|
-
BIG = 40_000 # characters of fixtures; past this, free models start failing
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def calls() -> dict[str, dict]:
|
|
55
|
-
out = {}
|
|
56
|
-
for line in CALLS.read_text(encoding="utf-8").splitlines() if CALLS.exists() else []:
|
|
57
|
-
try:
|
|
58
|
-
r = json.loads(line)
|
|
59
|
-
except ValueError:
|
|
60
|
-
continue
|
|
61
|
-
out[r["panel"]] = r
|
|
62
|
-
return out
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def files_in(prompt: str) -> dict[str, str]:
|
|
66
|
-
"""`===== path` sections of a logged prompt -> {path: text}. A section
|
|
67
|
-
whose name is not a file (a git diff, say) is kept under a safe name."""
|
|
68
|
-
marks = list(HEADER.finditer(prompt))
|
|
69
|
-
out = {}
|
|
70
|
-
for i, m in enumerate(marks):
|
|
71
|
-
end = marks[i + 1].start() if i + 1 < len(marks) else len(prompt)
|
|
72
|
-
name = re.sub(r"[^\w.-]+", "-", m.group(1).strip()).strip("-") or f"part{i}"
|
|
73
|
-
out[name if "." in name else name + ".txt"] = prompt[m.end():end].strip("\n") + "\n"
|
|
74
|
-
return out
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def claims_for(panel: str) -> list[tuple[str, bool, str]]:
|
|
78
|
-
"""(claim, is_true, seat call id) from every judged seat of the call."""
|
|
79
|
-
seen, out = set(), []
|
|
80
|
-
for e in offload.entries():
|
|
81
|
-
if not str(e.get("call", "")).startswith(panel + "."):
|
|
82
|
-
continue
|
|
83
|
-
for text, truth in [(c, True) for c in e.get("real", [])] + [(c, False) for c in e.get("false", [])]:
|
|
84
|
-
if text.lower() not in seen:
|
|
85
|
-
seen.add(text.lower())
|
|
86
|
-
out.append((text, truth, e["call"]))
|
|
87
|
-
return out
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def mined() -> set[str]:
|
|
91
|
-
return {json.loads(p.read_text(encoding="utf-8")).get("mined_from", "")
|
|
92
|
-
for p in TASKS.glob("*/task.json")}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def draft(panel: str) -> list[str]:
|
|
96
|
-
record = calls().get(panel)
|
|
97
|
-
if record is None:
|
|
98
|
-
sys.exit(f"no gateway call {panel!r} in {CALLS.relative_to(ROOT)}")
|
|
99
|
-
files = files_in(record["prompt"])
|
|
100
|
-
if not files:
|
|
101
|
-
sys.exit(f"{panel}: its prompt attached no `===== file` sections, so nothing can be frozen")
|
|
102
|
-
found = claims_for(panel)
|
|
103
|
-
if not found:
|
|
104
|
-
sys.exit(f"{panel}: no verdict records --real or --false claims for it yet")
|
|
105
|
-
size = sum(len(t) for t in files.values())
|
|
106
|
-
written = []
|
|
107
|
-
for n, (claim, truth, call) in enumerate(found, 1):
|
|
108
|
-
task_id = f"mined-{panel.removeprefix('p')}-{n}"
|
|
109
|
-
d = TASKS / task_id
|
|
110
|
-
(d / "fixtures").mkdir(parents=True, exist_ok=True)
|
|
111
|
-
for name, text in files.items():
|
|
112
|
-
(d / "fixtures" / name).write_text(text, encoding="utf-8")
|
|
113
|
-
yes, no = ("verdict: true", "verdict: false") if truth else ("verdict: false", "verdict: true")
|
|
114
|
-
spec = {"id": task_id, "draft": True, "shape": "claim", "mined_from": call,
|
|
115
|
-
"title": f"Claim verification ({'true' if truth else 'false'} claim), mined",
|
|
116
|
-
"why": f"{'TRUE' if truth else 'FALSE'}, verified by the caller in the verdict on"
|
|
117
|
-
f" {call} (model {record.get('model')}). Review before removing draft.",
|
|
118
|
-
# Its files are the snapshot copied above: a repo key no install
|
|
119
|
-
# maps, so build_prompt always reads the fixtures.
|
|
120
|
-
"prompt": PROMPT.format(claim=claim), "repo": "fixtures",
|
|
121
|
-
"attach": [f"snapshot/{name}" for name in files],
|
|
122
|
-
"must_find": [{"any_of": [yes]}], "must_not_claim": [no],
|
|
123
|
-
"score_in": "first_line", "min_answer_chars": 10}
|
|
124
|
-
(d / "task.json").write_text(json.dumps(spec, indent=2, ensure_ascii=False) + "\n",
|
|
125
|
-
encoding="utf-8")
|
|
126
|
-
written.append(task_id)
|
|
127
|
-
print(f"{len(written)} draft task(s) from {panel}: {', '.join(written)}")
|
|
128
|
-
if size > BIG:
|
|
129
|
-
print(f" note: {size:,} characters of fixtures; trim them to what the claims need"
|
|
130
|
-
" before promoting, or free models will fail on size rather than judgement")
|
|
131
|
-
print(" review each task.json, then remove \"draft\": true to put it in the suite")
|
|
132
|
-
return written
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def main() -> None:
|
|
136
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
137
|
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
138
|
-
ap.add_argument("panel", nargs="?", help="a gateway call id, e.g. p20261016-165636-afd6")
|
|
139
|
-
args = ap.parse_args()
|
|
140
|
-
if args.panel:
|
|
141
|
-
draft(args.panel)
|
|
142
|
-
return
|
|
143
|
-
done = {c.rsplit(".", 1)[0] for c in mined()}
|
|
144
|
-
rows = [(p, len(claims_for(p))) for p in calls()]
|
|
145
|
-
rows = [(p, n) for p, n in rows if n and p not in done]
|
|
146
|
-
if not rows:
|
|
147
|
-
print("Nothing to mine: no judged gateway call carries --real/--false claims that are"
|
|
148
|
-
" not already tasks.")
|
|
149
|
-
for p, n in rows:
|
|
150
|
-
print(f" {p}: {n} claim(s) python suite/mine.py {p}")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if __name__ == "__main__":
|
|
154
|
-
main()
|
|
1
|
+
"""Turn judged bench-gateway calls into DRAFT claim-verification tasks.
|
|
2
|
+
|
|
3
|
+
python suite/mine.py # judged calls with claims not yet mined
|
|
4
|
+
python suite/mine.py p20261016-... # draft tasks from one call
|
|
5
|
+
|
|
6
|
+
THE SUITE GROWS FROM REAL WORK
|
|
7
|
+
------------------------------
|
|
8
|
+
Every bench-gateway call logs its prompt — including the files, exactly as the
|
|
9
|
+
models saw them — and a verdict can record verified claims:
|
|
10
|
+
|
|
11
|
+
python metrics/offload.py verdict <call id> --useful \\
|
|
12
|
+
--real "<a finding it got right>" --false "<a claim it made that is untrue>"
|
|
13
|
+
|
|
14
|
+
Each claim becomes one task: the files as fixtures, the claim as a neutral
|
|
15
|
+
hypothesis, `VERDICT: TRUE` or `VERDICT: FALSE` on the first line. A --real
|
|
16
|
+
claim is true, a --false one is not, so both kinds accumulate. The code is the
|
|
17
|
+
version that was reviewed, frozen: a claim task tests verification, not
|
|
18
|
+
whether today's code still has the bug.
|
|
19
|
+
|
|
20
|
+
DRAFTS, NOT TASKS
|
|
21
|
+
-----------------
|
|
22
|
+
A mined task is written with "draft": true, and suite/run.py skips drafts
|
|
23
|
+
unless asked for by --task. Read it before removing the flag: is the claim
|
|
24
|
+
stated so the files alone decide it, with no model's name and no line number
|
|
25
|
+
that gives the answer away? Is the fixture small enough for free-tier models?
|
|
26
|
+
A claim the attached files cannot settle is a bad task, however true.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import pathlib
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
38
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
39
|
+
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
40
|
+
TASKS = DATA / "suite" / "tasks"
|
|
41
|
+
CALLS = DATA / "runs" / "gateway" / "calls.jsonl"
|
|
42
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
43
|
+
import offload # noqa: E402
|
|
44
|
+
|
|
45
|
+
PROMPT = ("Hypothesis about the attached code:\n\n{claim}\n\n"
|
|
46
|
+
"Decide whether the hypothesis is TRUE or FALSE for the code exactly as attached, using"
|
|
47
|
+
" standard Python and standard-library behaviour. Your first line must be exactly"
|
|
48
|
+
" `VERDICT: TRUE` or `VERDICT: FALSE`. Then quote the line or lines that decide it and"
|
|
49
|
+
" explain in at most five sentences. Plain ASCII.")
|
|
50
|
+
HEADER = re.compile(r"^===== (?:FILE: )?(.+?)(?: =====)?$", re.M)
|
|
51
|
+
BIG = 40_000 # characters of fixtures; past this, free models start failing
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calls() -> dict[str, dict]:
|
|
55
|
+
out = {}
|
|
56
|
+
for line in CALLS.read_text(encoding="utf-8").splitlines() if CALLS.exists() else []:
|
|
57
|
+
try:
|
|
58
|
+
r = json.loads(line)
|
|
59
|
+
except ValueError:
|
|
60
|
+
continue
|
|
61
|
+
out[r["panel"]] = r
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def files_in(prompt: str) -> dict[str, str]:
|
|
66
|
+
"""`===== path` sections of a logged prompt -> {path: text}. A section
|
|
67
|
+
whose name is not a file (a git diff, say) is kept under a safe name."""
|
|
68
|
+
marks = list(HEADER.finditer(prompt))
|
|
69
|
+
out = {}
|
|
70
|
+
for i, m in enumerate(marks):
|
|
71
|
+
end = marks[i + 1].start() if i + 1 < len(marks) else len(prompt)
|
|
72
|
+
name = re.sub(r"[^\w.-]+", "-", m.group(1).strip()).strip("-") or f"part{i}"
|
|
73
|
+
out[name if "." in name else name + ".txt"] = prompt[m.end():end].strip("\n") + "\n"
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def claims_for(panel: str) -> list[tuple[str, bool, str]]:
|
|
78
|
+
"""(claim, is_true, seat call id) from every judged seat of the call."""
|
|
79
|
+
seen, out = set(), []
|
|
80
|
+
for e in offload.entries():
|
|
81
|
+
if not str(e.get("call", "")).startswith(panel + "."):
|
|
82
|
+
continue
|
|
83
|
+
for text, truth in [(c, True) for c in e.get("real", [])] + [(c, False) for c in e.get("false", [])]:
|
|
84
|
+
if text.lower() not in seen:
|
|
85
|
+
seen.add(text.lower())
|
|
86
|
+
out.append((text, truth, e["call"]))
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mined() -> set[str]:
|
|
91
|
+
return {json.loads(p.read_text(encoding="utf-8")).get("mined_from", "")
|
|
92
|
+
for p in TASKS.glob("*/task.json")}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def draft(panel: str) -> list[str]:
|
|
96
|
+
record = calls().get(panel)
|
|
97
|
+
if record is None:
|
|
98
|
+
sys.exit(f"no gateway call {panel!r} in {CALLS.relative_to(ROOT)}")
|
|
99
|
+
files = files_in(record["prompt"])
|
|
100
|
+
if not files:
|
|
101
|
+
sys.exit(f"{panel}: its prompt attached no `===== file` sections, so nothing can be frozen")
|
|
102
|
+
found = claims_for(panel)
|
|
103
|
+
if not found:
|
|
104
|
+
sys.exit(f"{panel}: no verdict records --real or --false claims for it yet")
|
|
105
|
+
size = sum(len(t) for t in files.values())
|
|
106
|
+
written = []
|
|
107
|
+
for n, (claim, truth, call) in enumerate(found, 1):
|
|
108
|
+
task_id = f"mined-{panel.removeprefix('p')}-{n}"
|
|
109
|
+
d = TASKS / task_id
|
|
110
|
+
(d / "fixtures").mkdir(parents=True, exist_ok=True)
|
|
111
|
+
for name, text in files.items():
|
|
112
|
+
(d / "fixtures" / name).write_text(text, encoding="utf-8")
|
|
113
|
+
yes, no = ("verdict: true", "verdict: false") if truth else ("verdict: false", "verdict: true")
|
|
114
|
+
spec = {"id": task_id, "draft": True, "shape": "claim", "mined_from": call,
|
|
115
|
+
"title": f"Claim verification ({'true' if truth else 'false'} claim), mined",
|
|
116
|
+
"why": f"{'TRUE' if truth else 'FALSE'}, verified by the caller in the verdict on"
|
|
117
|
+
f" {call} (model {record.get('model')}). Review before removing draft.",
|
|
118
|
+
# Its files are the snapshot copied above: a repo key no install
|
|
119
|
+
# maps, so build_prompt always reads the fixtures.
|
|
120
|
+
"prompt": PROMPT.format(claim=claim), "repo": "fixtures",
|
|
121
|
+
"attach": [f"snapshot/{name}" for name in files],
|
|
122
|
+
"must_find": [{"any_of": [yes]}], "must_not_claim": [no],
|
|
123
|
+
"score_in": "first_line", "min_answer_chars": 10}
|
|
124
|
+
(d / "task.json").write_text(json.dumps(spec, indent=2, ensure_ascii=False) + "\n",
|
|
125
|
+
encoding="utf-8")
|
|
126
|
+
written.append(task_id)
|
|
127
|
+
print(f"{len(written)} draft task(s) from {panel}: {', '.join(written)}")
|
|
128
|
+
if size > BIG:
|
|
129
|
+
print(f" note: {size:,} characters of fixtures; trim them to what the claims need"
|
|
130
|
+
" before promoting, or free models will fail on size rather than judgement")
|
|
131
|
+
print(" review each task.json, then remove \"draft\": true to put it in the suite")
|
|
132
|
+
return written
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
137
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
138
|
+
ap.add_argument("panel", nargs="?", help="a gateway call id, e.g. p20261016-165636-afd6")
|
|
139
|
+
args = ap.parse_args()
|
|
140
|
+
if args.panel:
|
|
141
|
+
draft(args.panel)
|
|
142
|
+
return
|
|
143
|
+
done = {c.rsplit(".", 1)[0] for c in mined()}
|
|
144
|
+
rows = [(p, len(claims_for(p))) for p in calls()]
|
|
145
|
+
rows = [(p, n) for p, n in rows if n and p not in done]
|
|
146
|
+
if not rows:
|
|
147
|
+
print("Nothing to mine: no judged gateway call carries --real/--false claims that are"
|
|
148
|
+
" not already tasks.")
|
|
149
|
+
for p, n in rows:
|
|
150
|
+
print(f" {p}: {n} claim(s) python suite/mine.py {p}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|
package/suite/pace.py
CHANGED
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
"""Run suite tasks across the roster slowly, one call at a time, in the background.
|
|
2
|
-
|
|
3
|
-
python suite/pace.py # every seeded-hard task, every model, 120 s apart
|
|
4
|
-
python suite/pace.py --prefix claim- --gap 300
|
|
5
|
-
python suite/pace.py --dry-run # list what would run, call nothing
|
|
6
|
-
|
|
7
|
-
Started detached it works through the queue on its own and stops when done:
|
|
8
|
-
|
|
9
|
-
Start-Process .venv\\Scripts\\python.exe -ArgumentList suite\\pace.py -WindowStyle Hidden # Windows
|
|
10
|
-
nohup .venv/bin/python suite/pace.py > /dev/null 2>&1 & # macOS, Linux
|
|
11
|
-
|
|
12
|
-
FREE TIERS LIMIT RATE, NOT TOTAL
|
|
13
|
-
--------------------------------
|
|
14
|
-
A full roster pass of ten seeded pairs is ~300 calls of 8-14K characters. Sent
|
|
15
|
-
back to back it trips every provider's per-minute limit and the run fills
|
|
16
|
-
with "rate limit" rows that say nothing about the models. Sent one at a time,
|
|
17
|
-
`--gap` seconds apart and ROUND-ROBIN — task by task across every model, so
|
|
18
|
-
one model is asked once per lap rather than twenty times in a row — each model
|
|
19
|
-
sees a call roughly every (models x gap) seconds, far under any limit.
|
|
20
|
-
|
|
21
|
-
A call that comes back rate-limited is deleted (it is not an answer) and its
|
|
22
|
-
model is rested for `--rest` seconds before being asked again later; the rest
|
|
23
|
-
of the queue carries on. Pairs that already have a saved answer are skipped,
|
|
24
|
-
so the run can be stopped and started again at any time and picks up where it
|
|
25
|
-
was. It logs to runs/pace-<date>.log and calls nothing that suite/run.py
|
|
26
|
-
would not.
|
|
27
|
-
|
|
28
|
-
Models on a retiring alert, ad hoc models and decision models are left out:
|
|
29
|
-
the first are failing their probes, the second cost money, the third cannot
|
|
30
|
-
answer a prompt.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
from __future__ import annotations
|
|
34
|
-
|
|
35
|
-
import argparse
|
|
36
|
-
import datetime as dt
|
|
37
|
-
import json
|
|
38
|
-
import os
|
|
39
|
-
import pathlib
|
|
40
|
-
import subprocess
|
|
41
|
-
import sys
|
|
42
|
-
import time
|
|
43
|
-
|
|
44
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
45
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
46
|
-
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
47
|
-
TASKS = DATA / "suite" / "tasks"
|
|
48
|
-
sys.path.insert(0, str(ROOT / "suite"))
|
|
49
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
50
|
-
import run # noqa: E402
|
|
51
|
-
|
|
52
|
-
PY = run.bench_python()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def models() -> list[str]:
|
|
56
|
-
try:
|
|
57
|
-
import alerts
|
|
58
|
-
retiring = {a.get("alias") for a in alerts.active().values() if a.get("kind") == "retiring"}
|
|
59
|
-
except Exception: # noqa: BLE001
|
|
60
|
-
retiring = set()
|
|
61
|
-
return [m for m in run.available_models() if m not in retiring and m not in run.ad_hoc_models()]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def answered(task: str, model: str) -> bool:
|
|
65
|
-
"""A saved file that is an answer. One that recorded a block (a rate
|
|
66
|
-
limit, an error, a timeout) is not, so the pair is asked again."""
|
|
67
|
-
out = TASKS / task / "output" / f"{model}.md"
|
|
68
|
-
if not out.exists():
|
|
69
|
-
return False
|
|
70
|
-
spec = json.loads((TASKS / task / "task.json").read_text(encoding="utf-8"))
|
|
71
|
-
return run.score(spec, out.read_text(encoding="utf-8", errors="replace"))["kind"] == "answered"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def queue(prefix: str, roster: list[str]) -> list[tuple[str, str]]:
|
|
75
|
-
"""(task, model) pairs without an answer yet, task by task across models."""
|
|
76
|
-
tasks = sorted(p.parent.name for p in TASKS.glob(f"{prefix}*/task.json")
|
|
77
|
-
if not json.loads(p.read_text(encoding="utf-8")).get("draft"))
|
|
78
|
-
return [(t, m) for t in tasks for m in roster if not answered(t, m)]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def rate_limited(task: str, model: str) -> bool:
|
|
82
|
-
out = TASKS / task / "output" / f"{model}.md"
|
|
83
|
-
if not out.exists():
|
|
84
|
-
return False
|
|
85
|
-
blocked = run.score(json.loads((TASKS / task / "task.json").read_text(encoding="utf-8")),
|
|
86
|
-
out.read_text(encoding="utf-8", errors="replace")).get("blocked") or ""
|
|
87
|
-
return "rate limit" in blocked or "capacity" in blocked
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def main() -> None:
|
|
91
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
92
|
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
93
|
-
ap.add_argument("--prefix", default="seeded-hard-", help="task directories starting with this")
|
|
94
|
-
ap.add_argument("--gap", type=int, default=120, help="seconds between calls")
|
|
95
|
-
ap.add_argument("--rest", type=int, default=1800, help="seconds a rate-limited model is rested")
|
|
96
|
-
ap.add_argument("--timeout", type=int, default=420)
|
|
97
|
-
ap.add_argument("--dry-run", action="store_true")
|
|
98
|
-
args = ap.parse_args()
|
|
99
|
-
|
|
100
|
-
roster = models()
|
|
101
|
-
todo = queue(args.prefix, roster)
|
|
102
|
-
log_path = DATA / "runs" / f"pace-{dt.date.today().isoformat()}.log"
|
|
103
|
-
|
|
104
|
-
def log(msg: str) -> None:
|
|
105
|
-
line = f"{dt.datetime.now():%H:%M:%S} {msg}"
|
|
106
|
-
print(line, flush=True)
|
|
107
|
-
if not args.dry_run:
|
|
108
|
-
with log_path.open("a", encoding="utf-8") as fh:
|
|
109
|
-
fh.write(line + "\n")
|
|
110
|
-
|
|
111
|
-
eta = len(todo) * (args.gap + 60) / 3600
|
|
112
|
-
log(f"{len(todo)} calls queued across {len(roster)} models, {args.gap}s apart"
|
|
113
|
-
f" (about {eta:.1f} h): {', '.join(roster)}")
|
|
114
|
-
if args.dry_run:
|
|
115
|
-
for t, m in todo:
|
|
116
|
-
print(f" {t} {m}")
|
|
117
|
-
return
|
|
118
|
-
rested: dict[str, float] = {}
|
|
119
|
-
while todo:
|
|
120
|
-
now = time.monotonic()
|
|
121
|
-
pick = next(((t, m) for t, m in todo if rested.get(m, 0) <= now), None)
|
|
122
|
-
if pick is None:
|
|
123
|
-
# Only models with work left: a rest that already ended would make
|
|
124
|
-
# this negative, and time.sleep raises on a negative wait.
|
|
125
|
-
time.sleep(max(1, min(rested[m] for _, m in todo) - now + 1))
|
|
126
|
-
continue
|
|
127
|
-
todo.remove(pick)
|
|
128
|
-
task, model = pick
|
|
129
|
-
subprocess.run([str(PY), str(ROOT / "suite" / "run.py"), "--task", task, "--model", model,
|
|
130
|
-
"--timeout", str(args.timeout)], cwd=str(ROOT), capture_output=True,
|
|
131
|
-
env={**os.environ, "PYTHONIOENCODING": "utf-8"})
|
|
132
|
-
if rate_limited(task, model):
|
|
133
|
-
(TASKS / task / "output" / f"{model}.md").unlink()
|
|
134
|
-
rested[model] = time.monotonic() + args.rest
|
|
135
|
-
todo.append(pick)
|
|
136
|
-
log(f"rate-limited {model} on {task}; resting it {args.rest}s, retried later")
|
|
137
|
-
else:
|
|
138
|
-
log(f"done {model} on {task} ({len(todo)} left)")
|
|
139
|
-
if todo:
|
|
140
|
-
time.sleep(args.gap)
|
|
141
|
-
log("queue empty; finished")
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if __name__ == "__main__":
|
|
145
|
-
main()
|
|
1
|
+
"""Run suite tasks across the roster slowly, one call at a time, in the background.
|
|
2
|
+
|
|
3
|
+
python suite/pace.py # every seeded-hard task, every model, 120 s apart
|
|
4
|
+
python suite/pace.py --prefix claim- --gap 300
|
|
5
|
+
python suite/pace.py --dry-run # list what would run, call nothing
|
|
6
|
+
|
|
7
|
+
Started detached it works through the queue on its own and stops when done:
|
|
8
|
+
|
|
9
|
+
Start-Process .venv\\Scripts\\python.exe -ArgumentList suite\\pace.py -WindowStyle Hidden # Windows
|
|
10
|
+
nohup .venv/bin/python suite/pace.py > /dev/null 2>&1 & # macOS, Linux
|
|
11
|
+
|
|
12
|
+
FREE TIERS LIMIT RATE, NOT TOTAL
|
|
13
|
+
--------------------------------
|
|
14
|
+
A full roster pass of ten seeded pairs is ~300 calls of 8-14K characters. Sent
|
|
15
|
+
back to back it trips every provider's per-minute limit and the run fills
|
|
16
|
+
with "rate limit" rows that say nothing about the models. Sent one at a time,
|
|
17
|
+
`--gap` seconds apart and ROUND-ROBIN — task by task across every model, so
|
|
18
|
+
one model is asked once per lap rather than twenty times in a row — each model
|
|
19
|
+
sees a call roughly every (models x gap) seconds, far under any limit.
|
|
20
|
+
|
|
21
|
+
A call that comes back rate-limited is deleted (it is not an answer) and its
|
|
22
|
+
model is rested for `--rest` seconds before being asked again later; the rest
|
|
23
|
+
of the queue carries on. Pairs that already have a saved answer are skipped,
|
|
24
|
+
so the run can be stopped and started again at any time and picks up where it
|
|
25
|
+
was. It logs to runs/pace-<date>.log and calls nothing that suite/run.py
|
|
26
|
+
would not.
|
|
27
|
+
|
|
28
|
+
Models on a retiring alert, ad hoc models and decision models are left out:
|
|
29
|
+
the first are failing their probes, the second cost money, the third cannot
|
|
30
|
+
answer a prompt.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import datetime as dt
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import pathlib
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
45
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
46
|
+
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
47
|
+
TASKS = DATA / "suite" / "tasks"
|
|
48
|
+
sys.path.insert(0, str(ROOT / "suite"))
|
|
49
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
50
|
+
import run # noqa: E402
|
|
51
|
+
|
|
52
|
+
PY = run.bench_python()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def models() -> list[str]:
|
|
56
|
+
try:
|
|
57
|
+
import alerts
|
|
58
|
+
retiring = {a.get("alias") for a in alerts.active().values() if a.get("kind") == "retiring"}
|
|
59
|
+
except Exception: # noqa: BLE001
|
|
60
|
+
retiring = set()
|
|
61
|
+
return [m for m in run.available_models() if m not in retiring and m not in run.ad_hoc_models()]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def answered(task: str, model: str) -> bool:
|
|
65
|
+
"""A saved file that is an answer. One that recorded a block (a rate
|
|
66
|
+
limit, an error, a timeout) is not, so the pair is asked again."""
|
|
67
|
+
out = TASKS / task / "output" / f"{model}.md"
|
|
68
|
+
if not out.exists():
|
|
69
|
+
return False
|
|
70
|
+
spec = json.loads((TASKS / task / "task.json").read_text(encoding="utf-8"))
|
|
71
|
+
return run.score(spec, out.read_text(encoding="utf-8", errors="replace"))["kind"] == "answered"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def queue(prefix: str, roster: list[str]) -> list[tuple[str, str]]:
|
|
75
|
+
"""(task, model) pairs without an answer yet, task by task across models."""
|
|
76
|
+
tasks = sorted(p.parent.name for p in TASKS.glob(f"{prefix}*/task.json")
|
|
77
|
+
if not json.loads(p.read_text(encoding="utf-8")).get("draft"))
|
|
78
|
+
return [(t, m) for t in tasks for m in roster if not answered(t, m)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def rate_limited(task: str, model: str) -> bool:
|
|
82
|
+
out = TASKS / task / "output" / f"{model}.md"
|
|
83
|
+
if not out.exists():
|
|
84
|
+
return False
|
|
85
|
+
blocked = run.score(json.loads((TASKS / task / "task.json").read_text(encoding="utf-8")),
|
|
86
|
+
out.read_text(encoding="utf-8", errors="replace")).get("blocked") or ""
|
|
87
|
+
return "rate limit" in blocked or "capacity" in blocked
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
92
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
93
|
+
ap.add_argument("--prefix", default="seeded-hard-", help="task directories starting with this")
|
|
94
|
+
ap.add_argument("--gap", type=int, default=120, help="seconds between calls")
|
|
95
|
+
ap.add_argument("--rest", type=int, default=1800, help="seconds a rate-limited model is rested")
|
|
96
|
+
ap.add_argument("--timeout", type=int, default=420)
|
|
97
|
+
ap.add_argument("--dry-run", action="store_true")
|
|
98
|
+
args = ap.parse_args()
|
|
99
|
+
|
|
100
|
+
roster = models()
|
|
101
|
+
todo = queue(args.prefix, roster)
|
|
102
|
+
log_path = DATA / "runs" / f"pace-{dt.date.today().isoformat()}.log"
|
|
103
|
+
|
|
104
|
+
def log(msg: str) -> None:
|
|
105
|
+
line = f"{dt.datetime.now():%H:%M:%S} {msg}"
|
|
106
|
+
print(line, flush=True)
|
|
107
|
+
if not args.dry_run:
|
|
108
|
+
with log_path.open("a", encoding="utf-8") as fh:
|
|
109
|
+
fh.write(line + "\n")
|
|
110
|
+
|
|
111
|
+
eta = len(todo) * (args.gap + 60) / 3600
|
|
112
|
+
log(f"{len(todo)} calls queued across {len(roster)} models, {args.gap}s apart"
|
|
113
|
+
f" (about {eta:.1f} h): {', '.join(roster)}")
|
|
114
|
+
if args.dry_run:
|
|
115
|
+
for t, m in todo:
|
|
116
|
+
print(f" {t} {m}")
|
|
117
|
+
return
|
|
118
|
+
rested: dict[str, float] = {}
|
|
119
|
+
while todo:
|
|
120
|
+
now = time.monotonic()
|
|
121
|
+
pick = next(((t, m) for t, m in todo if rested.get(m, 0) <= now), None)
|
|
122
|
+
if pick is None:
|
|
123
|
+
# Only models with work left: a rest that already ended would make
|
|
124
|
+
# this negative, and time.sleep raises on a negative wait.
|
|
125
|
+
time.sleep(max(1, min(rested[m] for _, m in todo) - now + 1))
|
|
126
|
+
continue
|
|
127
|
+
todo.remove(pick)
|
|
128
|
+
task, model = pick
|
|
129
|
+
subprocess.run([str(PY), str(ROOT / "suite" / "run.py"), "--task", task, "--model", model,
|
|
130
|
+
"--timeout", str(args.timeout)], cwd=str(ROOT), capture_output=True,
|
|
131
|
+
env={**os.environ, "PYTHONIOENCODING": "utf-8"})
|
|
132
|
+
if rate_limited(task, model):
|
|
133
|
+
(TASKS / task / "output" / f"{model}.md").unlink()
|
|
134
|
+
rested[model] = time.monotonic() + args.rest
|
|
135
|
+
todo.append(pick)
|
|
136
|
+
log(f"rate-limited {model} on {task}; resting it {args.rest}s, retried later")
|
|
137
|
+
else:
|
|
138
|
+
log(f"done {model} on {task} ({len(todo)} left)")
|
|
139
|
+
if todo:
|
|
140
|
+
time.sleep(args.gap)
|
|
141
|
+
log("queue empty; finished")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
main()
|