@wendellresearch-ai/benchwarmers 0.6.2 → 0.6.4
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 +3 -1
- package/BENCHMARKS.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +4 -6
- package/VERSION +1 -1
- package/package.json +1 -1
- package/scripts/check.py +1 -0
- package/scripts/clients_doc.py +98 -0
- package/scripts/provider_tags.py +19 -3
- package/scripts/test_no_owui.py +8 -0
- package/serve.py +130 -248
- package/ui/bench.css +37 -11
- package/ui/bench.js +355 -118
package/serve.py
CHANGED
|
@@ -39,6 +39,7 @@ import datetime
|
|
|
39
39
|
import functools
|
|
40
40
|
import html
|
|
41
41
|
import json
|
|
42
|
+
import math
|
|
42
43
|
import os
|
|
43
44
|
import pathlib
|
|
44
45
|
import re
|
|
@@ -2253,27 +2254,6 @@ def open_count(groups: dict) -> int:
|
|
|
2253
2254
|
return sum(len(g["rows"]) + len(g["held"]) for g in groups.values())
|
|
2254
2255
|
|
|
2255
2256
|
|
|
2256
|
-
def and_list(parts: list[str]) -> str:
|
|
2257
|
-
return " and ".join(parts) if len(parts) < 3 else ", ".join(parts[:-1]) + " and " + parts[-1]
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
def actions_line(groups: dict) -> str:
|
|
2261
|
-
"""At a glance's one line about Action needed, when anything is open:
|
|
2262
|
-
what is waiting and how soon, and the way to its page."""
|
|
2263
|
-
rows = [r for g in groups.values() for r in g["rows"]]
|
|
2264
|
-
held = sum(len(g["held"]) for g in groups.values())
|
|
2265
|
-
if not rows and not held:
|
|
2266
|
-
return ""
|
|
2267
|
-
models = sum(1 for r in rows if r["alias"])
|
|
2268
|
-
what = and_list([f"{n} {word}{'s' * (n != 1)}" for n, word in
|
|
2269
|
-
((models, "model"), (held, "connection"), (len(rows) - models, "notice")) if n])
|
|
2270
|
-
soon = min(r["days"] for r in rows) if rows else 99
|
|
2271
|
-
many = sum(r["days"] == soon for r in rows)
|
|
2272
|
-
when = (f"; {many} retire{'s' * (many == 1)} in {soon} day{'s' * (soon != 1)}" if soon < 99 else "")
|
|
2273
|
-
return (f"<p class='a-line'><strong>Action needed</strong> <span>{what}{when}.</span>"
|
|
2274
|
-
" <button type='button' class='linkish' data-go='needs'>Review →</button></p>")
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
2257
|
def actions_page(groups: dict, auto: list) -> str:
|
|
2278
2258
|
"""Action needed, its own page since 2026-11-02: it sat above every page
|
|
2279
2259
|
and grew to a screen and more of red. Grouped by connection, because
|
|
@@ -2372,167 +2352,6 @@ def price_of(alias: str, served: str, f: dict) -> str:
|
|
|
2372
2352
|
return "paid" if sources.is_free(f.get("cost")) is False else "unknown"
|
|
2373
2353
|
|
|
2374
2354
|
|
|
2375
|
-
def facet(name: str, label: str, values) -> str:
|
|
2376
|
-
"""A filter by one column's value, beside a panel's search box (bench.js
|
|
2377
|
-
matches it against each row's data-<name>)."""
|
|
2378
|
-
opts = "".join(f"<option value='{esc(v)}'>{esc(v)}</option>" for v in sorted(set(values) - {""}))
|
|
2379
|
-
return (f"<label>{esc(label)} <select data-facet='{esc(name)}'><option value=''>any</option>"
|
|
2380
|
-
f"{opts}</select></label>")
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
def facts_cells(alias: str, f: dict | None = None) -> str:
|
|
2384
|
-
"""Context window and capabilities, from metrics/catalog.py; a value you
|
|
2385
|
-
entered is marked, and an unknown one says so rather than looking empty."""
|
|
2386
|
-
f = f or catalog.info(alias)
|
|
2387
|
-
ctx = f.get("context_window")
|
|
2388
|
-
src = f["sources"].get("context_window", "")
|
|
2389
|
-
ctx_cell = (f"<td class='num' data-sort='{ctx}' title='{esc(src)}'>"
|
|
2390
|
-
f"{ctx // 1000:,}k{'*' if src == 'you' else ''}</td>" if ctx
|
|
2391
|
-
else "<td class='num muted' data-sort='-1' title='unknown: fill it in the model popup'>?</td>")
|
|
2392
|
-
caps = [CAP_LABELS[c] for c in catalog.CAPS if f.get(c)]
|
|
2393
|
-
return ctx_cell + (f"<td class='caps'>{' '.join(chip(c, '') for c in caps)}</td>" if caps
|
|
2394
|
-
else "<td class='muted'>—</td>")
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
def roster() -> str:
|
|
2398
|
-
"""Every model on one table: its state, what it has done, what it scored.
|
|
2399
|
-
|
|
2400
|
-
Replaces a grid of offload cards that answered only "what was it used
|
|
2401
|
-
for". Status first, because the first question on opening this page is
|
|
2402
|
-
whether anything is broken; real work second; benchmarks after."""
|
|
2403
|
-
try:
|
|
2404
|
-
from offload import summary
|
|
2405
|
-
work = summary()
|
|
2406
|
-
except Exception: # noqa: BLE001
|
|
2407
|
-
work = {}
|
|
2408
|
-
try:
|
|
2409
|
-
import auto_retire
|
|
2410
|
-
rows, answers, _ = auto_retire.evidence()
|
|
2411
|
-
except Exception: # noqa: BLE001
|
|
2412
|
-
rows, answers = [], {}
|
|
2413
|
-
last = dict(answers)
|
|
2414
|
-
for r in rows:
|
|
2415
|
-
if r["answered"]:
|
|
2416
|
-
last[r["alias"]] = max(last.get(r["alias"], r["date"]), r["date"])
|
|
2417
|
-
suite: dict[str, list[int]] = {}
|
|
2418
|
-
for (_task, model), hit in suite_scores().items():
|
|
2419
|
-
if (m := re.match(r"(\d+)/(\d+)", hit["found"])):
|
|
2420
|
-
got = suite.setdefault(model, [0, 0, 0])
|
|
2421
|
-
got[0] += int(m.group(1)); got[1] += int(m.group(2)); got[2] += 1
|
|
2422
|
-
tiny = {r["model"]: float(r["p50_s"]) for r in perf_rows()
|
|
2423
|
-
if r["prompt"] == "tiny" and int(r.get("answered") or 0) > 0}
|
|
2424
|
-
status = model_status()
|
|
2425
|
-
new = recently_onboarded()
|
|
2426
|
-
stuck = onboard_issues(status)
|
|
2427
|
-
directory = model_directory()
|
|
2428
|
-
severity = {"err": 0, "warn": 1, "": 2, "ok": 3}
|
|
2429
|
-
|
|
2430
|
-
up = uptime_rows()["models"]
|
|
2431
|
-
body = ["<table><thead><tr>", th("Model"), th("Status"), th("Uptime 7d", True), th("Role"),
|
|
2432
|
-
th("Context", True), th("Can"),
|
|
2433
|
-
th("Offloads", True), th("Helped"), th("Suite", True),
|
|
2434
|
-
th("Reply time", True), th("Last answer"), "</tr></thead><tbody>"]
|
|
2435
|
-
# The model browser: each row says its connection, status, role and price
|
|
2436
|
-
# for the toolbar's filters, and the filters offer what the rows hold.
|
|
2437
|
-
offered: dict[str, list[str]] = {"connection": [], "status": [], "role": [], "price": []}
|
|
2438
|
-
|
|
2439
|
-
def browse(**values) -> str:
|
|
2440
|
-
for k, v in values.items():
|
|
2441
|
-
offered[k].append(v)
|
|
2442
|
-
return "".join(f" data-{k}='{esc(v)}'" for k, v in values.items())
|
|
2443
|
-
for alias in sorted(status, key=lambda a: (-work.get(a, {}).get("offloads", 0), a)):
|
|
2444
|
-
label, css, why = status[alias]
|
|
2445
|
-
f = catalog.info(alias)
|
|
2446
|
-
served = directory.get(alias, {}).get("served") or retired().get(alias, {}).get("served", "")
|
|
2447
|
-
w = work.get(alias, {})
|
|
2448
|
-
judged, useful = w.get("judged", 0), w.get("useful", 0)
|
|
2449
|
-
if judged:
|
|
2450
|
-
rate = useful / judged
|
|
2451
|
-
helped = (f"<span class='helped'><span class='meter'><span"
|
|
2452
|
-
f"{' class=low' if rate < .5 else ''} style='width:{max(4, round(rate * 100))}%'>"
|
|
2453
|
-
f"</span></span><span class='mono'>{useful}/{judged}</span></span>")
|
|
2454
|
-
else:
|
|
2455
|
-
rate, helped = -1, "<span class='muted'>not judged</span>"
|
|
2456
|
-
s = suite.get(alias)
|
|
2457
|
-
suite_cell = (f"<td class='num' data-sort='{s[0] / s[1]:.3f}'"
|
|
2458
|
-
f" title='{s[2]} task{'s' * (s[2] != 1)}'>{s[0]}/{s[1]}</td>"
|
|
2459
|
-
if s and s[1] else "<td class='num muted' data-sort='-1'>—</td>")
|
|
2460
|
-
p50 = tiny.get(alias)
|
|
2461
|
-
seen = last.get(alias)
|
|
2462
|
-
role = directory.get(alias, {}).get("role") or retired().get(alias, {}).get("role", "")
|
|
2463
|
-
conn = catalog.connection_of(alias)
|
|
2464
|
-
if conn == "?": # retired: no registration left to say, but its record does
|
|
2465
|
-
conn = retired().get(alias, {}).get("tag", "").split("/")[0] or "unknown"
|
|
2466
|
-
attrs = browse(connection=conn, status=re.sub(r" in \d+d$", "", label),
|
|
2467
|
-
role=role, price=price_of(alias, served, f))
|
|
2468
|
-
body.append(
|
|
2469
|
-
f"{tr(alias, attrs)}<td>{named(alias)}</td>"
|
|
2470
|
-
f"<td data-sort='{severity[css]}'>{chip(label, css, why)}"
|
|
2471
|
-
+ (" " + chip("new", "new", "onboarded this week") if alias in new else "")
|
|
2472
|
-
+ (" " + chip("onboard stopped", stuck[alias]["css"],
|
|
2473
|
-
f"onboarding stopped at the {stuck[alias].get('step', 'probe')}:"
|
|
2474
|
-
f" {stuck[alias]['label']}. Open the model for the error.")
|
|
2475
|
-
if alias in stuck else "")
|
|
2476
|
-
+ "</td>" + uptime_cell(up.get(alias)) + f"<td>{esc(role)}</td>{facts_cells(alias, f)}"
|
|
2477
|
-
f"<td class='num'>{w.get('offloads', 0)}</td>"
|
|
2478
|
-
f"<td data-sort='{rate:.3f}'>{helped}</td>{suite_cell}"
|
|
2479
|
-
+ (f"<td class='num' data-sort='{p50}'>{p50:.1f}s</td>" if p50 is not None
|
|
2480
|
-
else "<td class='num muted' data-sort='99999'>—</td>")
|
|
2481
|
-
+ (f"<td class='mono' data-sort='{seen}'>{seen}</td>" if seen
|
|
2482
|
-
else "<td class='muted' data-sort='0'>never</td>")
|
|
2483
|
-
+ "</tr>")
|
|
2484
|
-
# Decision models are not in the llm yaml, so no probe, suite or reply time
|
|
2485
|
-
# applies; the role column says what they are and what they answer.
|
|
2486
|
-
for alias, d in sorted(decide.load().items()):
|
|
2487
|
-
w = work.get(alias, {})
|
|
2488
|
-
body.append(
|
|
2489
|
-
"<tr" + browse(connection=d.get("via", ""), status="no probe", role="decision",
|
|
2490
|
-
price="free" if ":free" in d["served"] else "unknown") + ">"
|
|
2491
|
-
f"<td>{named(alias)}</td>"
|
|
2492
|
-
"<td data-sort='2'><span class='muted' title='decision models have no"
|
|
2493
|
-
" availability probe yet'>—</span>"
|
|
2494
|
-
+ (" " + chip("new", "new", "onboarded this week") if d.get("onboarded", "") >=
|
|
2495
|
-
(datetime.date.today() - datetime.timedelta(days=7)).isoformat() else "")
|
|
2496
|
-
+ "</td>" + uptime_cell(up.get(alias)) + f"<td>decision · {esc(', '.join(d['question_types']))}</td>"
|
|
2497
|
-
"<td class='num muted' data-sort='-1'>—</td><td class='muted'>—</td>"
|
|
2498
|
-
f"<td class='num'>{w.get('offloads', 0)}</td>"
|
|
2499
|
-
"<td data-sort='-1'><span class='muted'>not judged</span></td>"
|
|
2500
|
-
"<td class='num muted' data-sort='-1'>—</td><td class='num muted' data-sort='99999'>—</td>"
|
|
2501
|
-
"<td class='muted' data-sort='0'>—</td></tr>")
|
|
2502
|
-
body.append("</tbody></table>")
|
|
2503
|
-
key = """<dl>
|
|
2504
|
-
<dt>Status</dt><dd>From the latest probe. <em>healthy</em> answered;
|
|
2505
|
-
<em>throttled</em> and <em>slow</em> are rate limits and timeouts, which
|
|
2506
|
-
never retire a model inside 7 days; <em>retiring in Nd</em> is the
|
|
2507
|
-
countdown before the bench retires it on its own. Hover a chip for the
|
|
2508
|
-
provider's own words.</dd>
|
|
2509
|
-
<dt>Uptime 7d</dt><dd>Of its calls in the last 7 days that say anything about the provider,
|
|
2510
|
-
the share answered: busy, unreachable and timed out count against it; our own quota,
|
|
2511
|
-
pacing and setup problems are left out. Benchmarks has it a day at a time.</dd>
|
|
2512
|
-
<dt>Offloads, Helped</dt><dd>Real work handed over, and how much of what
|
|
2513
|
-
was judged actually helped. Benchmarks never count here.</dd>
|
|
2514
|
-
<dt>Suite</dt><dd>Verified findings located across the internal tasks:
|
|
2515
|
-
the tier that decides routing.</dd>
|
|
2516
|
-
<dt>Reply time</dt><dd>Median seconds to answer a one-word prompt: the
|
|
2517
|
-
serving path's overhead, not the model's worth.</dd>
|
|
2518
|
-
<dt>Last answer</dt><dd>The latest real answer from any source: probe,
|
|
2519
|
-
suite or a useful offload. Seven days without one retires a model.</dd>
|
|
2520
|
-
<dt>decision</dt><dd>A decision model: it answers typed questions (yes/no,
|
|
2521
|
-
choice, score) with probabilities through a Decisions API, never chat.
|
|
2522
|
-
The role column lists the question types it takes. No probe, suite or
|
|
2523
|
-
reply time applies to it.</dd>
|
|
2524
|
-
<dt>Filters</dt><dd>Connection is where a model's calls go (its Open WebUI
|
|
2525
|
-
tag, or its own endpoint). Price: <em>free</em> for a <code>:free</code>
|
|
2526
|
-
slug, a local model, a list price of 0 or a free-tier listing;
|
|
2527
|
-
<em>paid</em> for a subscription or a list price; else unknown. Combine
|
|
2528
|
-
them with the search box.</dd>
|
|
2529
|
-
</dl>"""
|
|
2530
|
-
facets = "".join(facet(k, k.title(), v) for k, v in offered.items())
|
|
2531
|
-
return card("At a glance", "Every model on the bench: whether it works right "
|
|
2532
|
-
"now, what it has been used for, and what it scored.",
|
|
2533
|
-
"".join(body), key, facets)
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
2355
|
def on_bench() -> set[str]:
|
|
2537
2356
|
"""Aliases registered now: what the charts draw."""
|
|
2538
2357
|
return set(registered().values())
|
|
@@ -2639,7 +2458,7 @@ def charts_data() -> dict:
|
|
|
2639
2458
|
"rate": int(hit[1]) / max(1, int(hit[2])) if hit else None, "invented": bool(v["invented"])})
|
|
2640
2459
|
bars = [{"model": m, "kind": k, "rate": w / r["answers"], "n": f"{w}/{n}"}
|
|
2641
2460
|
for m, r in sorted(wr.items()) if m in live and r["answers"] for k, (w, n) in r["by"].items() if w]
|
|
2642
|
-
totals = {m: {"rate": r["wrong"] / r["answers"], "answers": r["answers"]}
|
|
2461
|
+
totals = {m: {"rate": r["wrong"] / r["answers"], "answers": r["answers"], "wrong": r["wrong"]}
|
|
2643
2462
|
for m, r in wr.items() if m in live and r["answers"]}
|
|
2644
2463
|
hunt = [{"model": m, **r["hunt"]} for m, r in sorted(wr.items()) if m in live and any(r["hunt"].values())]
|
|
2645
2464
|
return {"overview": overview, "heat": heat, "families": ["hand-built", "claims", "seeded, function",
|
|
@@ -2900,16 +2719,10 @@ def vision_card() -> str:
|
|
|
2900
2719
|
|
|
2901
2720
|
|
|
2902
2721
|
def perf_charts() -> dict:
|
|
2903
|
-
"""
|
|
2904
|
-
|
|
2722
|
+
"""Speed on real tasks against how much a model finds, for the Performance
|
|
2723
|
+
panel of Benchmarks. (Whether probes answered is Uptime's.)"""
|
|
2905
2724
|
import route
|
|
2906
2725
|
live = on_bench()
|
|
2907
|
-
since = (datetime.date.today() - datetime.timedelta(days=14)).isoformat()
|
|
2908
|
-
outcomes: dict[tuple[str, str], int] = {}
|
|
2909
|
-
for r in perf_rows():
|
|
2910
|
-
if r["model"] in live and r.get("timestamp", "") >= since:
|
|
2911
|
-
o = "answered" if int(r.get("answered") or 0) > 0 else (r.get("failure") or "error")
|
|
2912
|
-
outcomes[(r["model"], o)] = outcomes.get((r["model"], o), 0) + 1
|
|
2913
2726
|
seconds: dict[str, list[float]] = {}
|
|
2914
2727
|
for (_task, m), v in suite_scores().items():
|
|
2915
2728
|
if m in live and re.match(r"\d+/\d+", v["found"]):
|
|
@@ -2917,8 +2730,7 @@ def perf_charts() -> dict:
|
|
|
2917
2730
|
recall = route.suite_recall()
|
|
2918
2731
|
speed = [{"model": m, "seconds": sorted(s)[len(s) // 2], "recall": recall[m][0] / recall[m][1], "tasks": len(s)}
|
|
2919
2732
|
for m, s in sorted(seconds.items()) if recall.get(m) and recall[m][1]]
|
|
2920
|
-
return {"
|
|
2921
|
-
"speed": speed}
|
|
2733
|
+
return {"speed": speed}
|
|
2922
2734
|
|
|
2923
2735
|
|
|
2924
2736
|
# ---- Trend: does a model score lower, or higher, than it used to? ----------
|
|
@@ -3384,15 +3196,6 @@ def _uptime_window(cells: list, days: int) -> dict:
|
|
|
3384
3196
|
"median": seconds[len(seconds) // 2] if seconds else None}
|
|
3385
3197
|
|
|
3386
3198
|
|
|
3387
|
-
def uptime_cell(cells: list | None, days: int = UPTIME_DAYS[0]) -> str:
|
|
3388
|
-
"""At a glance's Uptime column: answered of counted over the last `days`."""
|
|
3389
|
-
t = _uptime_window(cells or [], days)
|
|
3390
|
-
if t["pct"] is None:
|
|
3391
|
-
return "<td class='num muted' data-sort='-1' title='no counted calls'>—</td>"
|
|
3392
|
-
return (f"<td class='num{' err' if t['pct'] < .8 else ''}' data-sort='{t['pct']:.4f}'"
|
|
3393
|
-
f" title='{t['up']} of {t['up'] + t['down']} answered'>{t['pct'] * 100:.1f}%</td>")
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
3199
|
def _why(kinds: dict, which) -> str:
|
|
3397
3200
|
return ", ".join(f"{k} {n}" for k, n in sorted(kinds.items(), key=lambda kv: -kv[1]) if which(k))
|
|
3398
3201
|
|
|
@@ -3545,9 +3348,15 @@ DECIDE_KIND = {"errors": "choice of 6", "collection": "choice of the collections
|
|
|
3545
3348
|
def decision_data() -> dict:
|
|
3546
3349
|
"""For bench.js: each model's calibration bins per set, [said, right, n]."""
|
|
3547
3350
|
res = decide.results()
|
|
3548
|
-
|
|
3351
|
+
models = sorted((res.get("models") or {}).items())
|
|
3352
|
+
return {"sets": [[s, DECIDE_SETS[s]] for s in decide.SETS], "low": decide.MIN_ITEMS,
|
|
3353
|
+
# Right of asked per model and set: the bars, as the tables count it.
|
|
3354
|
+
"scores": [{"model": a, "set": s, "correct": r.get("correct", 0), "asked": r["asked"],
|
|
3355
|
+
"answered": r.get("answered", 0), "brier": r.get("brier")}
|
|
3356
|
+
for a, by in models for s, r in by.items()
|
|
3357
|
+
if s in decide.SETS and isinstance(r, dict) and r.get("asked")],
|
|
3549
3358
|
"points": [{"model": a, "set": s, "said": b["said"], "right": b["right"], "n": b["n"]}
|
|
3550
|
-
for a, by in
|
|
3359
|
+
for a, by in models for s, r in by.items()
|
|
3551
3360
|
if s in decide.SETS for b in r.get("calibration") or []]}
|
|
3552
3361
|
|
|
3553
3362
|
|
|
@@ -3614,8 +3423,7 @@ def decision_card() -> str:
|
|
|
3614
3423
|
extra = (f"<h3 class='chart-head'>Who each use asks now</h3><ul class='links'>{''.join(uses)}</ul>"
|
|
3615
3424
|
f"<p class='hint'>{'Measured up to ' + esc(when[:16].replace('T', ' ')) + '.' if when else 'Not measured yet.'}"
|
|
3616
3425
|
" The scheduler's <code>decisions</code> job asks each model one question a set a day;"
|
|
3617
|
-
" <code>python metrics/decide.py bench</code> runs a pass by hand.</p>"
|
|
3618
|
-
"<h3 class='chart-head'>Calibration: said against right</h3>")
|
|
3426
|
+
" <code>python metrics/decide.py bench</code> runs a pass by hand.</p>")
|
|
3619
3427
|
key = f"""<dl>
|
|
3620
3428
|
<dt>The sets</dt><dd>Questions with known answers, built from the bench's own data, no hand labelling.
|
|
3621
3429
|
<b>Error classes</b>: failed calls' error text (redacted: no keys, ids or emails) and the class the
|
|
@@ -3627,8 +3435,12 @@ def decision_card() -> str:
|
|
|
3627
3435
|
picked, so always saying the commonest option scores low.</dd>
|
|
3628
3436
|
<dt>Brier</dt><dd>How far its probabilities sat from the truth: 0 is perfect; for a choice it is
|
|
3629
3437
|
summed over the options, so up to 2. Lower is better, and a sure wrong answer costs most.</dd>
|
|
3630
|
-
<dt>
|
|
3631
|
-
|
|
3438
|
+
<dt>The bars</dt><dd>Right of asked for each model on each set, from 0%. A faint bar is a model
|
|
3439
|
+
under {low} items on that set: shown, not ranked yet. Hover a bar for its Brier.</dd>
|
|
3440
|
+
<dt>Calibration</dt><dd>The second chart, once a model answers at two or more levels of sureness
|
|
3441
|
+
on a set: answers grouped by the probability the model gave its answer
|
|
3442
|
+
(x), against how often those answers were right (y); both axes start just under the lowest
|
|
3443
|
+
answer, not at 0%. On the diagonal, its 80% answers come true
|
|
3632
3444
|
80% of the time; under it, it is surer than it should be. A bigger dot is more answers. The gap is
|
|
3633
3445
|
the average distance from the diagonal, weighted by answers.</dd>
|
|
3634
3446
|
<dt>At {decide.CONFIDENT}</dt><dd>The gateway acts on a choice only when the model is this sure: how
|
|
@@ -3669,26 +3481,45 @@ def bughunt_card() -> str:
|
|
|
3669
3481
|
grey said there was no bug.</dd>
|
|
3670
3482
|
<dt>Left of the line</dt><dd>The same files untouched: red is a false alarm, claiming a
|
|
3671
3483
|
bug in clean code. The longer the red, the less to trust its "there's a bug here".</dd>
|
|
3672
|
-
<dt>Order</dt><dd>
|
|
3484
|
+
<dt>Order</dt><dd>On what is proven, best at the top: the low end of the found share's likely
|
|
3485
|
+
range (95%) less the high end of the false alarms'. A few tasks give wide ranges, so they
|
|
3486
|
+
rank low; a model never shown clean code could be raising alarms on all of it.</dd>
|
|
3487
|
+
<dt>Faint bars</dt><dd>Under 10 tasks: shown, but little to go on.</dd>
|
|
3673
3488
|
</dl>"""
|
|
3674
3489
|
return chart_card("Bug hunt", "Every task with an answer key: bugs found against false alarms on the"
|
|
3675
3490
|
" same files left clean. Right is good, left is bad.", "chart-hunt", "".join(body), key)
|
|
3676
3491
|
|
|
3677
3492
|
|
|
3493
|
+
def _wilson(k: int, n: int, z: float = 1.96) -> tuple[float, float]:
|
|
3494
|
+
"""The 95% Wilson interval of k successes in n, as bench.js wilson() draws it."""
|
|
3495
|
+
if not n:
|
|
3496
|
+
return 0.0, 1.0
|
|
3497
|
+
p, z2 = k / n, z * z
|
|
3498
|
+
mid, half = p + z2 / (2 * n), z * math.sqrt(p * (1 - p) / n + z2 / (4 * n * n))
|
|
3499
|
+
return max(0.0, (mid - half) / (1 + z2 / n)), min(1.0, (mid + half) / (1 + z2 / n))
|
|
3500
|
+
|
|
3501
|
+
|
|
3678
3502
|
def work_card() -> str:
|
|
3679
3503
|
"""How often real offloads helped."""
|
|
3680
3504
|
rows = work_rows()
|
|
3681
3505
|
body = ["<table><thead><tr>", th("Model"), th("Offloads", True), th("Judged", True), th("Helped", True),
|
|
3682
|
-
th("Verified findings", True), th("False claims", True), "</tr></thead><tbody>"]
|
|
3683
|
-
for r in sorted(rows, key=lambda r: -r["useful"]
|
|
3506
|
+
th("Likely between", True), th("Verified findings", True), th("False claims", True), "</tr></thead><tbody>"]
|
|
3507
|
+
for r in sorted(rows, key=lambda r: (-_wilson(r["useful"], r["judged"])[0], -r["judged"])):
|
|
3508
|
+
lo, hi = _wilson(r["useful"], r["judged"])
|
|
3684
3509
|
body.append(f"<tr><td>{named(r['model'])}</td><td class='num'>{r['offloads']}</td><td class='num'>{r['judged']}</td>"
|
|
3685
3510
|
f"<td class='num' data-sort='{r['useful'] / r['judged']:.3f}'>{r['useful']}/{r['judged']}</td>"
|
|
3511
|
+
f"<td class='num' data-sort='{lo:.3f}'>{lo * 100:.0f}–{hi * 100:.0f}%</td>"
|
|
3686
3512
|
f"<td class='num'>{r['real']}</td><td class='num'>{r['false']}</td></tr>")
|
|
3687
3513
|
body.append("</tbody></table>")
|
|
3688
3514
|
key = """<dl>
|
|
3689
3515
|
<dt>Helped</dt><dd>Of the offloads judged, the share recorded useful: real work (reviews,
|
|
3690
3516
|
enumerations) handed to the model and checked afterwards. Benchmarks never count here.</dd>
|
|
3691
|
-
<dt>The
|
|
3517
|
+
<dt>The dot and its line</dt><dd>The dot is the share that helped, bigger for more judged. The
|
|
3518
|
+
line is where the true share likely sits (a 95% Wilson interval): long for 3 of 3, short for
|
|
3519
|
+
40 of 50. Green when even its low end is over half, red when even its high end is under,
|
|
3520
|
+
grey when there are too few to say.</dd>
|
|
3521
|
+
<dt>Order</dt><dd>By the low end of that line, so volume counts: 40 of 50 ranks above 3 of 3.</dd>
|
|
3522
|
+
<dt>The dashed line</dt><dd>Every model's judged offloads together, for comparison.</dd>
|
|
3692
3523
|
<dt>Findings and false claims</dt><dd>Verified one by one in reviews, recorded since 2026-10-17.</dd>
|
|
3693
3524
|
</dl>"""
|
|
3694
3525
|
return chart_card("Real work", "When a model was given real work and the result was checked: how often"
|
|
@@ -3698,7 +3529,8 @@ def work_card() -> str:
|
|
|
3698
3529
|
def overview_card() -> str:
|
|
3699
3530
|
return (f"<div class='panel'>{SHARE}<h2>Who to trust with a review</h2><p class='sub'>Each dot is a model."
|
|
3700
3531
|
" Right: finds more of the real bugs in our own code. Up: makes fewer things up (confidently"
|
|
3701
|
-
" wrong on clean code, wrong lines, false claims, traps). Top right is where a reviewer should be
|
|
3532
|
+
" wrong on clean code, wrong lines, false claims, traps). Top right is where a reviewer should be."
|
|
3533
|
+
" The models in or near the box, and the three that make most up, are named; hover for the rest.</p>"
|
|
3702
3534
|
"<div class='chart' id='chart-overview'></div></div>")
|
|
3703
3535
|
|
|
3704
3536
|
|
|
@@ -3811,6 +3643,10 @@ def wrong_card() -> str:
|
|
|
3811
3643
|
<dt>Reviews</dt><dd>False claims recorded in verdicts (<code>offload.py verdict|add
|
|
3812
3644
|
--false "..."</code>), over judged offloads. Recorded in structured form only since
|
|
3813
3645
|
2026-10-17, so older reviews show none.</dd>
|
|
3646
|
+
<dt>The thin line</dt><dd>Where the true share likely sits (95% range). Long for a few
|
|
3647
|
+
answers: 0 of 2 is not a clean record, and the line shows how bad it could be.</dd>
|
|
3648
|
+
<dt>Order</dt><dd>Worst proven at the top: by the low end of that line. Under 10 answers a
|
|
3649
|
+
model's bar is faint.</dd>
|
|
3814
3650
|
</dl>"""
|
|
3815
3651
|
return chart_card("Confidently wrong", "Who asserts things that are not true: false alarms on clean"
|
|
3816
3652
|
" code, wrong lines, false claims confirmed, traps taken. Shorter is better.",
|
|
@@ -3874,17 +3710,14 @@ def perf_card() -> str:
|
|
|
3874
3710
|
when it is slow enough to change how you work.</dd>
|
|
3875
3711
|
</dl>"""
|
|
3876
3712
|
key = key.replace("</dl>", """
|
|
3877
|
-
<dt>Did it answer?</dt><dd>
|
|
3878
|
-
rate limit (capacity), a timeout, or a failure the bench classifies (structural, paywalled,
|
|
3879
|
-
entitlement, other).</dd>
|
|
3713
|
+
<dt>Did it answer?</dt><dd>Is on Uptime, by day, with our own quota and setup problems left out.</dd>
|
|
3880
3714
|
<dt>Speed vs quality</dt><dd>Median seconds per answer on our own suite tasks (real prompts,
|
|
3881
|
-
not the one-word probe) against the share of findings located. Top left is fast and good
|
|
3715
|
+
not the one-word probe) against the share of findings located. Top left is fast and good:
|
|
3716
|
+
the green box is the faster half and the top quarter of findings.</dd>
|
|
3882
3717
|
</dl>""")
|
|
3883
3718
|
return chart_card("Performance", "What the serving path does — not what the models are worth. One"
|
|
3884
3719
|
" reading is weather; the history is climate.", "chart-speed", body, key,
|
|
3885
|
-
extra="<h3 class='chart-head'>
|
|
3886
|
-
"<div class='chart' id='chart-reliability'></div>"
|
|
3887
|
-
"<h3 class='chart-head'>Speed vs quality on our own tasks</h3>")
|
|
3720
|
+
extra="<h3 class='chart-head'>Speed vs quality on our own tasks</h3>")
|
|
3888
3721
|
|
|
3889
3722
|
|
|
3890
3723
|
def favicon(theme: str) -> str:
|
|
@@ -3901,12 +3734,12 @@ def favicon(theme: str) -> str:
|
|
|
3901
3734
|
return f"<link rel='icon' type='image/svg+xml' href='data:image/svg+xml,{quote(svg)}'>"
|
|
3902
3735
|
|
|
3903
3736
|
|
|
3904
|
-
def render(theme: str | None = None, shown: str = "
|
|
3737
|
+
def render(theme: str | None = None, shown: str = "suite") -> str:
|
|
3905
3738
|
with locked.snapshot(): # each file read once a render (metrics/locked.py)
|
|
3906
3739
|
return _page(theme, shown)
|
|
3907
3740
|
|
|
3908
3741
|
|
|
3909
|
-
def _page(theme: str | None, shown: str = "
|
|
3742
|
+
def _page(theme: str | None, shown: str = "suite") -> str:
|
|
3910
3743
|
theme = theme if theme in themes() else saved_theme() # a ?theme preview, else the saved one
|
|
3911
3744
|
return (
|
|
3912
3745
|
f"<!doctype html><html lang='en' data-theme='{esc(theme)}'><meta charset='utf-8'>"
|
|
@@ -4300,13 +4133,21 @@ def collections_tab() -> str:
|
|
|
4300
4133
|
" the paid ones. Click a suggestion or type your own: comma-separated lower-case words, up to 8.</p>"
|
|
4301
4134
|
# Not data-filter: that binds to the whole panel. The same match (any
|
|
4302
4135
|
# text, case-insensitive), on the member list only; the seats show who is ticked.
|
|
4303
|
-
"<label class='field' for='c-member-filter'>Members</label>"
|
|
4136
|
+
"<div class='members-head'><label class='field' for='c-member-filter'>Members</label>"
|
|
4137
|
+
# Model details as they are now (a fact set in a model's popup, a refresh of the
|
|
4138
|
+
# catalog), the ticks and the search kept; also done when a model popup opened from here closes.
|
|
4139
|
+
"<button class='btn small' type='button' id='c-refresh' title='Fetch the models and their details again;"
|
|
4140
|
+
" what is ticked stays ticked'>Refresh model details</button>"
|
|
4141
|
+
"<span class='done' id='c-refresh-out' role='status'></span></div>"
|
|
4304
4142
|
# The panel as it will sit, drawn as the Collections list draws it
|
|
4305
4143
|
# (bench.js recount): a seat per ticked member, explorer seats dashed.
|
|
4306
4144
|
"<div id='c-seats' class='seats' aria-live='polite'></div>"
|
|
4307
4145
|
"<div class='member-find'><input type='search' id='c-member-filter' class='member-filter' spellcheck='false'"
|
|
4308
4146
|
" autocomplete='off' placeholder='Filter by model, provider or served name' aria-controls='c-members'"
|
|
4309
|
-
" aria-describedby='c-count'><select id='c-member-
|
|
4147
|
+
" aria-describedby='c-count'><select id='c-member-show' aria-label='Show' aria-controls='c-members'>"
|
|
4148
|
+
"<option value=''>Every model</option><option value='ticked'>Ticked</option>"
|
|
4149
|
+
"<option value='fits'>Fits the tags</option></select>"
|
|
4150
|
+
"<select id='c-member-provider' aria-label='Provider' aria-controls='c-members'>"
|
|
4310
4151
|
"<option value=''>Every provider</option></select></div>"
|
|
4311
4152
|
"<div id='c-members' class='members' role='group' aria-label='Members'></div>"
|
|
4312
4153
|
"<p class='rules' id='c-count'></p>"
|
|
@@ -4374,9 +4215,10 @@ def collections_tab() -> str:
|
|
|
4374
4215
|
# A grouped sidebar since 2026-10-19 (RELEASE-PLAN "The dashboard"): eight
|
|
4375
4216
|
# pages were too many for a row of tabs. Collections holds the gateway's
|
|
4376
4217
|
# requests under the collections they ran; Benchmarks holds Performance.
|
|
4377
|
-
|
|
4218
|
+
# At a glance (every model's state and uptime) was retired 2026-11-10: Benchmarks,
|
|
4219
|
+
# Uptime and Models said it all again. Benchmarks is the home page.
|
|
4220
|
+
SECTIONS = (("Observe", (("suite", "Benchmarks", "benchmarks_tab"),
|
|
4378
4221
|
("needs", "Action needed", "actions_page"),
|
|
4379
|
-
("suite", "Benchmarks", "benchmarks_tab"),
|
|
4380
4222
|
("quotas", "Quota management", "quotas_page"),
|
|
4381
4223
|
("logs", "Logs", "logs_page"))),
|
|
4382
4224
|
("Gateway", (("endpoints", "Endpoints", "endpoints_page"),
|
|
@@ -4401,7 +4243,9 @@ def collections_and_gateway() -> str:
|
|
|
4401
4243
|
# names an environment variable, or the app asks, or (an MCP header in Zed,
|
|
4402
4244
|
# Cline or goose, which take no variable) KEY_HOLDER stays for the reader.
|
|
4403
4245
|
# Only blocks marked `key` (shell lines, fields typed into an app) get the key
|
|
4404
|
-
# made on the page, swapped in for KEY_HOLDER by bench.js.
|
|
4246
|
+
# made on the page, swapped in for KEY_HOLDER by bench.js. CLIENTS.md is written
|
|
4247
|
+
# from client_setups (scripts/clients_doc.py): after a change here, run it, as
|
|
4248
|
+
# check.py fails while the file differs.
|
|
4405
4249
|
KEY_HOLDER = "<your gateway key>"
|
|
4406
4250
|
KEY_ENV = "BENCHWARMERS_KEY" # the plugin's and the CLI's name for it too
|
|
4407
4251
|
CLIENTS_READ = "2026-11-08"
|
|
@@ -4697,20 +4541,20 @@ def _snip_html(group: str, i: int, s: dict) -> str:
|
|
|
4697
4541
|
def pane(key: str, actions: tuple | None = None) -> str:
|
|
4698
4542
|
"""One tab's page, as its section holds it. The page renders the tab shown
|
|
4699
4543
|
(tabs); bench.js fetches the others from GET /api/pane when first opened."""
|
|
4700
|
-
if key
|
|
4544
|
+
if key == "needs":
|
|
4701
4545
|
groups, auto = actions or open_actions()
|
|
4702
|
-
return actions_page(groups, auto)
|
|
4546
|
+
return actions_page(groups, auto)
|
|
4703
4547
|
return globals()[next(fn for k, _, fn in TABS if k == key)]()
|
|
4704
4548
|
|
|
4705
4549
|
|
|
4706
|
-
def tabs(shown: str = "
|
|
4707
|
-
# Read once a render: Action needed's count in the sidebar, and its page
|
|
4708
|
-
#
|
|
4709
|
-
#
|
|
4710
|
-
#
|
|
4550
|
+
def tabs(shown: str = "suite") -> str:
|
|
4551
|
+
# Read once a render: Action needed's count in the sidebar, and its page when
|
|
4552
|
+
# shown. Only the tab shown is rendered (the bench_tab cookie, set by bench.js):
|
|
4553
|
+
# rendering every one took seconds. The rest are empty until opened (data-lazy).
|
|
4554
|
+
# A tab that is gone (a cookie still saying glance) shows the home page.
|
|
4711
4555
|
actions = open_actions()
|
|
4712
4556
|
count = {"needs": open_count(actions[0])}
|
|
4713
|
-
shown = shown if any(k == shown for k, _, _ in TABS) else "
|
|
4557
|
+
shown = shown if any(k == shown for k, _, _ in TABS) else "suite"
|
|
4714
4558
|
|
|
4715
4559
|
def button(key: str, label: str) -> str:
|
|
4716
4560
|
n = count.get(key)
|
|
@@ -5331,6 +5175,28 @@ def endpoints_page() -> str:
|
|
|
5331
5175
|
"<div class='done' id='l-done'></div></div>")
|
|
5332
5176
|
|
|
5333
5177
|
|
|
5178
|
+
def _reply_cell(by_prompt: dict) -> str:
|
|
5179
|
+
"""Models' Reply: the daily probe's median seconds to a one-word answer (tiny),
|
|
5180
|
+
with the 18 KB prompt's (large) and tokens a second on hover. Was on Benchmarks'
|
|
5181
|
+
Performance, beside whether it answered, which is Uptime's now."""
|
|
5182
|
+
def p50(r):
|
|
5183
|
+
try:
|
|
5184
|
+
return float(r["p50_s"]) if r and int(r.get("answered") or 0) > 0 else None
|
|
5185
|
+
except (KeyError, TypeError, ValueError):
|
|
5186
|
+
return None
|
|
5187
|
+
tiny, large = p50(by_prompt.get("tiny")), p50(by_prompt.get("large"))
|
|
5188
|
+
if tiny is None and large is None:
|
|
5189
|
+
return "<td class='num muted' data-sort='999999'>—</td>"
|
|
5190
|
+
shown = tiny if tiny is not None else large
|
|
5191
|
+
why = "; ".join(b for b in (
|
|
5192
|
+
f"one-word prompt {tiny:.1f}s" if tiny is not None else "",
|
|
5193
|
+
f"18 KB prompt {large:.1f}s" if large is not None else "",
|
|
5194
|
+
f"{(by_prompt.get('large') or {}).get('tokens_per_s')} tok/s"
|
|
5195
|
+
if (by_prompt.get("large") or {}).get("tokens_per_s") else "") if b)
|
|
5196
|
+
return (f"<td class='num' data-sort='{shown:.2f}' title='median, the daily probe: {esc(why)}'>"
|
|
5197
|
+
f"{shown:.1f}s</td>")
|
|
5198
|
+
|
|
5199
|
+
|
|
5334
5200
|
def models_page() -> str:
|
|
5335
5201
|
"""Every model on the bench by the connection it comes from."""
|
|
5336
5202
|
cards = []
|
|
@@ -5344,6 +5210,10 @@ def models_page() -> str:
|
|
|
5344
5210
|
# for what a collection may hold): they are on the bench like any other.
|
|
5345
5211
|
by: dict[str, list[str]] = {}
|
|
5346
5212
|
directory = model_directory()
|
|
5213
|
+
# The daily probe's latest reading per model and prompt (perf/history.csv), for Reply.
|
|
5214
|
+
speed: dict[str, dict] = {}
|
|
5215
|
+
for r in perf_rows():
|
|
5216
|
+
speed.setdefault(r["model"], {})[r.get("prompt", "")] = r
|
|
5347
5217
|
for a in directory:
|
|
5348
5218
|
by.setdefault(catalog.connection_of(a), []).append(a)
|
|
5349
5219
|
for c in ({"name": n, "models": sorted(m)} for n, m in sorted(by.items())):
|
|
@@ -5356,7 +5226,7 @@ def models_page() -> str:
|
|
|
5356
5226
|
kind = (" " + chip("decision", "", "a decision model: answers typed questions, not chat")
|
|
5357
5227
|
if a in decisions else "")
|
|
5358
5228
|
if directory[a].get("ad_hoc"):
|
|
5359
|
-
kind += " " + chip("ad hoc", "", "ad hoc only: never chosen by the bench on its own, only when you"
|
|
5229
|
+
kind += " " + chip("ad hoc", "adhoc", "ad hoc only: never chosen by the bench on its own, only when you"
|
|
5360
5230
|
" name it (a collection member, with, --model)")
|
|
5361
5231
|
rows.append(f"<tr><td><a href='#' class='model' data-alias='{esc(a)}'>"
|
|
5362
5232
|
f"<span class='alias'>{esc(a)}</span></a>{kind}</td>"
|
|
@@ -5364,19 +5234,27 @@ def models_page() -> str:
|
|
|
5364
5234
|
f"<td class='num' data-sort='{f.get('context_window') or 0}'>{num(f.get('context_window'))}</td>"
|
|
5365
5235
|
f"<td class='num' data-sort='{f.get('max_output') or 0}'>{num(f.get('max_output'))}</td>"
|
|
5366
5236
|
f"<td>{esc(', '.join(f.get('input_modalities') or []) or '—')}</td>"
|
|
5237
|
+
+ _reply_cell(speed.get(a, {}))
|
|
5367
5238
|
+ "".join(f"<td class='num'>{num(lim.get(k))}</td>" for k in catalog.LIMIT_KEYS) + "</tr>")
|
|
5368
5239
|
# One column plan for every card, so the cards line up under each other
|
|
5369
5240
|
# (each table sized its own columns, so they drifted from card to card).
|
|
5370
5241
|
cols = "<colgroup><col class='c-model'><col class='c-via'><col class='c-n'><col class='c-n'>" \
|
|
5371
|
-
"<col class='c-input'>" + "<col class='c-lim'>" * len(catalog.LIMIT_KEYS) + "</colgroup>"
|
|
5372
|
-
|
|
5242
|
+
"<col class='c-input'><col class='c-reply'>" + "<col class='c-lim'>" * len(catalog.LIMIT_KEYS) + "</colgroup>"
|
|
5243
|
+
# Each connection folds; the page's one box filters them all (bench.js applyFilter, data-conn).
|
|
5244
|
+
cards.append(f"<details class='panel model-card' open data-conn='{esc(c['name'])}'><summary>"
|
|
5245
|
+
f"<h3 class='chart-head'>{esc(c['name'])} · {len(c['models'])} models</h3></summary>"
|
|
5373
5246
|
f"<table class='model-cards'>{cols}<thead><tr>" + th("Model") + th("Through")
|
|
5374
5247
|
+ th("Context", True) + th("Max output", True)
|
|
5375
|
-
+ th("Input") + "".join(th(k, True) for k in catalog.LIMIT_KEYS)
|
|
5376
|
-
+ f"</tr></thead><tbody>{''.join(rows)}</tbody></table></
|
|
5248
|
+
+ th("Input") + th("Reply", True) + "".join(th(k, True) for k in catalog.LIMIT_KEYS)
|
|
5249
|
+
+ f"</tr></thead><tbody>{''.join(rows)}</tbody></table></details>")
|
|
5377
5250
|
return ("<div class='panel'><h2>Models</h2><p class='sub'>Every model on the bench by the connection it"
|
|
5378
5251
|
" comes from, with the endpoint it is reached through and what is known of its limits. A name"
|
|
5379
|
-
" opens the model.</p
|
|
5252
|
+
" opens the model.</p><div class='toolbar'><input type='search' data-filter='page'"
|
|
5253
|
+
" placeholder='Find a model or connection' aria-label='Filter models'>"
|
|
5254
|
+
# One labelled toggle, as accordions do it (GOV.UK's "Show all sections"): Collapse all
|
|
5255
|
+
# while every table is open, else Expand all (bench.js foldButtons keeps it right).
|
|
5256
|
+
"<button class='fold-all' type='button' data-fold-all='close' aria-expanded='true'>"
|
|
5257
|
+
"<span class='fold-chev' aria-hidden='true'></span>Collapse all</button></div></div>"
|
|
5380
5258
|
+ "".join(cards))
|
|
5381
5259
|
|
|
5382
5260
|
|
|
@@ -5553,11 +5431,16 @@ def logs_page() -> str:
|
|
|
5553
5431
|
for p in sorted((DATA / "runs").glob("onboard-*.log"), reverse=True)[:30])
|
|
5554
5432
|
|
|
5555
5433
|
def section(title, body, n, open_=False):
|
|
5556
|
-
|
|
5557
|
-
|
|
5434
|
+
# CSV: this log alone, its own columns, the filter applied (bench.js); for a spreadsheet.
|
|
5435
|
+
return (f"<details class='panel'{' open' if open_ else ''} data-log='{esc(title)}'><summary><b>{esc(title)}</b>"
|
|
5436
|
+
f" <span class='muted'>· {n}</span> <button class='btn small' type='button' data-csv"
|
|
5437
|
+
f" title='Save this log, as filtered, as a CSV file'>CSV</button></summary>{body}</details>")
|
|
5558
5438
|
return (
|
|
5559
|
-
"<div class='panel'><input data-filter='page' placeholder='Filter every log on this page'"
|
|
5560
|
-
" aria-label='Filter logs'
|
|
5439
|
+
"<div class='panel'><div class='toolbar'><input data-filter='page' placeholder='Filter every log on this page'"
|
|
5440
|
+
" aria-label='Filter logs'>"
|
|
5441
|
+
# bench.js: what the page shows, filter applied, as one .txt to read offline.
|
|
5442
|
+
"<button class='btn small' type='button' id='logs-export' title='Save the logs on this page, as"
|
|
5443
|
+
" filtered, to a text file'>Export as text</button></div></div>"
|
|
5561
5444
|
+ section("Gateway calls", "<table><thead><tr>" + th("When") + th("Collection") + th("Shape")
|
|
5562
5445
|
+ th("Seats") + f"</tr></thead><tbody>{''.join(gw)}</tbody></table>", len(gw), True)
|
|
5563
5446
|
+ section("Changes", "<table><thead><tr>" + th("When") + th("By") + th("Model") + th("What")
|
|
@@ -5876,8 +5759,7 @@ class Handler(gateway.Handler):
|
|
|
5876
5759
|
if self.path == "/api/needs": # after an action there: that page, not the whole one
|
|
5877
5760
|
with locked.snapshot():
|
|
5878
5761
|
groups, auto = open_actions()
|
|
5879
|
-
self._json(200, {"html": actions_page(groups, auto), "
|
|
5880
|
-
"count": open_count(groups)})
|
|
5762
|
+
self._json(200, {"html": actions_page(groups, auto), "count": open_count(groups)})
|
|
5881
5763
|
return
|
|
5882
5764
|
if path == "/api/pane": # a tab's page, when first opened (bench.js load)
|
|
5883
5765
|
key = q.get("tab", [""])[0]
|
|
@@ -5996,7 +5878,7 @@ class Handler(gateway.Handler):
|
|
|
5996
5878
|
theme = q.get("theme", [None])[0] # a preview; else the saved one
|
|
5997
5879
|
shown = re.search(r"(?:^|;)\s*bench_tab=(\w+)", self.headers.get("Cookie") or "") # the tab last shown
|
|
5998
5880
|
try:
|
|
5999
|
-
page = render(theme, shown.group(1) if shown else "
|
|
5881
|
+
page = render(theme, shown.group(1) if shown else "suite")
|
|
6000
5882
|
except RuntimeError as exc: # endpoints.json unreadable: say which and why
|
|
6001
5883
|
page = f"<!doctype html><meta charset='utf-8'><title>Benchwarmers</title><p>{esc(exc)}</p>"
|
|
6002
5884
|
self._page(page) # gzipped beyond this machine (_length)
|