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.
@@ -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
+ )