@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.
Files changed (96) hide show
  1. package/AGENTS.md +147 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +272 -271
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +368 -275
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +170 -155
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -72
  12. package/cli.py +666 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +755 -712
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -0
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +295 -295
  22. package/metrics/client.py +394 -373
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +152 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/guard.py +617 -592
  30. package/metrics/learn.py +221 -221
  31. package/metrics/litellm_hub.py +206 -206
  32. package/metrics/model_roles.py +84 -84
  33. package/metrics/offload.py +166 -166
  34. package/metrics/owui.py +205 -205
  35. package/metrics/paths.py +60 -60
  36. package/metrics/route.py +579 -574
  37. package/metrics/scheduler.py +275 -275
  38. package/metrics/sources.py +167 -167
  39. package/metrics/wrong.py +144 -144
  40. package/package.json +68 -64
  41. package/perf/README.md +89 -89
  42. package/perf/run.py +261 -261
  43. package/scripts/auto_retire.py +367 -367
  44. package/scripts/check.py +108 -104
  45. package/scripts/check_deprecations.py +378 -378
  46. package/scripts/check_themes.py +103 -103
  47. package/scripts/init.py +124 -124
  48. package/scripts/list_served.py +78 -78
  49. package/scripts/notice.py +78 -78
  50. package/scripts/onboard.py +401 -331
  51. package/scripts/provider_tags.py +129 -129
  52. package/scripts/reprefix.py +136 -136
  53. package/scripts/sync_config.py +105 -105
  54. package/scripts/test_auth.py +255 -255
  55. package/scripts/test_cli.py +189 -0
  56. package/scripts/test_client.py +233 -233
  57. package/scripts/test_connect.py +222 -222
  58. package/scripts/test_first_run.py +180 -180
  59. package/scripts/test_hubs.py +328 -328
  60. package/scripts/test_launcher.py +67 -0
  61. package/scripts/test_llm_log.py +85 -0
  62. package/scripts/test_mcp.py +178 -0
  63. package/scripts/test_merged.py +214 -193
  64. package/scripts/test_no_owui.py +291 -291
  65. package/scripts/test_owui.py +90 -90
  66. package/scripts/test_serve_edit.py +448 -386
  67. package/scripts/test_verdict.py +98 -98
  68. package/scripts/test_vision.py +228 -228
  69. package/serve.py +3904 -3662
  70. package/suite/mine.py +154 -154
  71. package/suite/pace.py +145 -145
  72. package/suite/run.py +1046 -1040
  73. package/suite/seed.py +421 -421
  74. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  75. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  76. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  77. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  78. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  79. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  80. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  81. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  82. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  83. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  84. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  85. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  86. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  87. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  88. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  89. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  90. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  91. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  92. package/ui/bench.css +430 -424
  93. package/ui/bench.js +1756 -1555
  94. package/ui/themes/dark.css +35 -35
  95. package/ui/themes/hardwood.css +52 -52
  96. package/ui/themes/rainforest.css +32 -32
