opencode-models 0.1.0__py3-none-any.whl
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.
- opencode_models/__init__.py +8 -0
- opencode_models/__main__.py +5 -0
- opencode_models/arena.py +108 -0
- opencode_models/auth.py +48 -0
- opencode_models/availability.py +102 -0
- opencode_models/benchmarks.py +19 -0
- opencode_models/boards.py +518 -0
- opencode_models/cli.py +1172 -0
- opencode_models/data.py +82 -0
- opencode_models/fetch.py +101 -0
- opencode_models/identity.py +100 -0
- opencode_models/labelmap.py +84 -0
- opencode_models/live.py +88 -0
- opencode_models/overrides.py +252 -0
- opencode_models/pareto.py +34 -0
- opencode_models/pricing.py +415 -0
- opencode_models/ranking.py +591 -0
- opencode_models/scoring.py +230 -0
- opencode_models/seed.py +184 -0
- opencode_models/sources/go.txt +45 -0
- opencode_models/sources/zen.txt +87 -0
- opencode_models/sources.py +55 -0
- opencode_models/tables.py +94 -0
- opencode_models-0.1.0.dist-info/METADATA +544 -0
- opencode_models-0.1.0.dist-info/RECORD +28 -0
- opencode_models-0.1.0.dist-info/WHEEL +4 -0
- opencode_models-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_models-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Official benchmark boards fetched directly, as machine-readable records.
|
|
2
|
+
|
|
3
|
+
The console's axes used to come from one aggregator's transcription — 93%
|
|
4
|
+
vendor self-report. These fetchers go to whoever actually runs each board:
|
|
5
|
+
the official Terminal-Bench 4.0 board (out of the board page's own data
|
|
6
|
+
payload, with the agent and reasoning effort recorded per row) and
|
|
7
|
+
swebench.com's embedded leaderboard JSON. Each row carries its source URL and
|
|
8
|
+
tier so the merge can prefer an official number over a self-reported one and
|
|
9
|
+
report the disagreement.
|
|
10
|
+
|
|
11
|
+
Sources deliberately *not* fetched: Artificial Analysis, Vals AI, Scale's
|
|
12
|
+
SWE-Atlas board and DeepSWE render their numbers in a browser (probed: app
|
|
13
|
+
shells, JS-only tables, no data API) — a stdlib fetch gets labels without
|
|
14
|
+
scores. aider's polyglot table *is* static HTML, but its board stops at the
|
|
15
|
+
GPT-5 generation (top run Aug 2025, nothing from the Claude 5.x / GPT-6 /
|
|
16
|
+
Grok 4.7 era), and diff-editing is already covered by SWE-bench and
|
|
17
|
+
Terminal-Bench — stale and redundant, so it is left alone.
|
|
18
|
+
|
|
19
|
+
tbench.ai *looked* like one of those dead ends — the page renders nothing
|
|
20
|
+
without JavaScript — but a Next.js page streams the data it renders *from*,
|
|
21
|
+
and that payload is the whole leaderboard. Worth remembering before writing
|
|
22
|
+
off a JS-rendered source: check whether the data is embedded before
|
|
23
|
+
concluding it is unreachable. (The harness repo's submission files parse
|
|
24
|
+
more easily and are what this module fetched first; they turned out to be a
|
|
25
|
+
stale, deduplicated copy — 21 files against the board's 35 rows, missing
|
|
26
|
+
Grok 4.7 and GPT-6 Astra, one effort per model.)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
from collections import Counter
|
|
34
|
+
from dataclasses import asdict, dataclass, replace
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from .fetch import http_get
|
|
38
|
+
from .labelmap import mapped_id
|
|
39
|
+
|
|
40
|
+
#: Tier of a row that comes from the benchmark's own official board.
|
|
41
|
+
TIER_BOARD = 2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class BoardRow:
|
|
46
|
+
"""One benchmark board entry: a model's score on a board's own terms."""
|
|
47
|
+
|
|
48
|
+
benchmark: str
|
|
49
|
+
model_name: str
|
|
50
|
+
score: float
|
|
51
|
+
source: str
|
|
52
|
+
tier: int = TIER_BOARD
|
|
53
|
+
model_org: str | None = None
|
|
54
|
+
agent: str | None = None
|
|
55
|
+
effort: str | None = None
|
|
56
|
+
date: str | None = None
|
|
57
|
+
note: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _label(field) -> str | None:
|
|
61
|
+
"""A display field is {"url": ..., "label": ...} or a bare string."""
|
|
62
|
+
if isinstance(field, dict):
|
|
63
|
+
return field.get("label")
|
|
64
|
+
return field if isinstance(field, str) else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _island_json(html: str, marker: str) -> list:
|
|
68
|
+
"""Parse a ``<script type="application/json" id=marker>`` island."""
|
|
69
|
+
start = html.find(marker)
|
|
70
|
+
if start < 0:
|
|
71
|
+
return []
|
|
72
|
+
open_tag = html.index(">", start) + 1
|
|
73
|
+
end = html.index("</script>", open_tag)
|
|
74
|
+
try:
|
|
75
|
+
return json.loads(html[open_tag:end].strip())
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cells(row_html: str) -> list[str]:
|
|
81
|
+
"""The text of a table row's cells, blanks dropped."""
|
|
82
|
+
cells = [
|
|
83
|
+
re.sub(r"<[^>]+>", "", c).strip()
|
|
84
|
+
for c in re.findall(r"<t[dh][^>]*>(.*?)</t[dh]>", row_html, re.S)
|
|
85
|
+
]
|
|
86
|
+
return [c for c in cells if c]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
# Terminal-Bench 4.0 — the official board, out of the board page's payload.
|
|
91
|
+
#
|
|
92
|
+
# The page renders nothing without JavaScript, which is what made it look
|
|
93
|
+
# like a dead end; but Next.js ships the dehydrated data the page renders
|
|
94
|
+
# *from*, and that payload holds the whole leaderboard: the schema and every
|
|
95
|
+
# row, with each reasoning effort ranked separately.
|
|
96
|
+
#
|
|
97
|
+
# The harness repo's leaderboard/submissions/ looks like the same data in
|
|
98
|
+
# plain JSON, and was the first thing fetched here — it is a stale,
|
|
99
|
+
# deduplicated copy: one submission per model at one effort, missing rows
|
|
100
|
+
# the board already has (Grok 4.7, GPT-6 Astra). The page is the authority.
|
|
101
|
+
# --------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
TB_BENCHMARK = "Terminal-Bench 4.0"
|
|
104
|
+
TB_PAGE = "https://www.tbench.ai/leaderboard/terminal-bench/4.0"
|
|
105
|
+
|
|
106
|
+
#: Next.js streams the page's data as ``self.__next_f.push([1, "…"])`` with
|
|
107
|
+
#: the payload JSON-escaped inside a JS string literal.
|
|
108
|
+
_FLIGHT = re.compile(r'self\.__next_f\.push\(\[1,\s*"(.*?)"\]\)', re.S)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _flight_stream(html: str) -> str:
|
|
112
|
+
"""The page's RSC payload, unescaped.
|
|
113
|
+
|
|
114
|
+
The stream arrives as several ``push`` calls and a boundary can land in
|
|
115
|
+
the middle of an escape sequence, so the raw bodies are joined *before*
|
|
116
|
+
unescaping — unescaping each chunk on its own throws the chunk (and
|
|
117
|
+
everything after it) away.
|
|
118
|
+
"""
|
|
119
|
+
bodies = _FLIGHT.findall(html or "")
|
|
120
|
+
if not bodies:
|
|
121
|
+
return ""
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(f'"{"".join(bodies)}"')
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _first_object(text: str, start: int) -> str:
|
|
129
|
+
"""The balanced ``{…}`` beginning at *start*, or "" when unbalanced."""
|
|
130
|
+
depth = 0
|
|
131
|
+
for i in range(start, len(text)):
|
|
132
|
+
if text[i] == "{":
|
|
133
|
+
depth += 1
|
|
134
|
+
elif text[i] == "}":
|
|
135
|
+
depth -= 1
|
|
136
|
+
if depth == 0:
|
|
137
|
+
return text[start : i + 1]
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def tbench_board(html: str) -> dict | None:
|
|
142
|
+
"""The dehydrated leaderboard the page was rendered from.
|
|
143
|
+
|
|
144
|
+
Looks for the query data holding a Terminal-Bench board: an object with
|
|
145
|
+
a ``leaderboard`` and its ``rows``. A page without one (an error page, a
|
|
146
|
+
redesign, JS-only) yields None rather than an empty board, so a broken
|
|
147
|
+
fetch is visible instead of quietly emptying the axis.
|
|
148
|
+
"""
|
|
149
|
+
payload = _flight_stream(html)
|
|
150
|
+
if not payload:
|
|
151
|
+
return None
|
|
152
|
+
needle = payload.find('{"leaderboard"')
|
|
153
|
+
while needle != -1:
|
|
154
|
+
blob = _first_object(payload, needle)
|
|
155
|
+
if blob:
|
|
156
|
+
try:
|
|
157
|
+
data = json.loads(blob)
|
|
158
|
+
except json.JSONDecodeError:
|
|
159
|
+
data = None
|
|
160
|
+
if isinstance(data, dict) and isinstance(data.get("rows"), list):
|
|
161
|
+
return data
|
|
162
|
+
needle = payload.find('{"leaderboard"', needle + 1)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def tbench_row(entry: dict) -> BoardRow | None:
|
|
167
|
+
"""One board row: score, model, agent, effort, date, rank and CI95.
|
|
168
|
+
|
|
169
|
+
A row without an accuracy is skipped rather than guessed at — the board
|
|
170
|
+
would have no score for it either.
|
|
171
|
+
"""
|
|
172
|
+
metrics = entry.get("metrics") or {}
|
|
173
|
+
accuracy = metrics.get("accuracy")
|
|
174
|
+
if not isinstance(accuracy, (int, float)):
|
|
175
|
+
return None
|
|
176
|
+
meta = entry.get("metadata") or {}
|
|
177
|
+
model_name = _label(meta.get("model_display"))
|
|
178
|
+
if not model_name:
|
|
179
|
+
return None
|
|
180
|
+
bits = []
|
|
181
|
+
if entry.get("rank"):
|
|
182
|
+
bits.append(f"rank {entry['rank']}")
|
|
183
|
+
if meta.get("date"):
|
|
184
|
+
bits.append(meta["date"])
|
|
185
|
+
if metrics.get("accuracy_ci95_half_width"):
|
|
186
|
+
bits.append(f"±{metrics['accuracy_ci95_half_width']} CI95")
|
|
187
|
+
if metrics.get("n_trials"):
|
|
188
|
+
bits.append(f"{metrics['n_trials']} trials")
|
|
189
|
+
return BoardRow(
|
|
190
|
+
benchmark=TB_BENCHMARK,
|
|
191
|
+
model_name=model_name,
|
|
192
|
+
score=float(accuracy),
|
|
193
|
+
source=TB_PAGE,
|
|
194
|
+
tier=TIER_BOARD,
|
|
195
|
+
model_org=_label(meta.get("model_org")),
|
|
196
|
+
agent=_label(meta.get("agent_display")),
|
|
197
|
+
effort=meta.get("reasoning_effort"),
|
|
198
|
+
date=meta.get("date"),
|
|
199
|
+
note=", ".join(bits) or None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def tbench_page_rows(html: str) -> list[BoardRow]:
|
|
204
|
+
"""Every row of the official Terminal-Bench 4.0 board in *html*."""
|
|
205
|
+
data = tbench_board(html)
|
|
206
|
+
if data is None:
|
|
207
|
+
return []
|
|
208
|
+
rows = [tbench_row(entry) for entry in data["rows"] if isinstance(entry, dict)]
|
|
209
|
+
return [r for r in rows if r is not None]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def fetch_tbench(get=http_get) -> list[BoardRow]:
|
|
213
|
+
"""Fetch the official Terminal-Bench 4.0 board."""
|
|
214
|
+
return tbench_page_rows(get(TB_PAGE))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# --------------------------------------------------------------------------
|
|
218
|
+
# SWE-bench — the official site embeds its whole board as a JSON island.
|
|
219
|
+
# --------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
SWE_URL = "https://www.swebench.com/"
|
|
222
|
+
#: The subsets that are benchmark families; "Test" is their internal subset.
|
|
223
|
+
SWE_SUBSETS = ("Verified", "Multilingual", "Multimodal", "Lite")
|
|
224
|
+
_SWE_MARKER = 'id="leaderboard-data"'
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def swebench_rows(html: str) -> list[BoardRow]:
|
|
228
|
+
"""Board rows from swebench.com's embedded leaderboard JSON.
|
|
229
|
+
|
|
230
|
+
Only *checked* entries count: the board is submissions plus their
|
|
231
|
+
verification status, and an unchecked submission is a claim. Rows where
|
|
232
|
+
the model itself took system-level actions are kept but noted — an
|
|
233
|
+
os-model run is not the same task as a patch-submitting agent.
|
|
234
|
+
"""
|
|
235
|
+
rows: list[BoardRow] = []
|
|
236
|
+
for subset in _island_json(html, _SWE_MARKER):
|
|
237
|
+
name = subset.get("name")
|
|
238
|
+
if name not in SWE_SUBSETS:
|
|
239
|
+
continue
|
|
240
|
+
for entry in subset.get("results") or []:
|
|
241
|
+
score = entry.get("resolved")
|
|
242
|
+
if not entry.get("checked") or not isinstance(score, (int, float)):
|
|
243
|
+
continue
|
|
244
|
+
notes = []
|
|
245
|
+
if entry.get("os_model") or entry.get("os_system"):
|
|
246
|
+
notes.append("os-model/system run")
|
|
247
|
+
rows.append(
|
|
248
|
+
BoardRow(
|
|
249
|
+
benchmark=f"SWE-Bench {name}",
|
|
250
|
+
model_name=entry.get("model_display") or "",
|
|
251
|
+
score=float(score),
|
|
252
|
+
source=SWE_URL,
|
|
253
|
+
tier=TIER_BOARD,
|
|
254
|
+
model_org=entry.get("model_org"),
|
|
255
|
+
agent=entry.get("agent"),
|
|
256
|
+
date=entry.get("date"),
|
|
257
|
+
note=", ".join(notes) or None,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
return rows
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def fetch_swebench(get=http_get) -> list[BoardRow]:
|
|
264
|
+
"""Fetch swebench.com's official leaderboards."""
|
|
265
|
+
return swebench_rows(get(SWE_URL))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def normalize(text: str) -> Counter:
|
|
269
|
+
"""A comparable token multiset: lowercase, punctuation split, counts kept.
|
|
270
|
+
|
|
271
|
+
"Opus 5.5" and "opus-5-5" both become ``{"opus": 1, "5": 2}`` — counts
|
|
272
|
+
matter, or 5.5 and 5 are the same model. "Claude 4.6 Opus" and
|
|
273
|
+
"claude-opus-4-6" also meet, despite the word order.
|
|
274
|
+
"""
|
|
275
|
+
return Counter(t for t in re.split(r"[^a-z0-9]+", (text or "").lower()) if t)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _id_tokens(model_id: str) -> Counter:
|
|
279
|
+
return normalize(model_id.replace("/", " "))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
#: opencode marks the canonical row of a model with "(latest)" and keeps the
|
|
283
|
+
#: dated snapshot beside it. The marker says nothing about which model it is.
|
|
284
|
+
LATEST = re.compile(r"\blatest\b", re.I)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _name_tokens(model) -> Counter:
|
|
288
|
+
"""A model's display name as comparable tokens, marker dropped."""
|
|
289
|
+
return normalize(LATEST.sub(" ", model.name))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def match_models(row: BoardRow, models) -> list[str]:
|
|
293
|
+
"""Every catalog row the board row is about.
|
|
294
|
+
|
|
295
|
+
Boards and the catalog name the same models differently, so this is a
|
|
296
|
+
mapping rather than a comparison, in two steps.
|
|
297
|
+
|
|
298
|
+
First the catalog's own naming: a row whose display name says the same
|
|
299
|
+
thing, once the "(latest)" marker is dropped. That keeps "Claude 4
|
|
300
|
+
Sonnet" on Claude Sonnet 4 instead of reaching for 4.6, and recognises
|
|
301
|
+
``claude-sonnet-4-0`` and ``claude-sonnet-4-20250514`` as the one model
|
|
302
|
+
they are — ids that share no shape.
|
|
303
|
+
|
|
304
|
+
Failing that, tokens shared with the id, where the id's prefix is the
|
|
305
|
+
provider and is compared against the row's org: a lab that matches beats
|
|
306
|
+
one that does not, even at the cost of an extra token.
|
|
307
|
+
|
|
308
|
+
More than one result means the catalog holds the same model twice, which
|
|
309
|
+
is not ambiguity. Empty means the candidates are genuinely different
|
|
310
|
+
models — a different version, or the same name under a different
|
|
311
|
+
provider. A wrong merge is worse than a missing one.
|
|
312
|
+
"""
|
|
313
|
+
want = normalize(row.model_name)
|
|
314
|
+
if not want:
|
|
315
|
+
return []
|
|
316
|
+
_CATALOG[0] = list(models)
|
|
317
|
+
|
|
318
|
+
# a decision someone already made for this service wins outright
|
|
319
|
+
recorded = mapped_id(row.source, row.model_name)
|
|
320
|
+
if recorded:
|
|
321
|
+
hit = [m for m in models if m.model_id == recorded]
|
|
322
|
+
if hit:
|
|
323
|
+
return _all_rows_of(hit)
|
|
324
|
+
|
|
325
|
+
org = normalize(row.model_org or "")
|
|
326
|
+
|
|
327
|
+
by_name = [m for m in models if _name_tokens(m) == want]
|
|
328
|
+
if by_name:
|
|
329
|
+
return _one_model(by_name)
|
|
330
|
+
|
|
331
|
+
scored: list[tuple[int, int, object]] = []
|
|
332
|
+
for model in models:
|
|
333
|
+
tokens = _id_tokens(model.model_id) | _name_tokens(model)
|
|
334
|
+
if want - tokens: # a token (or count) the model lacks
|
|
335
|
+
continue
|
|
336
|
+
lab_miss = 0 if org and (org - normalize(model.lab)) == Counter() else 1
|
|
337
|
+
scored.append((len(tokens - want), lab_miss, model))
|
|
338
|
+
if not scored:
|
|
339
|
+
return []
|
|
340
|
+
scored.sort(key=lambda t: (t[0], t[1]))
|
|
341
|
+
best = scored[0][:2]
|
|
342
|
+
return _one_model([m for extra, lab, m in scored if (extra, lab) == best])
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _all_rows_of(model: list) -> list[str]:
|
|
346
|
+
"""Every catalog row of the model, not just the canonical one.
|
|
347
|
+
|
|
348
|
+
A recorded mapping names the canonical id; the dated snapshot beside it
|
|
349
|
+
is the same model and gets the same score.
|
|
350
|
+
"""
|
|
351
|
+
if not model:
|
|
352
|
+
return []
|
|
353
|
+
target = model[0]
|
|
354
|
+
same = [
|
|
355
|
+
m
|
|
356
|
+
for m in _CATALOG[0]
|
|
357
|
+
if m.lab == target.lab and _name_tokens(m) == _name_tokens(target)
|
|
358
|
+
]
|
|
359
|
+
return sorted(
|
|
360
|
+
(m.model_id for m in same or model),
|
|
361
|
+
key=lambda c: (0 if LATEST.search(c) else 1, c),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
#: the catalog currently being matched against, so _all_rows_of can find the
|
|
366
|
+
#: snapshot rows beside the one a mapping names
|
|
367
|
+
_CATALOG: list[list] = [[]]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _one_model(candidates: list) -> list[str]:
|
|
371
|
+
"""The ids of *candidates* when they are one model, else nothing.
|
|
372
|
+
|
|
373
|
+
They must agree on provider and on name — the prefix is what tells
|
|
374
|
+
``anthropic/claude-sonnet-5-5`` from ``otherproxy/claude-sonnet-5-5``.
|
|
375
|
+
The "(latest)" row sorts first: it is the canonical id, and the one an
|
|
376
|
+
override file is named after.
|
|
377
|
+
"""
|
|
378
|
+
if len({c.lab for c in candidates}) > 1:
|
|
379
|
+
return []
|
|
380
|
+
# Counter is unhashable, so compare the token sets as sorted items
|
|
381
|
+
if len({tuple(sorted(_name_tokens(c).items())) for c in candidates}) > 1:
|
|
382
|
+
return []
|
|
383
|
+
return sorted(
|
|
384
|
+
(c.model_id for c in candidates),
|
|
385
|
+
key=lambda c: (0 if LATEST.search(c) else 1, c),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def match_model(row: BoardRow, models) -> str | None:
|
|
390
|
+
"""The one catalog model a board row is about, or None when ambiguous.
|
|
391
|
+
|
|
392
|
+
See :func:`match_models`, which returns every row when the catalog holds
|
|
393
|
+
the same model twice.
|
|
394
|
+
"""
|
|
395
|
+
found = match_models(row, models)
|
|
396
|
+
return found[0] if found else None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def merge_boards(models, rows: list[BoardRow]):
|
|
400
|
+
"""Apply official board rows onto models, preferring the better source.
|
|
401
|
+
|
|
402
|
+
A board row replaces a model's score only when its source is *strictly*
|
|
403
|
+
better tiered than what the model already carries: an official board
|
|
404
|
+
beats a vendor's own number (Opus 5.5's claimed 66.4 on TB 4.0 yields to
|
|
405
|
+
the board's 64.8), but it does not displace an independent run. Every
|
|
406
|
+
disagreement is returned as a conflict so `conflicts` can show it.
|
|
407
|
+
|
|
408
|
+
Returns the models and the conflicts; models with no board row are
|
|
409
|
+
returned unchanged.
|
|
410
|
+
"""
|
|
411
|
+
from .overrides import Conflict, source_tier
|
|
412
|
+
|
|
413
|
+
by_model: dict[str, list[BoardRow]] = {}
|
|
414
|
+
for row in rows:
|
|
415
|
+
# every row of the model, not one of them: the catalog keeps a dated
|
|
416
|
+
# snapshot beside the canonical row and a board does not know that
|
|
417
|
+
for model_id in match_models(row, models):
|
|
418
|
+
by_model.setdefault(model_id, []).append(row)
|
|
419
|
+
|
|
420
|
+
conflicts: list[Conflict] = []
|
|
421
|
+
out = []
|
|
422
|
+
for model in models:
|
|
423
|
+
rows_for_model = by_model.get(model.model_id)
|
|
424
|
+
if not rows_for_model:
|
|
425
|
+
out.append(model)
|
|
426
|
+
continue
|
|
427
|
+
scores = dict(model.scores)
|
|
428
|
+
sources = dict(model.sources)
|
|
429
|
+
for row in sorted(rows_for_model, key=lambda r: -r.score):
|
|
430
|
+
key = row.benchmark
|
|
431
|
+
# a board row applies only when its source is strictly better
|
|
432
|
+
# tiered than what the model carries — the board never
|
|
433
|
+
# displaces an independent run, and never a same-tier number
|
|
434
|
+
better = key not in scores or source_tier(row.source) < source_tier(
|
|
435
|
+
sources.get(key)
|
|
436
|
+
)
|
|
437
|
+
if key in scores and scores[key] != row.score:
|
|
438
|
+
conflicts.append(
|
|
439
|
+
replace(
|
|
440
|
+
Conflict(
|
|
441
|
+
model_id=model.model_id,
|
|
442
|
+
benchmark=key,
|
|
443
|
+
upstream=scores[key],
|
|
444
|
+
override=row.score,
|
|
445
|
+
upstream_source=sources.get(key),
|
|
446
|
+
override_source=row.source,
|
|
447
|
+
reason=(
|
|
448
|
+
f"official board ({row.agent or 'board'}"
|
|
449
|
+
+ (f", {row.effort}" if row.effort else "")
|
|
450
|
+
+ (f", {row.note}" if row.note else "")
|
|
451
|
+
+ ")"
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
applied=better,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
if not better:
|
|
458
|
+
continue
|
|
459
|
+
scores[key] = row.score
|
|
460
|
+
sources[key] = row.source
|
|
461
|
+
out.append(replace(model, scores=scores, sources=sources))
|
|
462
|
+
conflicts.sort(key=lambda c: (c.model_id, c.benchmark))
|
|
463
|
+
return out, conflicts
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def fetch_boards(get=http_get, progress=None) -> dict[str, list[BoardRow]]:
|
|
467
|
+
"""Fetch every board source, keyed by source name.
|
|
468
|
+
|
|
469
|
+
A source that fails is skipped, not fatal: the boards are independent
|
|
470
|
+
of each other and of the registry, so one dead source leaves the rest.
|
|
471
|
+
*progress* is an optional ``(done, total, description)`` callback,
|
|
472
|
+
fired before each source is attempted.
|
|
473
|
+
"""
|
|
474
|
+
out = {}
|
|
475
|
+
sources = list(SOURCES)
|
|
476
|
+
for done, (name, fetch, description) in enumerate(sources, 1):
|
|
477
|
+
if progress is not None:
|
|
478
|
+
progress(done, len(sources), description)
|
|
479
|
+
try:
|
|
480
|
+
out[name] = fetch(get)
|
|
481
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
482
|
+
continue
|
|
483
|
+
return out
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def save_boards(directory, boards: dict[str, list[BoardRow]]) -> list[str]:
|
|
487
|
+
"""Write each source's rows to ``<dir>/<name>.json``."""
|
|
488
|
+
directory = Path(directory)
|
|
489
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
490
|
+
saved = []
|
|
491
|
+
for name, rows in boards.items():
|
|
492
|
+
path = directory / f"{name}.json"
|
|
493
|
+
path.write_text(json.dumps([asdict(r) for r in rows], indent=1))
|
|
494
|
+
saved.append(path.name)
|
|
495
|
+
return saved
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def load_boards(directory) -> list[BoardRow]:
|
|
499
|
+
"""Every cached board row from ``<dir>/*.json``; absent is not an error."""
|
|
500
|
+
rows: list[BoardRow] = []
|
|
501
|
+
for path in sorted(Path(directory).glob("*.json")):
|
|
502
|
+
try:
|
|
503
|
+
payload = json.loads(path.read_text())
|
|
504
|
+
except (json.JSONDecodeError, OSError):
|
|
505
|
+
continue
|
|
506
|
+
for row in payload if isinstance(payload, list) else []:
|
|
507
|
+
try:
|
|
508
|
+
rows.append(BoardRow(**row))
|
|
509
|
+
except TypeError:
|
|
510
|
+
continue
|
|
511
|
+
return rows
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
#: Every board source, in the order `fetch --boards` walks them.
|
|
515
|
+
SOURCES = (
|
|
516
|
+
("tbench", fetch_tbench, "Terminal-Bench 4.0 board from www.tbench.ai"),
|
|
517
|
+
("swebench", fetch_swebench, "SWE-bench boards from www.swebench.com"),
|
|
518
|
+
)
|