@wendellresearch-ai/benchwarmers 0.2.0 → 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 -272
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -314
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -163
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -74
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -755
- package/launcher.py +120 -0
- package/mcp_server.py +266 -266
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +295 -295
- package/metrics/client.py +394 -394
- 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 -579
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -65
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -106
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +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 -81
- package/scripts/test_mcp.py +178 -178
- package/scripts/test_merged.py +214 -214
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -438
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3764
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1042
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -425
- package/ui/bench.js +1756 -1576
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
package/metrics/sources.py
CHANGED
|
@@ -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()
|