@@ -1,167 +1,167 @@
1
- """Public model lists the bench reads: models.dev and FreeLLMAPI's free catalog.
2
-
3
- python metrics/sources.py # fetch both now (normally once a day)
4
-
5
- WHY THESE TWO
6
- -------------
7
- A comparison on 2026-10-17 of models.dev, FreeLLMAPI, truefoundry/models,
8
- ClawLabsAI/free-ai-models and OpenRouter's own list found:
9
- - models.dev (MIT, anomalyco/models.dev): 232 providers, ~9,000 models, price
10
- per model per provider, context and output limits, tool calling, reasoning,
11
- modalities, and each provider's API base URL. OpenRouter's free models are in
12
- it exactly (19 of 19), so OpenRouter needs no separate pull.
13
- - FreeLLMAPI's free monthly catalog (MIT project, tashfeenahmed/freellmapi):
14
- 219 of its 360 free models and 17 of its providers are not in models.dev, and
15
- it carries rate limits (rpm/rpd/tpm/tpd) and per-model operating notes.
16
- Only the free monthly tier is read, never the premium feed.
17
- - truefoundry/models was stale for free models and ClawLabs duplicated
18
- models.dev's coverage, so neither is pulled; each is a small adapter later.
19
-
20
- Both are cached in runs/cache/ (gitignored) and refetched at most once a day:
21
- nothing here is called per request.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import datetime as dt
27
- import json
28
- import pathlib
29
- import re
30
- import sys
31
- import time
32
- import urllib.request
33
-
34
- ROOT = pathlib.Path(__file__).resolve().parent.parent
35
- from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
36
- CACHE = DATA / "runs" / "cache"
37
- FEEDS = {
38
- "modelsdev": "https://models.dev/api.json",
39
- "freellmapi": "https://api.freellmapi.co/v1/latest",
40
- }
41
- MAX_AGE_S = 20 * 3600
42
-
43
- # The bench's connection hosts (route.connection_of) -> models.dev provider id.
44
- # An install's own names (a proxy fronting one vendor) come from
45
- # config/connections.json `models_dev`, which wins.
46
- CONNECTION_TO_MODELSDEV = {
47
- "nvidia-api": "nvidia", "ollama-cloud": "ollama-cloud", "mistral": "mistral",
48
- "groq": "groq", "openrouter": "openrouter",
49
- "api.inceptionlabs.ai": "inception", "api.reflection.ai": "reflection",
50
- "cerebras": "cerebras",
51
- }
52
-
53
-
54
- def connection_to_modelsdev() -> dict[str, str]:
55
- """The built-in map with this install's own names over it."""
56
- import endpoints
57
- return {**CONNECTION_TO_MODELSDEV, **(endpoints.connections().get("models_dev") or {})}
58
-
59
-
60
- # models.dev providers whose free models answer only inside the provider's own
61
- # client, so Discover leaves those out; their paid models stay. opencode, tried
62
- # 2026-10-21 through Open WebUI: "opencode's free tier can only be used from
63
- # within opencode". opencode-go serves the same free ids, assumed the same.
64
- # An install switches a provider either way on Settings, which writes
65
- # config/connections.json `free_in_own_client_only`, e.g. {"opencode": false}.
66
- FREE_IN_OWN_CLIENT_ONLY = {"opencode": True, "opencode-go": True}
67
-
68
-
69
- def free_in_own_client_only(over: dict | None = None) -> dict[str, bool]:
70
- """Provider -> whether its free models are left out: built-in, with this
71
- install's switches over it."""
72
- if over is None:
73
- import endpoints
74
- over = endpoints.connections().get("free_in_own_client_only")
75
- if not isinstance(over, dict):
76
- over = {} # a hand-written list or true: ignored rather than taking Discover down
77
- return {**FREE_IN_OWN_CLIENT_ONLY, **over}
78
- # FreeLLMAPI platform id -> models.dev provider id, where they are the same
79
- # provider under another name (from the 2026-10-17 comparison).
80
- FREELLMAPI_TO_MODELSDEV = {
81
- "cloudflare": "cloudflare-workers-ai", "ollama": "ollama-cloud", "ovh": "ovhcloud",
82
- "radeon": "amd", "zhipu": "zhipuai",
83
- }
84
-
85
-
86
- def fetch(name: str, force: bool = False) -> dict:
87
- """A feed, from the cache when it is under a day old."""
88
- path = CACHE / f"{name}.json"
89
- if not force and path.exists() and time.time() - path.stat().st_mtime < MAX_AGE_S:
90
- return json.loads(path.read_text(encoding="utf-8"))
91
- req = urllib.request.Request(FEEDS[name], headers={"User-Agent": "benchwarmers"})
92
- with urllib.request.urlopen(req, timeout=90) as r:
93
- data = json.load(r)
94
- CACHE.mkdir(parents=True, exist_ok=True)
95
- tmp = path.with_suffix(".part")
96
- tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
97
- tmp.replace(path)
98
- return data
99
-
100
-
101
- def cached(name: str) -> dict:
102
- """The last fetched copy, or {} — for pages that must not wait on the network."""
103
- try:
104
- return json.loads((CACHE / f"{name}.json").read_text(encoding="utf-8"))
105
- except (OSError, ValueError):
106
- return {}
107
-
108
-
109
- def norm_model(model_id: str) -> str:
110
- """Comparable model name: last path segment, no :free, no date/version tail."""
111
- s = model_id.lower().split("/")[-1].removesuffix(":free")
112
- s = re.sub(r"^(llm7|us|eu|apac|global)\.", "", s)
113
- prev = None
114
- while prev != s:
115
- prev = s
116
- # ...and reasoning-effort tails a proxy adds to one model (gemini-3.8-flash-high).
117
- s = re.sub(r"(@[\w.]+|-v\d+:\d+|-\d{4}-\d{2}-\d{2}|-\d{8}|-latest|-(?:high|medium|low|minimal))$", "", s)
118
- return s
119
-
120
-
121
- def modelsdev_entry(provider: str, served: str) -> dict | None:
122
- """models.dev's record of `served` under one provider: exact id, else by name."""
123
- models = (cached("modelsdev").get(provider) or {}).get("models") or {}
124
- if served in models:
125
- return models[served]
126
- key = norm_model(served)
127
- return next((m for k, m in models.items() if norm_model(k) == key), None)
128
-
129
-
130
- def modelsdev_anywhere(served: str) -> tuple[str, dict] | None:
131
- key = norm_model(served)
132
- for prov, p in cached("modelsdev").items():
133
- for k, m in (p.get("models") or {}).items():
134
- if norm_model(k) == key:
135
- return prov, m
136
- return None
137
-
138
-
139
- def freellmapi_entry(served: str, platform: str | None = None) -> dict | None:
140
- key = norm_model(served)
141
- for m in cached("freellmapi").get("models") or []:
142
- if norm_model(m.get("modelId", "")) == key and (platform is None or m.get("platform") == platform):
143
- return m
144
- return None
145
-
146
-
147
- def is_free(cost: dict | None) -> bool | None:
148
- if not cost:
149
- return None
150
- vals = [cost.get("input"), cost.get("output")]
151
- return None if all(v is None for v in vals) else all((v or 0) == 0 for v in vals)
152
-
153
-
154
- def main() -> None:
155
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
156
- for name in FEEDS:
157
- try:
158
- data = fetch(name, force=True)
159
- size = len(data) if name == "modelsdev" else len(data.get("models", []))
160
- print(f" {name}: {size} {'providers' if name == 'modelsdev' else 'models'}"
161
- f" at {dt.datetime.now():%H:%M}")
162
- except Exception as exc: # noqa: BLE001
163
- print(f" {name}: could not fetch ({type(exc).__name__}: {exc}); keeping the cached copy")
164
-
165
-
166
- if __name__ == "__main__":
167
- main()
1
+ """Public model lists the bench reads: models.dev and FreeLLMAPI's free catalog.
2
+
3
+ python metrics/sources.py # fetch both now (normally once a day)
4
+
5
+ WHY THESE TWO
6
+ -------------
7
+ A comparison on 2026-10-17 of models.dev, FreeLLMAPI, truefoundry/models,
8
+ ClawLabsAI/free-ai-models and OpenRouter's own list found:
9
+ - models.dev (MIT, anomalyco/models.dev): 232 providers, ~9,000 models, price
10
+ per model per provider, context and output limits, tool calling, reasoning,
11
+ modalities, and each provider's API base URL. OpenRouter's free models are in
12
+ it exactly (19 of 19), so OpenRouter needs no separate pull.
13
+ - FreeLLMAPI's free monthly catalog (MIT project, tashfeenahmed/freellmapi):
14
+ 219 of its 360 free models and 17 of its providers are not in models.dev, and
15
+ it carries rate limits (rpm/rpd/tpm/tpd) and per-model operating notes.
16
+ Only the free monthly tier is read, never the premium feed.
17
+ - truefoundry/models was stale for free models and ClawLabs duplicated
18
+ models.dev's coverage, so neither is pulled; each is a small adapter later.
19
+
20
+ Both are cached in runs/cache/ (gitignored) and refetched at most once a day:
21
+ nothing here is called per request.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import datetime as dt
27
+ import json
28
+ import pathlib
29
+ import re
30
+ import sys
31
+ import time
32
+ import urllib.request
33
+
34
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
35
+ from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
36
+ CACHE = DATA / "runs" / "cache"
37
+ FEEDS = {
38
+ "modelsdev": "https://models.dev/api.json",
39
+ "freellmapi": "https://api.freellmapi.co/v1/latest",
40
+ }
41
+ MAX_AGE_S = 20 * 3600
42
+
43
+ # The bench's connection hosts (route.connection_of) -> models.dev provider id.
44
+ # An install's own names (a proxy fronting one vendor) come from
45
+ # config/connections.json `models_dev`, which wins.
46
+ CONNECTION_TO_MODELSDEV = {
47
+ "nvidia-api": "nvidia", "ollama-cloud": "ollama-cloud", "mistral": "mistral",
48
+ "groq": "groq", "openrouter": "openrouter",
49
+ "api.inceptionlabs.ai": "inception", "api.reflection.ai": "reflection",
50
+ "cerebras": "cerebras",
51
+ }
52
+
53
+
54
+ def connection_to_modelsdev() -> dict[str, str]:
55
+ """The built-in map with this install's own names over it."""
56
+ import endpoints
57
+ return {**CONNECTION_TO_MODELSDEV, **(endpoints.connections().get("models_dev") or {})}
58
+
59
+
60
+ # models.dev providers whose free models answer only inside the provider's own
61
+ # client, so Discover leaves those out; their paid models stay. opencode, tried
62
+ # 2026-10-21 through Open WebUI: "opencode's free tier can only be used from
63
+ # within opencode". opencode-go serves the same free ids, assumed the same.
64
+ # An install switches a provider either way on Settings, which writes
65
+ # config/connections.json `free_in_own_client_only`, e.g. {"opencode": false}.
66
+ FREE_IN_OWN_CLIENT_ONLY = {"opencode": True, "opencode-go": True}
67
+
68
+
69
+ def free_in_own_client_only(over: dict | None = None) -> dict[str, bool]:
70
+ """Provider -> whether its free models are left out: built-in, with this
71
+ install's switches over it."""
72
+ if over is None:
73
+ import endpoints
74
+ over = endpoints.connections().get("free_in_own_client_only")
75
+ if not isinstance(over, dict):
76
+ over = {} # a hand-written list or true: ignored rather than taking Discover down
77
+ return {**FREE_IN_OWN_CLIENT_ONLY, **over}
78
+ # FreeLLMAPI platform id -> models.dev provider id, where they are the same
79
+ # provider under another name (from the 2026-10-17 comparison).
80
+ FREELLMAPI_TO_MODELSDEV = {
81
+ "cloudflare": "cloudflare-workers-ai", "ollama": "ollama-cloud", "ovh": "ovhcloud",
82
+ "radeon": "amd", "zhipu": "zhipuai",
83
+ }
84
+
85
+
86
+ def fetch(name: str, force: bool = False) -> dict:
87
+ """A feed, from the cache when it is under a day old."""
88
+ path = CACHE / f"{name}.json"
89
+ if not force and path.exists() and time.time() - path.stat().st_mtime < MAX_AGE_S:
90
+ return json.loads(path.read_text(encoding="utf-8"))
91
+ req = urllib.request.Request(FEEDS[name], headers={"User-Agent": "benchwarmers"})
92
+ with urllib.request.urlopen(req, timeout=90) as r:
93
+ data = json.load(r)
94
+ CACHE.mkdir(parents=True, exist_ok=True)
95
+ tmp = path.with_suffix(".part")
96
+ tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
97
+ tmp.replace(path)
98
+ return data
99
+
100
+
101
+ def cached(name: str) -> dict:
102
+ """The last fetched copy, or {} — for pages that must not wait on the network."""
103
+ try:
104
+ return json.loads((CACHE / f"{name}.json").read_text(encoding="utf-8"))
105
+ except (OSError, ValueError):
106
+ return {}
107
+
108
+
109
+ def norm_model(model_id: str) -> str:
110
+ """Comparable model name: last path segment, no :free, no date/version tail."""
111
+ s = model_id.lower().split("/")[-1].removesuffix(":free")
112
+ s = re.sub(r"^(llm7|us|eu|apac|global)\.", "", s)
113
+ prev = None
114
+ while prev != s:
115
+ prev = s
116
+ # ...and reasoning-effort tails a proxy adds to one model (gemini-3.8-flash-high).
117
+ s = re.sub(r"(@[\w.]+|-v\d+:\d+|-\d{4}-\d{2}-\d{2}|-\d{8}|-latest|-(?:high|medium|low|minimal))$", "", s)
118
+ return s
119
+
120
+
121
+ def modelsdev_entry(provider: str, served: str) -> dict | None:
122
+ """models.dev's record of `served` under one provider: exact id, else by name."""
123
+ models = (cached("modelsdev").get(provider) or {}).get("models") or {}
124
+ if served in models:
125
+ return models[served]
126
+ key = norm_model(served)
127
+ return next((m for k, m in models.items() if norm_model(k) == key), None)
128
+
129
+
130
+ def modelsdev_anywhere(served: str) -> tuple[str, dict] | None:
131
+ key = norm_model(served)
132
+ for prov, p in cached("modelsdev").items():
133
+ for k, m in (p.get("models") or {}).items():
134
+ if norm_model(k) == key:
135
+ return prov, m
136
+ return None
137
+
138
+
139
+ def freellmapi_entry(served: str, platform: str | None = None) -> dict | None:
140
+ key = norm_model(served)
141
+ for m in cached("freellmapi").get("models") or []:
142
+ if norm_model(m.get("modelId", "")) == key and (platform is None or m.get("platform") == platform):
143
+ return m
144
+ return None
145
+
146
+
147
+ def is_free(cost: dict | None) -> bool | None:
148
+ if not cost:
149
+ return None
150
+ vals = [cost.get("input"), cost.get("output")]
151
+ return None if all(v is None for v in vals) else all((v or 0) == 0 for v in vals)
152
+
153
+
154
+ def main() -> None:
155
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
156
+ for name in FEEDS:
157
+ try:
158
+ data = fetch(name, force=True)
159
+ size = len(data) if name == "modelsdev" else len(data.get("models", []))
160
+ print(f" {name}: {size} {'providers' if name == 'modelsdev' else 'models'}"
161
+ f" at {dt.datetime.now():%H:%M}")
162
+ except Exception as exc: # noqa: BLE001
163
+ print(f" {name}: could not fetch ({type(exc).__name__}: {exc}); keeping the cached copy")
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
package/metrics/wrong.py CHANGED
@@ -1,144 +1,144 @@
1
- """Confidently wrong: how often a model asserts something that is not true.
2
-
3
- python metrics/wrong.py # per model, worst first
4
-
5
- Being wrong is expected; it is why the bench asks several models. Being wrong
6
- WITH CONFIDENCE is what costs: a reviewer that names a bug that is not there
7
- sends someone to fix working code, and three of them agreeing once asserted a
8
- security hole that did not exist. Counted from evidence already on disk, one
9
- opportunity per answer:
10
-
11
- control a seeded or real-bug control (the clean file) answered FAULT
12
- wrong line a seeded or real-bug task answered FAULT at a line the change did not touch
13
- false claim a claim task whose claim is false, answered VERDICT: TRUE
14
- trap a hand-built task's planted falsehood asserted (its must_not_claim)
15
- review a false claim recorded in a verdict (offload.py verdict --false)
16
-
17
- A miss (NO FAULT on a bug, FALSE on a true claim) is not counted: saying
18
- nothing is cheap to check, inventing is not. Drafts and non-answers (rate
19
- limits, errors) are left out. Reviews are reported apart, as false claims
20
- per judged offload, because one review can hold several claims.
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import collections
26
- import json
27
- import pathlib
28
- import re
29
- import sys
30
-
31
- ROOT = pathlib.Path(__file__).resolve().parent.parent
32
- from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
33
- TASKS = DATA / "suite" / "tasks"
34
- sys.path.insert(0, str(ROOT / "metrics"))
35
- sys.path.insert(0, str(ROOT / "suite"))
36
- KINDS = ("control", "wrong line", "false claim", "trap")
37
- # What a seeded or real-bug answer did: on a bug task it found the line, named
38
- # another, or missed it; on a control (the clean file) it said clean or raised
39
- # a false alarm. The Bug hunt chart draws these.
40
- HUNT = ("found", "wrong line", "missed", "clean", "false alarm")
41
- FAULT = re.compile(r"^\W*fault:\s*line\s*(\d+)", re.I)
42
-
43
-
44
- def outcome(spec: dict, text: str) -> str | None:
45
- """One of HUNT for a seeded or real-bug answer, None for any other task."""
46
- if spec.get("shape") not in ("seeded", "rebench"):
47
- return None
48
- first = next((ln.strip() for ln in text.splitlines() if ln.strip()), "")
49
- named = FAULT.match(first)
50
- if spec["id"].endswith("-control"):
51
- return "false alarm" if named else "clean"
52
- label = ((spec.get("must_find") or [{}])[0] or {}).get("label", "")
53
- keys = {int(n) for n in re.findall(r"\d+", label)}
54
- if not keys: # no answer line to compare with
55
- return None
56
- if not named:
57
- return "missed"
58
- return "found" if int(named.group(1)) in keys else "wrong line"
59
-
60
-
61
- def judge(spec: dict, text: str, invented: list) -> tuple[str | None, bool]:
62
- """(kind of opportunity, was it confidently wrong) for one answer, or
63
- (None, False) when the answer could not have been."""
64
- shape = spec.get("shape")
65
- if shape in ("seeded", "rebench"):
66
- o = outcome(spec, text)
67
- if o is None:
68
- return None, False
69
- return ("control", o == "false alarm") if o in ("clean", "false alarm") else ("wrong line", o == "wrong line")
70
- if shape == "claim":
71
- claim_false = any("verdict: false" in str(c).lower() for c in spec.get("must_find", []))
72
- return ("false claim", bool(invented)) if claim_false else (None, False)
73
- if spec.get("must_not_claim"):
74
- return "trap", bool(invented)
75
- return None, False
76
-
77
-
78
- def table() -> dict[str, dict]:
79
- import offload
80
- import run
81
- from aliases import current
82
- out: dict[str, dict] = collections.defaultdict(
83
- lambda: {"answers": 0, "wrong": 0, "by": {k: [0, 0] for k in KINDS}, "reviews": 0, "review_false": 0,
84
- "hunt": dict.fromkeys(HUNT, 0)})
85
- for path in sorted(TASKS.glob("*/task.json")):
86
- spec = json.loads(path.read_text(encoding="utf-8"))
87
- if spec.get("draft"):
88
- continue
89
- for answer in sorted((path.parent / "output").glob("*.md")):
90
- text = answer.read_text(encoding="utf-8", errors="replace")
91
- s = run.score(spec, text)
92
- if s["kind"] != "answered":
93
- continue
94
- kind, wrong = judge(spec, text, s["invented"])
95
- if kind is None:
96
- continue
97
- row = out[current(answer.stem)]
98
- if (o := outcome(spec, text)):
99
- row["hunt"][o] += 1
100
- row["answers"] += 1
101
- row["by"][kind][1] += 1
102
- if wrong:
103
- row["wrong"] += 1
104
- row["by"][kind][0] += 1
105
- for e in offload.entries():
106
- if e.get("useful") is not None:
107
- row = out[e["model"]]
108
- row["reviews"] += 1
109
- row["review_false"] += len(e.get("false") or [])
110
- return dict(out)
111
-
112
-
113
- def _selfcheck() -> None:
114
- control = {"id": "seeded-x-control", "shape": "seeded"}
115
- bug = {"id": "seeded-x-bug", "shape": "seeded", "must_find": [{"regex": "", "label": "fault: line 9 or 10"}]}
116
- assert judge(control, "FAULT: line 4\nbecause", []) == ("control", True)
117
- assert judge(control, "NO FAULT", []) == ("control", False)
118
- assert judge(bug, "FAULT: line 10", []) == ("wrong line", False)
119
- assert judge(bug, "FAULT: line 100", []) == ("wrong line", True)
120
- assert judge(bug, "NO FAULT", ["no fault"]) == ("wrong line", False) # a miss, not invention
121
- false_claim = {"shape": "claim", "must_find": [{"any_of": ["verdict: false"]}]}
122
- true_claim = {"shape": "claim", "must_find": [{"any_of": ["verdict: true"]}]}
123
- assert judge(false_claim, "VERDICT: TRUE", ["verdict: true"]) == ("false claim", True)
124
- assert judge(true_claim, "VERDICT: FALSE", ["verdict: false"]) == (None, False)
125
- assert judge({"must_not_claim": ["INSERT"]}, "x", ["INSERT"]) == ("trap", True)
126
- assert judge({"must_find": []}, "x", []) == (None, False)
127
- assert judge({"id": "seeded-y-bug", "shape": "seeded"}, "FAULT: line 3", []) == (None, False)
128
- assert [outcome(bug, t) for t in ("FAULT: line 9", "FAULT: line 1", "NO FAULT")] == ["found", "wrong line", "missed"]
129
- assert [outcome(control, t) for t in ("NO FAULT", "FAULT: line 2")] == ["clean", "false alarm"]
130
- assert outcome({"shape": "claim"}, "VERDICT: TRUE") is None
131
-
132
-
133
- def main() -> None:
134
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
135
- _selfcheck()
136
- rows = table()
137
- for alias, r in sorted(rows.items(), key=lambda kv: -(kv[1]["wrong"] / kv[1]["answers"] if kv[1]["answers"] else 0)):
138
- rate = f"{r['wrong']}/{r['answers']}" if r["answers"] else "-"
139
- by = ", ".join(f"{k} {w}/{n}" for k, (w, n) in r["by"].items() if n)
140
- print(f" {alias:<34} {rate:>7} {by}" + (f" · reviews: {r['review_false']} false in {r['reviews']}" if r["reviews"] else ""))
141
-
142
-
143
- if __name__ == "__main__":
144
- main()
1
+ """Confidently wrong: how often a model asserts something that is not true.
2
+
3
+ python metrics/wrong.py # per model, worst first
4
+
5
+ Being wrong is expected; it is why the bench asks several models. Being wrong
6
+ WITH CONFIDENCE is what costs: a reviewer that names a bug that is not there
7
+ sends someone to fix working code, and three of them agreeing once asserted a
8
+ security hole that did not exist. Counted from evidence already on disk, one
9
+ opportunity per answer:
10
+
11
+ control a seeded or real-bug control (the clean file) answered FAULT
12
+ wrong line a seeded or real-bug task answered FAULT at a line the change did not touch
13
+ false claim a claim task whose claim is false, answered VERDICT: TRUE
14
+ trap a hand-built task's planted falsehood asserted (its must_not_claim)
15
+ review a false claim recorded in a verdict (offload.py verdict --false)
16
+
17
+ A miss (NO FAULT on a bug, FALSE on a true claim) is not counted: saying
18
+ nothing is cheap to check, inventing is not. Drafts and non-answers (rate
19
+ limits, errors) are left out. Reviews are reported apart, as false claims
20
+ per judged offload, because one review can hold several claims.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import collections
26
+ import json
27
+ import pathlib
28
+ import re
29
+ import sys
30
+
31
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
32
+ from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
33
+ TASKS = DATA / "suite" / "tasks"
34
+ sys.path.insert(0, str(ROOT / "metrics"))
35
+ sys.path.insert(0, str(ROOT / "suite"))
36
+ KINDS = ("control", "wrong line", "false claim", "trap")
37
+ # What a seeded or real-bug answer did: on a bug task it found the line, named
38
+ # another, or missed it; on a control (the clean file) it said clean or raised
39
+ # a false alarm. The Bug hunt chart draws these.
40
+ HUNT = ("found", "wrong line", "missed", "clean", "false alarm")
41
+ FAULT = re.compile(r"^\W*fault:\s*line\s*(\d+)", re.I)
42
+
43
+
44
+ def outcome(spec: dict, text: str) -> str | None:
45
+ """One of HUNT for a seeded or real-bug answer, None for any other task."""
46
+ if spec.get("shape") not in ("seeded", "rebench"):
47
+ return None
48
+ first = next((ln.strip() for ln in text.splitlines() if ln.strip()), "")
49
+ named = FAULT.match(first)
50
+ if spec["id"].endswith("-control"):
51
+ return "false alarm" if named else "clean"
52
+ label = ((spec.get("must_find") or [{}])[0] or {}).get("label", "")
53
+ keys = {int(n) for n in re.findall(r"\d+", label)}
54
+ if not keys: # no answer line to compare with
55
+ return None
56
+ if not named:
57
+ return "missed"
58
+ return "found" if int(named.group(1)) in keys else "wrong line"
59
+
60
+
61
+ def judge(spec: dict, text: str, invented: list) -> tuple[str | None, bool]:
62
+ """(kind of opportunity, was it confidently wrong) for one answer, or
63
+ (None, False) when the answer could not have been."""
64
+ shape = spec.get("shape")
65
+ if shape in ("seeded", "rebench"):
66
+ o = outcome(spec, text)
67
+ if o is None:
68
+ return None, False
69
+ return ("control", o == "false alarm") if o in ("clean", "false alarm") else ("wrong line", o == "wrong line")
70
+ if shape == "claim":
71
+ claim_false = any("verdict: false" in str(c).lower() for c in spec.get("must_find", []))
72
+ return ("false claim", bool(invented)) if claim_false else (None, False)
73
+ if spec.get("must_not_claim"):
74
+ return "trap", bool(invented)
75
+ return None, False
76
+
77
+
78
+ def table() -> dict[str, dict]:
79
+ import offload
80
+ import run
81
+ from aliases import current
82
+ out: dict[str, dict] = collections.defaultdict(
83
+ lambda: {"answers": 0, "wrong": 0, "by": {k: [0, 0] for k in KINDS}, "reviews": 0, "review_false": 0,
84
+ "hunt": dict.fromkeys(HUNT, 0)})
85
+ for path in sorted(TASKS.glob("*/task.json")):
86
+ spec = json.loads(path.read_text(encoding="utf-8"))
87
+ if spec.get("draft"):
88
+ continue
89
+ for answer in sorted((path.parent / "output").glob("*.md")):
90
+ text = answer.read_text(encoding="utf-8", errors="replace")
91
+ s = run.score(spec, text)
92
+ if s["kind"] != "answered":
93
+ continue
94
+ kind, wrong = judge(spec, text, s["invented"])
95
+ if kind is None:
96
+ continue
97
+ row = out[current(answer.stem)]
98
+ if (o := outcome(spec, text)):
99
+ row["hunt"][o] += 1
100
+ row["answers"] += 1
101
+ row["by"][kind][1] += 1
102
+ if wrong:
103
+ row["wrong"] += 1
104
+ row["by"][kind][0] += 1
105
+ for e in offload.entries():
106
+ if e.get("useful") is not None:
107
+ row = out[e["model"]]
108
+ row["reviews"] += 1
109
+ row["review_false"] += len(e.get("false") or [])
110
+ return dict(out)
111
+
112
+
113
+ def _selfcheck() -> None:
114
+ control = {"id": "seeded-x-control", "shape": "seeded"}
115
+ bug = {"id": "seeded-x-bug", "shape": "seeded", "must_find": [{"regex": "", "label": "fault: line 9 or 10"}]}
116
+ assert judge(control, "FAULT: line 4\nbecause", []) == ("control", True)
117
+ assert judge(control, "NO FAULT", []) == ("control", False)
118
+ assert judge(bug, "FAULT: line 10", []) == ("wrong line", False)
119
+ assert judge(bug, "FAULT: line 100", []) == ("wrong line", True)
120
+ assert judge(bug, "NO FAULT", ["no fault"]) == ("wrong line", False) # a miss, not invention
121
+ false_claim = {"shape": "claim", "must_find": [{"any_of": ["verdict: false"]}]}
122
+ true_claim = {"shape": "claim", "must_find": [{"any_of": ["verdict: true"]}]}
123
+ assert judge(false_claim, "VERDICT: TRUE", ["verdict: true"]) == ("false claim", True)
124
+ assert judge(true_claim, "VERDICT: FALSE", ["verdict: false"]) == (None, False)
125
+ assert judge({"must_not_claim": ["INSERT"]}, "x", ["INSERT"]) == ("trap", True)
126
+ assert judge({"must_find": []}, "x", []) == (None, False)
127
+ assert judge({"id": "seeded-y-bug", "shape": "seeded"}, "FAULT: line 3", []) == (None, False)
128
+ assert [outcome(bug, t) for t in ("FAULT: line 9", "FAULT: line 1", "NO FAULT")] == ["found", "wrong line", "missed"]
129
+ assert [outcome(control, t) for t in ("NO FAULT", "FAULT: line 2")] == ["clean", "false alarm"]
130
+ assert outcome({"shape": "claim"}, "VERDICT: TRUE") is None
131
+
132
+
133
+ def main() -> None:
134
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
135
+ _selfcheck()
136
+ rows = table()
137
+ for alias, r in sorted(rows.items(), key=lambda kv: -(kv[1]["wrong"] / kv[1]["answers"] if kv[1]["answers"] else 0)):
138
+ rate = f"{r['wrong']}/{r['answers']}" if r["answers"] else "-"
139
+ by = ", ".join(f"{k} {w}/{n}" for k, (w, n) in r["by"].items() if n)
140
+ print(f" {alias:<34} {rate:>7} {by}" + (f" · reviews: {r['review_false']} false in {r['reviews']}" if r["reviews"] else ""))
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()