@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.
Files changed (103) hide show
  1. package/AGENTS.md +152 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +415 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +408 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +186 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +674 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +939 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  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 +304 -295
  22. package/metrics/client.py +804 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +171 -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/formats.py +1021 -0
  30. package/metrics/guard.py +647 -592
  31. package/metrics/learn.py +234 -221
  32. package/metrics/ledger.py +148 -0
  33. package/metrics/litellm_hub.py +206 -206
  34. package/metrics/model_roles.py +94 -84
  35. package/metrics/offload.py +166 -166
  36. package/metrics/owui.py +205 -205
  37. package/metrics/pacing.py +248 -0
  38. package/metrics/paths.py +60 -60
  39. package/metrics/quota.py +383 -0
  40. package/metrics/route.py +581 -579
  41. package/metrics/scheduler.py +275 -275
  42. package/metrics/sources.py +167 -167
  43. package/metrics/wrong.py +144 -144
  44. package/package.json +69 -65
  45. package/perf/README.md +89 -89
  46. package/perf/run.py +284 -261
  47. package/scripts/auto_retire.py +367 -367
  48. package/scripts/check.py +113 -106
  49. package/scripts/check_deprecations.py +378 -378
  50. package/scripts/check_themes.py +103 -103
  51. package/scripts/init.py +124 -124
  52. package/scripts/list_served.py +78 -78
  53. package/scripts/notice.py +78 -78
  54. package/scripts/onboard.py +410 -331
  55. package/scripts/provider_tags.py +129 -129
  56. package/scripts/reprefix.py +136 -136
  57. package/scripts/sync_config.py +105 -105
  58. package/scripts/test_auth.py +255 -255
  59. package/scripts/test_cli.py +189 -0
  60. package/scripts/test_client.py +277 -233
  61. package/scripts/test_connect.py +222 -222
  62. package/scripts/test_first_run.py +180 -180
  63. package/scripts/test_formats.py +549 -0
  64. package/scripts/test_hubs.py +328 -328
  65. package/scripts/test_launcher.py +67 -0
  66. package/scripts/test_llm_log.py +85 -81
  67. package/scripts/test_mcp.py +178 -178
  68. package/scripts/test_merged.py +215 -214
  69. package/scripts/test_no_owui.py +291 -291
  70. package/scripts/test_owui.py +90 -90
  71. package/scripts/test_quota.py +334 -0
  72. package/scripts/test_serve_edit.py +453 -438
  73. package/scripts/test_server_json.py +79 -0
  74. package/scripts/test_verdict.py +102 -98
  75. package/scripts/test_vision.py +231 -228
  76. package/serve.py +3972 -3764
  77. package/suite/mine.py +154 -154
  78. package/suite/pace.py +145 -145
  79. package/suite/run.py +1046 -1042
  80. package/suite/seed.py +421 -421
  81. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  82. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  83. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  84. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  85. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  86. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  87. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  88. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  89. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  90. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  91. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  92. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  93. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  94. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  95. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  96. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  97. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  98. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  99. package/ui/bench.css +430 -425
  100. package/ui/bench.js +1757 -1576
  101. package/ui/themes/dark.css +35 -35
  102. package/ui/themes/hardwood.css +52 -52
  103. 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()