@wendellresearch-ai/benchwarmers 0.6.6 → 0.6.7
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/VERSION +1 -1
- package/package.json +1 -1
- package/serve.py +74 -231
- package/ui/bench.css +18 -1
- package/ui/bench.js +74 -19
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.7
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
|
|
5
5
|
"mcpName": "io.github.WendellResearch-AI/benchwarmers",
|
|
6
6
|
"license": "MIT",
|
package/serve.py
CHANGED
|
@@ -2370,10 +2370,11 @@ SHARE = ("<details class='share'><summary>Share</summary><div class='share-menu'
|
|
|
2370
2370
|
+ "</span></div></details>")
|
|
2371
2371
|
|
|
2372
2372
|
|
|
2373
|
-
def chart_card(title: str, sub: str, chart_id: str, table: str, key: str, extra: str = "",
|
|
2373
|
+
def chart_card(title: str, sub: str, chart_id: str, table: str, key: str = "", extra: str = "",
|
|
2374
2374
|
retired: bool = True, after: str = "") -> str:
|
|
2375
2375
|
"""A Benchmarks panel: the chart first (drawn by bench.js with Observable
|
|
2376
|
-
Plot), the table folded under it,
|
|
2376
|
+
Plot), the table folded under it, then a key if one is given (the reports
|
|
2377
|
+
have none: the name, the chart and one line say it). `extra` goes above
|
|
2377
2378
|
the chart, `after` right under it."""
|
|
2378
2379
|
return (f"<div class='panel'>{SHARE}<h2>{title}</h2><p class='sub'>{sub}</p>{extra}"
|
|
2379
2380
|
f"<div class='chart' id='{chart_id}'></div>{after}"
|
|
@@ -2381,7 +2382,7 @@ def chart_card(title: str, sub: str, chart_id: str, table: str, key: str, extra:
|
|
|
2381
2382
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
2382
2383
|
+ ("<label><input type='checkbox' data-retired> Show retired</label>" if retired else "") + "</div>"
|
|
2383
2384
|
f"<div class='scroll'>{table}</div></details>"
|
|
2384
|
-
f"<details class='key'><summary>How to read this</summary>{key}</details
|
|
2385
|
+
+ (f"<details class='key'><summary>How to read this</summary>{key}</details>" if key else "") + "</div>")
|
|
2385
2386
|
|
|
2386
2387
|
|
|
2387
2388
|
def _family(task: str) -> str:
|
|
@@ -2441,16 +2442,22 @@ def wrong_table() -> dict:
|
|
|
2441
2442
|
return seen[1]
|
|
2442
2443
|
|
|
2443
2444
|
|
|
2445
|
+
def overview_rows() -> list[dict]:
|
|
2446
|
+
"""Who to trust, a row per registered model with both suite scores and answers:
|
|
2447
|
+
the share of real findings located, and the share of answers confidently wrong."""
|
|
2448
|
+
import route
|
|
2449
|
+
recall, wr = route.suite_recall(), wrong_table()
|
|
2450
|
+
return [{"model": m, "recall": recall[m][0] / recall[m][1], "wrong": wr[m]["wrong"] / wr[m]["answers"],
|
|
2451
|
+
"found": f"{recall[m][0]}/{recall[m][1]}", "wrongN": f"{wr[m]['wrong']}/{wr[m]['answers']}"}
|
|
2452
|
+
for m in sorted(on_bench()) if recall.get(m) and recall[m][1] and wr.get(m, {}).get("answers")]
|
|
2453
|
+
|
|
2454
|
+
|
|
2444
2455
|
def charts_data() -> dict:
|
|
2445
2456
|
"""Everything the Benchmarks charts draw, for bench.js. Registered models
|
|
2446
2457
|
only: the tables keep retired ones behind "Show retired", a chart has no toggle."""
|
|
2447
|
-
import route
|
|
2448
2458
|
import wrong
|
|
2449
2459
|
live = on_bench()
|
|
2450
|
-
|
|
2451
|
-
overview = [{"model": m, "recall": recall[m][0] / recall[m][1], "wrong": wr[m]["wrong"] / wr[m]["answers"],
|
|
2452
|
-
"found": f"{recall[m][0]}/{recall[m][1]}", "wrongN": f"{wr[m]['wrong']}/{wr[m]['answers']}"}
|
|
2453
|
-
for m in sorted(live) if recall.get(m) and recall[m][1] and wr.get(m, {}).get("answers")]
|
|
2460
|
+
wr = wrong_table()
|
|
2454
2461
|
heat = []
|
|
2455
2462
|
for (task, m), v in sorted(suite_scores().items()):
|
|
2456
2463
|
if m in live:
|
|
@@ -2462,7 +2469,7 @@ def charts_data() -> dict:
|
|
|
2462
2469
|
totals = {m: {"rate": r["wrong"] / r["answers"], "answers": r["answers"], "wrong": r["wrong"]}
|
|
2463
2470
|
for m, r in wr.items() if m in live and r["answers"]}
|
|
2464
2471
|
hunt = [{"model": m, **r["hunt"]} for m, r in sorted(wr.items()) if m in live and any(r["hunt"].values())]
|
|
2465
|
-
return {"overview":
|
|
2472
|
+
return {"overview": overview_rows(), "heat": heat, "families": ["hand-built", "claims", "seeded, function",
|
|
2466
2473
|
"seeded, whole file", "real bugs"],
|
|
2467
2474
|
"wrong": bars, "wrongTotals": totals, "kinds": list(wrong.KINDS), "hunt": hunt,
|
|
2468
2475
|
"work": work_rows(), "vision": vision_rows(), "uptime": uptime_data(), "trend": trend_data(),
|
|
@@ -2688,42 +2695,18 @@ def vision_card() -> str:
|
|
|
2688
2695
|
+ (f"<td class='num'>{r['seconds']:.1f}</td>" if r.get("seconds") else "<td class='num muted'>—</td>")
|
|
2689
2696
|
+ "</tr>")
|
|
2690
2697
|
body.append("</tbody></table>")
|
|
2691
|
-
key = """<dl>
|
|
2692
|
-
<dt>The vision tasks</dt><dd>Grouped by where the pictures come from: <b>made</b> with their answer
|
|
2693
|
-
key (<code>suite/vision_tasks.py</code>), your <b>own</b> photos keyed by hand, and items sampled
|
|
2694
|
-
from public datasets with people's answers (<code>suite/vision_sample.py</code>): photographed
|
|
2695
|
-
<b>receipts</b>, app <b>screens</b>, <b>tables</b>, and <b>objects</b> in photos. Each field is
|
|
2696
|
-
<b>right</b>, <b>misread</b> (another value), <b>left out</b>, or a <b>trap taken</b>: a value for
|
|
2697
|
-
something the picture does not show, where the right answer is <code>not shown</code> (a dot marks
|
|
2698
|
-
those fields). They run for every model that takes images; a vision model gets only these, never
|
|
2699
|
-
the code tasks.</dd>
|
|
2700
|
-
<dt>Real work on images</dt><dd>Every seat of a vision panel through the gateway. <b>Checked</b> means
|
|
2701
|
-
its verdict lists claims, each compared with the picture by hand: the claims right and wrong
|
|
2702
|
-
come from these alone. <b>Useful, no claims</b> is a verdict without them, which on thicken's
|
|
2703
|
-
photos means the seat agreed with another seat. Agreement is not a check, since seats fail
|
|
2704
|
-
together, so it is never counted as right.</dd>
|
|
2705
|
-
<dt>The real-work chart</dt><dd>Read as Real work is. The dot is the share of its judged seats your
|
|
2706
|
-
verdict called useful (checked and helped, or useful with no claims), bigger for more judged; the
|
|
2707
|
-
line is where that share likely sits (95%), and the order is by its low end, so a lot of evidence
|
|
2708
|
-
outranks a little. Green when even the low end is over half, red when even the high end is under,
|
|
2709
|
-
grey when there are too few to say. Beside it: useful of judged, how often it answered at all, and
|
|
2710
|
-
the claims checked by hand, the only count of right and wrong. Seats not judged or not answered
|
|
2711
|
-
are in those words and the table, not the dot.</dd>
|
|
2712
|
-
<dt>The number beside a name</dt><dd>How much there is: fields asked on the tasks.</dd>
|
|
2713
|
-
</dl>"""
|
|
2714
2698
|
held = (f"<p class='hint'>The vision tasks wait until {esc(v['held'])}: no run picks them before then"
|
|
2715
2699
|
" (<code>vision_from</code> in <code>config/suite.json</code>; remove it to start them now).</p>"
|
|
2716
2700
|
if v["held"] else "")
|
|
2717
|
-
return (f"<div class='panel'>{SHARE}<h2>Vision</h2><p class='sub'>Reading images: fields read right on pictures
|
|
2718
|
-
" a known answer, then real work on images
|
|
2701
|
+
return (f"<div class='panel'>{SHARE}<h2>Vision</h2><p class='sub'>Reading images: fields read right on pictures"
|
|
2702
|
+
" with a known answer, then real work on images.</p>" + held +
|
|
2719
2703
|
"<h3 class='chart-head'>On the vision tasks, field by field</h3><div class='chart' id='chart-vision-fields'></div>"
|
|
2720
2704
|
"<h3 class='chart-head'>On real work with images: judged useful, and how sure that is</h3>"
|
|
2721
2705
|
"<div class='chart' id='chart-vision-work'></div>"
|
|
2722
2706
|
"<details class='key'><summary>Table</summary><div class='toolbar'><input type='search'"
|
|
2723
2707
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
2724
2708
|
"<label><input type='checkbox' data-retired> Show retired</label></div>"
|
|
2725
|
-
f"<div class='scroll'>{''.join(body)}</div></details>"
|
|
2726
|
-
f"<details class='key'><summary>How to read this</summary>{key}</details></div>")
|
|
2709
|
+
f"<div class='scroll'>{''.join(body)}</div></details></div>")
|
|
2727
2710
|
|
|
2728
2711
|
|
|
2729
2712
|
def perf_charts() -> dict:
|
|
@@ -2892,35 +2875,8 @@ def trend_card() -> str:
|
|
|
2892
2875
|
pick = ("<div class='toolbar'><label>Window <select data-trend-days>"
|
|
2893
2876
|
+ "".join(f"<option value='{w}'>last {w} days</option>" for w in TREND_DAYS)
|
|
2894
2877
|
+ f"</select></label></div>{legend}")
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
scored in both: each task as a share of its findings, so a 1-finding claim and an 11-finding file count
|
|
2898
|
-
the same, then averaged over those tasks. Left is lower, right is higher; the shaded band is 10 points
|
|
2899
|
-
either side. Hover for each task's move.</dd>
|
|
2900
|
-
<dt>The groups</dt><dd><b>Lower</b> (or <b>higher</b>) when, on average, the scores moved 10 points or
|
|
2901
|
-
more, on at least two tasks and twice as many as moved the other way, in at least two separate runs.
|
|
2902
|
-
<b>Steady</b> otherwise: a steady bar past the band moved on average, but not on enough tasks or runs.
|
|
2903
|
-
<b>Too little to say</b>, with no bar, when fewer than three tasks can be compared, or fewer than two
|
|
2904
|
-
runs in the window or before it. One bad run, or one task, is never a trend, and nor is one run to
|
|
2905
|
-
compare against. Biggest move first in each group.</dd>
|
|
2906
|
-
<dt>Beside the bar</dt><dd>The evidence: the tasks compared and how many went down or up, then the runs
|
|
2907
|
-
in the window against the runs before it. A model with no task scored both in the window and before it
|
|
2908
|
-
is named under the chart.</dd>
|
|
2909
|
-
<dt>The window</dt><dd>{first} days by default: a paid model is scored about once a week and a free one
|
|
2910
|
-
about every 15 days, six tasks a run, so 7 days holds one run at most and could only say what one
|
|
2911
|
-
afternoon did. {first} days holds 2 to 4 runs. Longer windows hold more runs but leave less before them
|
|
2912
|
-
to compare with (the scores start {esc(t['first'] or 'with the first run')}).</dd>
|
|
2913
|
-
<dt>Runs</dt><dd>Days a model was scored (a task asked twice in a day counts once, as the mean of its
|
|
2914
|
-
scores). In the window, how many sat below or above its earlier scores.</dd>
|
|
2915
|
-
<dt>What is counted</dt><dd>Scores as the run notes recorded them (<code>runs/suite-*.md</code>), under
|
|
2916
|
-
each model's current name; timeouts, errors and refusals are not scores and are left out (Uptime counts
|
|
2917
|
-
them). A task whose number of findings changed starts again. The answers behind the scores are kept
|
|
2918
|
-
from 2026-11-08 in <code>runs/answers/<task>/<model>/</code>; older ones were replaced by
|
|
2919
|
-
the next run and are gone.</dd>
|
|
2920
|
-
</dl>"""
|
|
2921
|
-
return chart_card("Trend", "Who scores lower, or higher, than it used to? Each model against its own earlier"
|
|
2922
|
-
" scores on the same tasks; flagged only when the move holds over several tasks and runs.",
|
|
2923
|
-
"chart-trend", "".join(window(w) for w in TREND_DAYS), key, extra=pick)
|
|
2878
|
+
return chart_card("Trend", "Who scores lower, or higher, than it used to on the same tasks.",
|
|
2879
|
+
"chart-trend", "".join(window(w) for w in TREND_DAYS), extra=pick)
|
|
2924
2880
|
|
|
2925
2881
|
|
|
2926
2882
|
# ---- Uptime: was the provider there when the bench called it? --------------
|
|
@@ -3311,31 +3267,8 @@ def uptime_card() -> str:
|
|
|
3311
3267
|
+ "".join(f"<option value='{w}'>last {w}</option>" for w in UPTIME_DAYS)
|
|
3312
3268
|
+ "</select></label><label>By <select data-uptime-by><option value='providers'>provider</option>"
|
|
3313
3269
|
f"<option value='models'>model</option></select></label></div>{legend}")
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
429), unreachable (a broken stream, no reply), timed out (the gateway's 420 s included), or
|
|
3317
|
-
failed in a way no rule recognises yet, listed under the chart so a rule can be added. A
|
|
3318
|
-
shape a decision model was asked about counts as it said, marked "decided by" with the model
|
|
3319
|
-
and how sure it was (at 0.6 or more; below that it stays unrecognised).</dd>
|
|
3320
|
-
<dt>Left out</dt><dd>Neither up nor down, since it says nothing about the provider: our own
|
|
3321
|
-
quota or allowance used up (a day, a month, credits), our own per-minute or concurrency
|
|
3322
|
-
limit, a call the bench's pacing never sent, our client failing, our LAN hub refusing the
|
|
3323
|
-
connection, and setup problems (sign-in, no such model, no access, our own request).</dd>
|
|
3324
|
-
<dt>Up</dt><dd>It answered. An empty reply still counts as an answer.</dd>
|
|
3325
|
-
<dt>A cell</dt><dd>One day: answered of the calls counted that day. Hollow when nothing was
|
|
3326
|
-
counted that day; hover for the day, the count and the main reasons.</dd>
|
|
3327
|
-
<dt>The % and (n)</dt><dd>Answered of counted over the days shown, and how many were counted:
|
|
3328
|
-
weighted by calls, so a provider's row is every call to any of its models.</dd>
|
|
3329
|
-
<dt>What is counted</dt><dd>Only calls the bench makes anyway: the daily probe, every gateway
|
|
3330
|
-
seat, the suite's own calls once the client's ledger keeps their error text
|
|
3331
|
-
(2026-11-08 on), and every decision model call (<code>metrics/decisions.jsonl</code>), on the
|
|
3332
|
-
connection behind its served name. Each is classified from its error text when this is drawn,
|
|
3333
|
-
so a better rule changes the past too. It is shown only: nothing is routed or retired by it.</dd>
|
|
3334
|
-
</dl>"""
|
|
3335
|
-
return chart_card("Uptime", "Whether the provider was there when the bench called it: the daily probe"
|
|
3336
|
-
" and every real call, one cell a day. Busy, unreachable and timed out count as down;"
|
|
3337
|
-
" our own quota, our own pacing and setup problems are left out.",
|
|
3338
|
-
"chart-uptime-models", "".join(window(w) for w in UPTIME_DAYS), key,
|
|
3270
|
+
return chart_card("Uptime", "Whether the provider was there when the bench called it, one cell a day.",
|
|
3271
|
+
"chart-uptime-models", "".join(window(w) for w in UPTIME_DAYS),
|
|
3339
3272
|
extra=pick + "<div class='chart' id='chart-uptime-providers'></div>",
|
|
3340
3273
|
after="".join(odd(w) for w in UPTIME_DAYS))
|
|
3341
3274
|
|
|
@@ -3432,42 +3365,8 @@ def decision_card() -> str:
|
|
|
3432
3365
|
f"<p class='hint'>{'Measured up to ' + esc(when[:16].replace('T', ' ')) + '.' if when else 'Not measured yet.'}"
|
|
3433
3366
|
" The scheduler's <code>decisions</code> job asks each model one question a set a day;"
|
|
3434
3367
|
" <code>python metrics/decide.py bench</code> runs a pass by hand.</p>")
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
<b>Error classes</b>: failed calls' error text (redacted: no keys, ids or emails) and the class the
|
|
3438
|
-
uptime rules give it. <b>Collections</b>: gateway requests and the collection the caller named (not
|
|
3439
|
-
ones a decision model chose). <b>Useful answers</b>: seat answers you judged, with their request,
|
|
3440
|
-
a yes/no question.</dd>
|
|
3441
|
-
<dt>Right of asked</dt><dd>Each item's latest answer; a failed call counts as asked and not right.
|
|
3442
|
-
<b>Accuracy</b> is of the answers given; F1 is the mean over the options of how well each was
|
|
3443
|
-
picked, so always saying the commonest option scores low.</dd>
|
|
3444
|
-
<dt>Brier</dt><dd>How far its probabilities sat from the truth: 0 is perfect; for a choice it is
|
|
3445
|
-
summed over the options, so up to 2. Lower is better, and a sure wrong answer costs most.</dd>
|
|
3446
|
-
<dt>The bars</dt><dd>Right of asked for each model on each set, from 0%. A faint bar is a model
|
|
3447
|
-
under {low} items on that set: shown, not ranked yet. Hover a bar for its Brier.</dd>
|
|
3448
|
-
<dt>Calibration</dt><dd>The second chart, once a model answers at two or more levels of sureness
|
|
3449
|
-
on a set: answers grouped by the probability the model gave its answer
|
|
3450
|
-
(x), against how often those answers were right (y); both axes start just under the lowest
|
|
3451
|
-
answer, not at 0%. On the diagonal, its 80% answers come true
|
|
3452
|
-
80% of the time; under it, it is surer than it should be. A bigger dot is more answers. The gap is
|
|
3453
|
-
the average distance from the diagonal, weighted by answers.</dd>
|
|
3454
|
-
<dt>At {decide.CONFIDENT}</dt><dd>The gateway acts on a choice only when the model is this sure: how
|
|
3455
|
-
many answers were that sure, and how many of those were right.</dd>
|
|
3456
|
-
<dt>Rank</dt><dd>Among models with {low} or more items asked on the set, by right answers
|
|
3457
|
-
smoothed for sample size ((right + 1) / (asked + 2)). Below {low} a model is shown but not ranked,
|
|
3458
|
-
and a use keeps its default (the first onboarded model that takes the question).</dd>
|
|
3459
|
-
<dt>Who each use asks</dt><dd>The shape of a request and a new model's role are ranked on the
|
|
3460
|
-
collection set (one described option of several, for a text), the notice check on useful answers
|
|
3461
|
-
(the only yes/no set). A use asked per request never goes to a <code>:free</code> model while
|
|
3462
|
-
another can answer: their calls come out of OpenRouter's free daily budget, which the suite and
|
|
3463
|
-
the probe share.</dd>
|
|
3464
|
-
<dt>Failures</dt><dd>Calls that came back with no answer, by the uptime classes (setup: no such
|
|
3465
|
-
model, or paywalled). The pass stops a model at its first failure.</dd>
|
|
3466
|
-
</dl>"""
|
|
3467
|
-
return chart_card("Decision models", "The models that answer typed questions (a choice, a yes or no) with"
|
|
3468
|
-
" a probability: how often they are right on the bench's own labelled questions, how far"
|
|
3469
|
-
" to trust their probabilities, and which one each use asks.",
|
|
3470
|
-
"chart-decide", "".join(tables), key, extra=extra, retired=False)
|
|
3368
|
+
return chart_card("Decision", "How often the models that answer typed questions (a choice, a yes or no)"
|
|
3369
|
+
" are right, and how sure.", "chart-decide", "".join(tables), extra=extra, retired=False)
|
|
3471
3370
|
|
|
3472
3371
|
|
|
3473
3372
|
def bughunt_card() -> str:
|
|
@@ -3483,19 +3382,8 @@ def bughunt_card() -> str:
|
|
|
3483
3382
|
+ "".join(f"<td class='num' data-sort='{h[k]}'>{h[k]}/{clean}</td>" for k in ("clean", "false alarm"))
|
|
3484
3383
|
+ "</tr>")
|
|
3485
3384
|
body.append("</tbody></table>")
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
file before a real fix): green found the changed line, yellow named another line,
|
|
3489
|
-
grey said there was no bug.</dd>
|
|
3490
|
-
<dt>Left of the line</dt><dd>The same files untouched: red is a false alarm, claiming a
|
|
3491
|
-
bug in clean code. The longer the red, the less to trust its "there's a bug here".</dd>
|
|
3492
|
-
<dt>Order</dt><dd>On what is proven, best at the top: the low end of the found share's likely
|
|
3493
|
-
range (95%) less the high end of the false alarms'. A few tasks give wide ranges, so they
|
|
3494
|
-
rank low; a model never shown clean code could be raising alarms on all of it.</dd>
|
|
3495
|
-
<dt>Faint bars</dt><dd>Under 10 tasks: shown, but little to go on.</dd>
|
|
3496
|
-
</dl>"""
|
|
3497
|
-
return chart_card("Bug hunt", "Every task with an answer key: bugs found against false alarms on the"
|
|
3498
|
-
" same files left clean. Right is good, left is bad.", "chart-hunt", "".join(body), key)
|
|
3385
|
+
return chart_card("Bug hunt", "Bugs found against false alarms on the same files left clean.",
|
|
3386
|
+
"chart-hunt", "".join(body))
|
|
3499
3387
|
|
|
3500
3388
|
|
|
3501
3389
|
def _wilson(k: int, n: int, z: float = 1.96) -> tuple[float, float]:
|
|
@@ -3519,27 +3407,21 @@ def work_card() -> str:
|
|
|
3519
3407
|
f"<td class='num' data-sort='{lo:.3f}'>{lo * 100:.0f}–{hi * 100:.0f}%</td>"
|
|
3520
3408
|
f"<td class='num'>{r['real']}</td><td class='num'>{r['false']}</td></tr>")
|
|
3521
3409
|
body.append("</tbody></table>")
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
enumerations) handed to the model and checked afterwards. Benchmarks never count here.</dd>
|
|
3525
|
-
<dt>The dot and its line</dt><dd>The dot is the share that helped, bigger for more judged. The
|
|
3526
|
-
line is where the true share likely sits (a 95% Wilson interval): long for 3 of 3, short for
|
|
3527
|
-
40 of 50. Green when even its low end is over half, red when even its high end is under,
|
|
3528
|
-
grey when there are too few to say.</dd>
|
|
3529
|
-
<dt>Order</dt><dd>By the low end of that line, so volume counts: 40 of 50 ranks above 3 of 3.</dd>
|
|
3530
|
-
<dt>The dashed line</dt><dd>Every model's judged offloads together, for comparison.</dd>
|
|
3531
|
-
<dt>Findings and false claims</dt><dd>Verified one by one in reviews, recorded since 2026-10-17.</dd>
|
|
3532
|
-
</dl>"""
|
|
3533
|
-
return chart_card("Real work", "When a model was given real work and the result was checked: how often"
|
|
3534
|
-
" it helped. The most direct measure of trust the bench has.", "chart-work", "".join(body), key)
|
|
3410
|
+
return chart_card("Real work", "How often a model helped when given real work that was checked afterwards.",
|
|
3411
|
+
"chart-work", "".join(body))
|
|
3535
3412
|
|
|
3536
3413
|
|
|
3537
3414
|
def overview_card() -> str:
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
"
|
|
3541
|
-
|
|
3542
|
-
|
|
3415
|
+
rows = sorted(overview_rows(), key=lambda d: (d["wrong"], -d["recall"]))
|
|
3416
|
+
body = ["<table><thead><tr>", th("Model"), th("Finds real bugs", True), th("Makes things up", True),
|
|
3417
|
+
"</tr></thead><tbody>"]
|
|
3418
|
+
body += [f"<tr><td>{named(d['model'])}</td>"
|
|
3419
|
+
f"<td class='num' data-sort='{d['recall']:.3f}'>{d['found']} ({d['recall'] * 100:.0f}%)</td>"
|
|
3420
|
+
f"<td class='num' data-sort='{d['wrong']:.3f}'>{d['wrongN']} ({d['wrong'] * 100:.0f}%)</td></tr>"
|
|
3421
|
+
for d in rows]
|
|
3422
|
+
body.append("</tbody></table>")
|
|
3423
|
+
return chart_card("Who to trust with a review", "Top right is the reviewer to pick: finds the real bugs"
|
|
3424
|
+
" and makes little up.", "chart-overview", "".join(body), retired=False)
|
|
3543
3425
|
|
|
3544
3426
|
|
|
3545
3427
|
def suite_card() -> str:
|
|
@@ -3575,22 +3457,8 @@ def suite_card() -> str:
|
|
|
3575
3457
|
+ "".join(cells) + flag + "</tr>")
|
|
3576
3458
|
body.append("</tbody></table>")
|
|
3577
3459
|
body = "".join(body)
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
for that task. The answers were established from source before any
|
|
3581
|
-
model saw them.</dd>
|
|
3582
|
-
<dt>Invented</dt><dd><strong>The column that matters.</strong> Something
|
|
3583
|
-
asserted that is not true of the code. A confident wrong answer costs
|
|
3584
|
-
more than a silent miss.</dd>
|
|
3585
|
-
<dt>timeout / refused / error</dt><dd>Not a score. The model did not
|
|
3586
|
-
answer — a filter, an overloaded service and a slow model are three
|
|
3587
|
-
different facts and are never merged.</dd>
|
|
3588
|
-
<dt>(tag)</dt><dd>The connection it arrives through: its Open WebUI tag, or its endpoint. Models
|
|
3589
|
-
sharing one share its rate limit.</dd>
|
|
3590
|
-
</dl>"""
|
|
3591
|
-
return chart_card("Our code", "Every task on our own code, one square each: green found everything,"
|
|
3592
|
-
" red found nothing, grey did not answer; a dot means it invented something. Hover"
|
|
3593
|
-
" for the task and score.", "chart-code", body, key)
|
|
3460
|
+
return chart_card("Our code", "Every task on our own code, one square per task and model: green found"
|
|
3461
|
+
" everything, red nothing.", "chart-code", body)
|
|
3594
3462
|
|
|
3595
3463
|
|
|
3596
3464
|
EXTERNAL = (("LiveBench", "https://livebench.ai/", "contamination-limited questions, refreshed monthly"),
|
|
@@ -3605,8 +3473,8 @@ def external_card() -> str:
|
|
|
3605
3473
|
"""Links, not scores: the External tab showed FuncBench numbers nobody
|
|
3606
3474
|
refreshed and was dropped (2026-10-16). Public leaderboards are read there."""
|
|
3607
3475
|
rows = "".join(f"<li>{ext_link(u, n)} <span class='muted'>— {esc(why)}</span></li>" for n, u, why in EXTERNAL)
|
|
3608
|
-
return ("<div class='panel'><h2>External</h2><p class='sub'>Public leaderboards, for comparison
|
|
3609
|
-
"
|
|
3476
|
+
return ("<div class='panel'><h2>External</h2><p class='sub'>Public leaderboards, for comparison;"
|
|
3477
|
+
" routing uses the bench's own tests.</p>"
|
|
3610
3478
|
f"<ul class='links'>{rows}</ul></div>")
|
|
3611
3479
|
|
|
3612
3480
|
|
|
@@ -3639,26 +3507,8 @@ def wrong_card() -> str:
|
|
|
3639
3507
|
+ (f"<td class='num' data-sort='{r['review_false']}'>{r['review_false']} in {r['reviews']}</td>"
|
|
3640
3508
|
if r["reviews"] else "<td class='num muted' data-sort='-1'>—</td>") + "</tr>")
|
|
3641
3509
|
body.append("</tbody></table>")
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
that could have. Being wrong is why the bench asks several models; asserting a bug
|
|
3645
|
-
that is not there is what sends someone to fix working code. Lower is better.</dd>
|
|
3646
|
-
<dt>Control</dt><dd>A seeded or real-bug control (the clean file) answered FAULT.</dd>
|
|
3647
|
-
<dt>Wrong line</dt><dd>A seeded or real-bug task answered FAULT at a line the change did
|
|
3648
|
-
not touch. Saying NO FAULT on a bug is a miss and is not counted.</dd>
|
|
3649
|
-
<dt>False claim</dt><dd>A claim task whose claim is false, answered VERDICT: TRUE.</dd>
|
|
3650
|
-
<dt>Trap</dt><dd>A hand-built task's planted falsehood, asserted.</dd>
|
|
3651
|
-
<dt>Reviews</dt><dd>False claims recorded in verdicts (<code>offload.py verdict|add
|
|
3652
|
-
--false "..."</code>), over judged offloads. Recorded in structured form only since
|
|
3653
|
-
2026-10-17, so older reviews show none.</dd>
|
|
3654
|
-
<dt>The thin line</dt><dd>Where the true share likely sits (95% range). Long for a few
|
|
3655
|
-
answers: 0 of 2 is not a clean record, and the line shows how bad it could be.</dd>
|
|
3656
|
-
<dt>Order</dt><dd>Worst proven at the top: by the low end of that line. Under 10 answers a
|
|
3657
|
-
model's bar is faint.</dd>
|
|
3658
|
-
</dl>"""
|
|
3659
|
-
return chart_card("Confidently wrong", "Who asserts things that are not true: false alarms on clean"
|
|
3660
|
-
" code, wrong lines, false claims confirmed, traps taken. Shorter is better.",
|
|
3661
|
-
"chart-wrong", "".join(body), key)
|
|
3510
|
+
return chart_card("Confidently wrong", "Who asserts things that are not true; shorter is better.",
|
|
3511
|
+
"chart-wrong", "".join(body))
|
|
3662
3512
|
|
|
3663
3513
|
|
|
3664
3514
|
def benchmarks_tab() -> str:
|
|
@@ -3670,7 +3520,7 @@ def benchmarks_tab() -> str:
|
|
|
3670
3520
|
("trend", "Trend", trend_card), ("performance", "Performance", perf_card),
|
|
3671
3521
|
("work", "Real work", work_card), ("hunt", "Bug hunt", bughunt_card),
|
|
3672
3522
|
("wrong", "Confidently wrong", wrong_card), ("code", "Our code", suite_card),
|
|
3673
|
-
("vision", "Vision", vision_card), ("decisions", "Decision
|
|
3523
|
+
("vision", "Vision", vision_card), ("decisions", "Decision", decision_card),
|
|
3674
3524
|
("external", "External", external_card))
|
|
3675
3525
|
links = "".join(f"<a href='#report-{key}'>{esc(label)}</a>" for key, label, _ in reports)
|
|
3676
3526
|
cards = "".join(card().replace("<div class='panel'", f"<div class='panel report' id='report-{key}'", 1)
|
|
@@ -3708,34 +3558,8 @@ def perf_card() -> str:
|
|
|
3708
3558
|
f"title='p50 {p50:.1f}s'><span style='width:100%'></span></span></td></tr>")
|
|
3709
3559
|
body.append("</tbody></table>")
|
|
3710
3560
|
body = "".join(body)
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
this table.</strong> A full-width bar is the slowest here, not a score
|
|
3714
|
-
out of anything.</dd>
|
|
3715
|
-
<dt>p50 / p95</dt><dd>Median and 95th-percentile seconds to a complete
|
|
3716
|
-
answer. Medians, not means — one timeout makes a mean fiction. p95 is
|
|
3717
|
-
where rate limiting shows itself.</dd>
|
|
3718
|
-
<dt>tiny / large</dt><dd>A one-word prompt measures overhead and
|
|
3719
|
-
queueing; an 18 KB prompt measures throughput. They answer different
|
|
3720
|
-
questions.</dd>
|
|
3721
|
-
<dt>ch/s</dt><dd>Characters per second, from the large prompt only. A
|
|
3722
|
-
proxy for tokens/s, affected by how verbose a model is.</dd>
|
|
3723
|
-
<dt>tok/s</dt><dd>Output tokens per second from the provider's own count, first token to
|
|
3724
|
-
last, large prompt only. Only where the built-in client made the call and the provider
|
|
3725
|
-
reported usage; blank otherwise, never estimated.</dd>
|
|
3726
|
-
<dt>Outcomes</dt><dd>answered / refused / error / timeout.</dd>
|
|
3727
|
-
<dt>Weight it LEAST</dt><dd>Seconds are a wash. Speed only vetoes a model
|
|
3728
|
-
when it is slow enough to change how you work.</dd>
|
|
3729
|
-
</dl>"""
|
|
3730
|
-
key = key.replace("</dl>", """
|
|
3731
|
-
<dt>Did it answer?</dt><dd>Is on Uptime, by day, with our own quota and setup problems left out.</dd>
|
|
3732
|
-
<dt>Speed vs quality</dt><dd>Median seconds per answer on our own suite tasks (real prompts,
|
|
3733
|
-
not the one-word probe) against the share of findings located. Top left is fast and good:
|
|
3734
|
-
the green box is the faster half and the top quarter of findings.</dd>
|
|
3735
|
-
</dl>""")
|
|
3736
|
-
return chart_card("Performance", "What the serving path does — not what the models are worth. One"
|
|
3737
|
-
" reading is weather; the history is climate.", "chart-speed", body, key,
|
|
3738
|
-
extra="<h3 class='chart-head'>Speed vs quality on our own tasks</h3>")
|
|
3561
|
+
return chart_card("Performance", "How fast each model answers our own tasks, against how much it finds.",
|
|
3562
|
+
"chart-speed", body)
|
|
3739
3563
|
|
|
3740
3564
|
|
|
3741
3565
|
def favicon(theme: str) -> str:
|
|
@@ -4017,6 +3841,8 @@ def discover_card() -> str:
|
|
|
4017
3841
|
which asks /api/discover-model for the rest: its routes are where Add is."""
|
|
4018
3842
|
all_rows = _discover_rows()
|
|
4019
3843
|
rows = [r for r in all_rows if r["free"] or r["reach"] in ("connected", "on bench")]
|
|
3844
|
+
# New models first, the newest at the top; the rest keep discover.py's order (a stable sort).
|
|
3845
|
+
rows.sort(key=lambda r: r["first_seen"] if r["new"] else "", reverse=True)
|
|
4020
3846
|
hidden = len(all_rows) - len(rows)
|
|
4021
3847
|
reach_css = {"on bench": "ok", "connected": "ok", "easy to add": "warn", "other": ""}
|
|
4022
3848
|
body = ["<table><thead><tr>", th("Model"), th("Rank", True), th("Provider"), th("Reach"), th("Price"),
|
|
@@ -4039,8 +3865,9 @@ def discover_card() -> str:
|
|
|
4039
3865
|
add = ("<button class='btn small' type='button'>Add…</button>"
|
|
4040
3866
|
if any(not x["on_bench_as"] for x in r["routes"]) else "")
|
|
4041
3867
|
body.append(
|
|
4042
|
-
f"<tr class='pick-row
|
|
4043
|
-
f" data-
|
|
3868
|
+
f"<tr class='pick-row' data-provider='{esc(r['provider'])}' data-model='{esc(r['model'])}'"
|
|
3869
|
+
f" data-reach='{r['reach']}' data-price='{'free' if r['free'] else 'paid'}'"
|
|
3870
|
+
f" data-new='{'1' if r['new'] else ''}' data-caps='{' '.join(caps)}'><td>"
|
|
4044
3871
|
f"<a href='#' class='dmodel'><span class='alias'>{esc(r['name'])}</span></a>"
|
|
4045
3872
|
+ (" " + chip("new", "new", f"first seen {r['first_seen']}") if r["new"] else "")
|
|
4046
3873
|
+ f"<span class='conn'>{esc(r['model'])}"
|
|
@@ -4063,9 +3890,23 @@ def discover_card() -> str:
|
|
|
4063
3890
|
import discover
|
|
4064
3891
|
import sources
|
|
4065
3892
|
ages = [a for a in map(sources.age_s, [*sources.FEEDS, discover.LISTS]) if a is not None]
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
3893
|
+
# The rail: a search box and a checkbox per value of each facet, matched against the
|
|
3894
|
+
# rows' data-* by bench.js (dApply), which also fills in the counts and pages the list.
|
|
3895
|
+
def group(title: str, facet: str, values) -> str:
|
|
3896
|
+
return (f"<fieldset><legend>{esc(title)}</legend>" + "".join(
|
|
3897
|
+
f"<label><input type='checkbox' data-df='{facet}' value='{esc(v)}'> {esc(label)}"
|
|
3898
|
+
"<span class='n'></span></label>" for v, label in values) + "</fieldset>")
|
|
3899
|
+
provs = {r["provider"]: r["provider_name"] for r in rows}
|
|
3900
|
+
rail = ("<aside class='facets' id='d-facets' aria-label='Filter models'>"
|
|
3901
|
+
"<input type='search' id='d-q' placeholder='Search models' aria-label='Search models'>"
|
|
3902
|
+
+ group("Show", "new", [("1", "New")])
|
|
3903
|
+
+ group("Price", "price", [("free", "Free"), ("paid", "Paid and unpriced")])
|
|
3904
|
+
+ group("Reach", "reach", [(v, v.capitalize()) for v in reach_css])
|
|
3905
|
+
+ group("Can", "caps", [(v, v.capitalize()) for v in ("tools", "vision", "reasons")])
|
|
3906
|
+
+ group(f"Provider · {len(provs)}", "provider",
|
|
3907
|
+
sorted(provs.items(), key=lambda kv: kv[1].lower()))
|
|
3908
|
+
+ "</aside>")
|
|
3909
|
+
toolbar = ("<div class='toolbar'>"
|
|
4069
3910
|
"<button class='btn small' type='button' id='d-refresh' title='Fetch FreeLLMAPI, models.dev and"
|
|
4070
3911
|
" every Open WebUI connection's own model list now, instead of waiting for the daily fetch'>"
|
|
4071
3912
|
"Refresh lists</button><span class='muted' id='d-refreshed'>"
|
|
@@ -4084,7 +3925,7 @@ def discover_card() -> str:
|
|
|
4084
3925
|
<dt>Price</dt><dd>Free if FreeLLMAPI lists it (a catalog of free tiers) or models.dev
|
|
4085
3926
|
prices it at 0: models.dev shows the PAID price even where a free tier exists. Otherwise
|
|
4086
3927
|
input / output per million tokens. <em>Unknown</em> where only the provider's own list has
|
|
4087
|
-
the model and it says nothing of price; those are
|
|
3928
|
+
the model and it says nothing of price; those are under <em>Paid and unpriced</em>.</dd>
|
|
4088
3929
|
<dt>Limits</dt><dd>The free tier's, from FreeLLMAPI: requests and tokens per minute (rpm,
|
|
4089
3930
|
tpm) and per day (rpd, tpd), and roughly how many tokens a month that allows.</dd>
|
|
4090
3931
|
<dt>New</dt><dd>First seen in either list in the last 7 days.</dd>
|
|
@@ -4111,7 +3952,9 @@ def discover_card() -> str:
|
|
|
4111
3952
|
f" models.dev and your providers' own lists, deduped; {hidden:,} paid models on providers you have no route"
|
|
4112
3953
|
f" to are left out. Click a model for everything known about it and the routes it can be"
|
|
4113
3954
|
f" added by.</p>{toolbar}"
|
|
3955
|
+
f"<div class='discover'>{rail}<div><p class='hint d-count' id='d-count' aria-live='polite'></p>"
|
|
4114
3956
|
f"<div class='scroll'>{''.join(body)}</div>"
|
|
3957
|
+
"<button class='btn small' type='button' id='d-more' hidden>Show 100 more</button></div></div>"
|
|
4115
3958
|
f"<details class='key'><summary>How to read this</summary>{key}</details></div>")
|
|
4116
3959
|
|
|
4117
3960
|
|
|
@@ -4219,7 +4062,7 @@ def collections_tab() -> str:
|
|
|
4219
4062
|
" names none</label>"
|
|
4220
4063
|
"<div class='settings'><div><label class='field' for='g-pick-conf'>Only when the model is at least"
|
|
4221
4064
|
"</label><input id='g-pick-conf' type='number' min='0.05' max='1' step='0.05'></div></div>"
|
|
4222
|
-
"<p class='rules'>A decision model (the one Benchmarks › Decision
|
|
4065
|
+
"<p class='rules'>A decision model (the one Benchmarks › Decision says the collection pick"
|
|
4223
4066
|
" asks) chooses from the collections' descriptions and tags, and the reply says which and why"
|
|
4224
4067
|
" (<code>bench.picked</code>, <code>X-Bench-Picked</code>). Less sure than this, or switched off,"
|
|
4225
4068
|
" the request is refused with each collection's description and tags, so the caller can choose."
|
package/ui/bench.css
CHANGED
|
@@ -302,7 +302,24 @@ tbody tr:hover > td { background: var(--step-1); }
|
|
|
302
302
|
tr.retired > td { color: var(--muted); }
|
|
303
303
|
tr.retired .alias { text-decoration: line-through; }
|
|
304
304
|
body:not(.show-retired) tr.retired { display: none; }
|
|
305
|
-
|
|
305
|
+
/* Discover: the facet rail beside the list. It stays in view and scrolls by itself. */
|
|
306
|
+
.discover { display: grid; grid-template-columns: 13.5rem minmax(0, 1fr); gap: 1.5rem; align-items: start; }
|
|
307
|
+
.facets { position: sticky; top: 1rem; max-height: calc(100vh - 2rem); overflow-y: auto; font-size: .875rem;
|
|
308
|
+
padding-right: .5rem; }
|
|
309
|
+
.facets input[type=search] { font: inherit; width: 100%; padding: .4rem .6rem; border: 1px solid var(--line);
|
|
310
|
+
border-radius: var(--radius); background: var(--paper); color: var(--ink); }
|
|
311
|
+
.facets fieldset { border: 0; padding: 0; margin: .9rem 0 0; min-width: 0; }
|
|
312
|
+
.facets legend { padding: 0; margin-bottom: .2rem; font-size: .8125rem; font-weight: 700; color: var(--muted); }
|
|
313
|
+
.facets label { display: flex; align-items: center; gap: .4rem; padding: .12rem 0; cursor: pointer; }
|
|
314
|
+
.facets label .n { margin-left: auto; padding-left: .5rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
315
|
+
.facets label.none { color: var(--muted); }
|
|
316
|
+
.discover td:nth-child(n+2) { white-space: nowrap; } /* one line a model: the table scrolls sideways instead */
|
|
317
|
+
.d-count { margin: 0 0 .5rem; }
|
|
318
|
+
#d-more { margin-top: .9rem; }
|
|
319
|
+
@media (max-width: 900px) {
|
|
320
|
+
.discover { grid-template-columns: minmax(0, 1fr); }
|
|
321
|
+
.facets { position: static; max-height: 45vh; }
|
|
322
|
+
}
|
|
306
323
|
.muted { color: var(--muted); }
|
|
307
324
|
.ok { color: var(--ok); } .warn { color: var(--warn); } .err { color: var(--err); }
|
|
308
325
|
|
package/ui/bench.js
CHANGED
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
var onShow = {}; // a page's loader, run each time it is shown
|
|
124
124
|
var onLoad = {}; // a page's set-up, run each time its HTML arrives
|
|
125
125
|
var shown = null, fetching = {}, ready = {}, marks = 0, last = {}, edited = {};
|
|
126
|
-
var KEPT = 'input[data-filter], select[data-facet], input[data-retired], input[data-
|
|
126
|
+
var KEPT = 'input[data-filter], select[data-facet], input[data-retired], input[data-df], #d-q, [data-kept]'; // controls a swap keeps (or may reset)
|
|
127
127
|
function show(id) {
|
|
128
128
|
shown = id;
|
|
129
129
|
tabs.forEach(function (b) { b.setAttribute('aria-selected', String(b.dataset.tab === id)); });
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
function headKey(tr) { return tr.textContent; }
|
|
205
205
|
function setUp(id) {
|
|
206
206
|
delete edited['tab-' + id];
|
|
207
|
-
applyRetired();
|
|
207
|
+
applyRetired();
|
|
208
208
|
return (ready[id] = Promise.resolve(onLoad[id] && onLoad[id]()));
|
|
209
209
|
}
|
|
210
210
|
tabs.forEach(function (b) { b.addEventListener('click', function () { show(b.dataset.tab); }); });
|
|
@@ -527,17 +527,67 @@
|
|
|
527
527
|
}, failed);
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
-
// ---- Discover:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
// ---- Discover: the facet rail -----------------------------------------
|
|
531
|
+
// A row shows when it matches the search box and every facet with a box ticked: any of
|
|
532
|
+
// the ticked values (for Can: all of them, since a model has several). Each box's count
|
|
533
|
+
// is what ticking it would leave, given the other facets. Free only until asked otherwise.
|
|
534
|
+
var D_FACETS = ['new', 'price', 'reach', 'caps', 'provider'], D_PAGE = 100;
|
|
535
|
+
var dPick = {price: ['free']}, dQ = '', dLimit = D_PAGE;
|
|
536
|
+
try { dPick = JSON.parse(store('bench.discover')) || dPick; } catch (e) { /* not ours: the default */ }
|
|
537
|
+
// The list is paged in CSS, so the first rows shown stay the first after a sort by a column.
|
|
538
|
+
var dPageCss = document.head.appendChild(el('style'));
|
|
539
|
+
function dHas(r, k, v) { return k === 'caps' ? r.dataset.caps.split(' ').indexOf(v) !== -1 : r.dataset[k] === v; }
|
|
540
|
+
function dApply() {
|
|
541
|
+
var rail = $('d-facets');
|
|
542
|
+
if (!rail) return;
|
|
543
|
+
var picked = D_FACETS.filter(function (k) { return (dPick[k] || []).length; });
|
|
544
|
+
var q = dQ.trim().toLowerCase(), counts = {}, hits = 0;
|
|
545
|
+
$('tab-discover').querySelectorAll('tr.pick-row').forEach(function (r) {
|
|
546
|
+
var fails = picked.filter(function (k) {
|
|
547
|
+
return !dPick[k][k === 'caps' ? 'every' : 'some'](function (v) { return dHas(r, k, v); });
|
|
548
|
+
});
|
|
549
|
+
var text = !q || r.textContent.toLowerCase().indexOf(q) !== -1;
|
|
550
|
+
r.hidden = !text || fails.length > 0;
|
|
551
|
+
if (!r.hidden) hits++;
|
|
552
|
+
if (!text || fails.length > 1) return;
|
|
553
|
+
D_FACETS.forEach(function (k) {
|
|
554
|
+
// Failing only this facet, another of its boxes would show the row (not Can: it wants all).
|
|
555
|
+
if (fails.length && (fails[0] !== k || k === 'caps')) return;
|
|
556
|
+
(k === 'caps' ? r.dataset.caps.split(' ') : [r.dataset[k]]).forEach(function (v) {
|
|
557
|
+
counts[k + '|' + v] = (counts[k + '|' + v] || 0) + 1;
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
rail.querySelectorAll('input[data-df]').forEach(function (c) {
|
|
562
|
+
var n = counts[c.dataset.df + '|' + c.value] || 0;
|
|
563
|
+
c.parentNode.querySelector('.n').textContent = n.toLocaleString();
|
|
564
|
+
c.parentNode.classList.toggle('none', !n && !c.checked);
|
|
565
|
+
});
|
|
566
|
+
$('d-count').textContent = hits.toLocaleString() + (hits === 1 ? ' model' : ' models')
|
|
567
|
+
+ (hits > dLimit ? ', the first ' + dLimit.toLocaleString() + ' shown' : '');
|
|
568
|
+
$('d-more').hidden = hits <= dLimit;
|
|
569
|
+
dPageCss.textContent = '#tab-discover tr.pick-row:nth-child(n+' + (dLimit + 1) + ' of :not([hidden])) { display: none; }';
|
|
535
570
|
}
|
|
571
|
+
onLoad.discover = function () {
|
|
572
|
+
$('d-q').value = dQ;
|
|
573
|
+
$('d-facets').querySelectorAll('input[data-df]').forEach(function (c) {
|
|
574
|
+
c.checked = (dPick[c.dataset.df] || []).indexOf(c.value) !== -1;
|
|
575
|
+
});
|
|
576
|
+
dApply();
|
|
577
|
+
};
|
|
536
578
|
document.addEventListener('change', function (ev) {
|
|
537
|
-
|
|
538
|
-
|
|
579
|
+
if (!ev.target.matches('input[data-df]')) return;
|
|
580
|
+
dPick = {};
|
|
581
|
+
$('d-facets').querySelectorAll('input[data-df]:checked').forEach(function (c) {
|
|
582
|
+
(dPick[c.dataset.df] = dPick[c.dataset.df] || []).push(c.value);
|
|
583
|
+
});
|
|
584
|
+
store('bench.discover', JSON.stringify(dPick));
|
|
585
|
+
dLimit = D_PAGE; dApply();
|
|
586
|
+
});
|
|
587
|
+
document.addEventListener('input', function (ev) {
|
|
588
|
+
if (ev.target.id === 'd-q') { dQ = ev.target.value; dLimit = D_PAGE; dApply(); }
|
|
539
589
|
});
|
|
540
|
-
|
|
590
|
+
on('click', 'd-more', function () { dLimit += D_PAGE; dApply(); });
|
|
541
591
|
|
|
542
592
|
// ---- live: the page follows the data ---------------------------------
|
|
543
593
|
// The data's stamp (serve.data_stamp) is asked every 10 s while the page is
|
|
@@ -3091,12 +3141,13 @@
|
|
|
3091
3141
|
// Who to trust: finds real bugs (right) vs makes things up (up = less).
|
|
3092
3142
|
put('chart-overview', function (w) {
|
|
3093
3143
|
if (!C.overview.length) return el('p', {class: 'hint'}, 'No model has both suite scores and answers yet.');
|
|
3094
|
-
var worst = Math.max(0.
|
|
3144
|
+
var worst = Math.max(0.15, Math.max.apply(null, C.overview.map(function (d) { return d.wrong; })));
|
|
3095
3145
|
var x0 = Math.max(0, Math.floor((Math.min.apply(null, C.overview.map(function (d) { return d.recall; })) - 0.05) * 20) / 20);
|
|
3096
3146
|
var pxW = w - 110, pxH = 370;
|
|
3097
3147
|
// Named: the models in or next to the zone (the ones to pick from), then the clearest
|
|
3098
3148
|
// warnings (most made up). The rest are on hover: named all at once they ran together.
|
|
3099
|
-
|
|
3149
|
+
// The zone: finds 90% or more of the real bugs, makes up 15% or less.
|
|
3150
|
+
var zoneX = Math.max(x0, 0.9), zoneY = 0.15;
|
|
3100
3151
|
var near = function (d) { return d.recall >= zoneX - (1 - x0) * 0.08 && d.wrong <= zoneY * 1.4; };
|
|
3101
3152
|
var worstFew = C.overview.slice().sort(function (a, b) { return b.wrong - a.wrong; }).slice(0, 3);
|
|
3102
3153
|
var pts = C.overview.filter(function (d) { return near(d) || worstFew.indexOf(d) !== -1; })
|
|
@@ -3106,12 +3157,13 @@
|
|
|
3106
3157
|
var labels = pts.map(function (d, i) { return offs[i] == null ? null : {model: d.model, recall: d.recall, ty: d.wrong + offs[i] * worst / pxH}; })
|
|
3107
3158
|
.filter(Boolean);
|
|
3108
3159
|
return Plot.plot({
|
|
3109
|
-
|
|
3160
|
+
// marginBottom: room for the axis label under the tick labels, which it ran into.
|
|
3161
|
+
width: w, height: 456, style: style, grid: true, inset: 16, marginRight: 50, marginTop: 30, marginBottom: 46,
|
|
3110
3162
|
x: {label: 'Finds real bugs in our code →', domain: [x0, 1], tickFormat: '.0%'},
|
|
3111
3163
|
y: {label: '↑ Makes fewer things up', domain: [0, worst], reverse: true, tickFormat: '.0%'},
|
|
3112
3164
|
marks: [
|
|
3113
3165
|
// The zone a reviewer should be in: outlined, so it reads on a dark ground too.
|
|
3114
|
-
Plot.rect([{x1:
|
|
3166
|
+
Plot.rect([{x1: zoneX, x2: 1, y1: 0, y2: zoneY}], {x1: 'x1', x2: 'x2', y1: 'y1', y2: 'y2',
|
|
3115
3167
|
fill: good, fillOpacity: 0.12, stroke: good, strokeOpacity: 0.5, strokeDasharray: '4 3'}),
|
|
3116
3168
|
Plot.dot(C.overview, {x: 'recall', y: 'wrong', r: 6, fill: good, stroke: ink, strokeWidth: 0.5}),
|
|
3117
3169
|
// A halo in the panel's colour keeps each name legible over dots, grid lines and the zone.
|
|
@@ -3205,19 +3257,22 @@
|
|
|
3205
3257
|
var lo = Math.max(1, Math.min.apply(null, s) * 0.8), hi = Math.max.apply(null, s) * 1.25;
|
|
3206
3258
|
var y0 = Math.max(0, Math.floor((Math.min.apply(null, C.speed.map(function (d) { return d.recall; })) - 0.05) * 20) / 20);
|
|
3207
3259
|
var pxW = w - 110, pxH = 340;
|
|
3208
|
-
|
|
3260
|
+
// The zone: 90% or more of the findings, in 40 seconds or less. Named: the models in
|
|
3261
|
+
// or next to it, as on Who to trust; the rest are on hover.
|
|
3262
|
+
var zoneS = Math.min(hi, Math.max(lo, 40)), zoneR = Math.max(y0, 0.9);
|
|
3263
|
+
var pts = C.speed.filter(function (d) { return d.seconds <= zoneS * 1.25 && d.recall >= zoneR - (1 - y0) * 0.08; })
|
|
3264
|
+
.sort(function (a, b) { return b.recall - a.recall; });
|
|
3209
3265
|
var offs = spread(pts, function (d) { return (Math.log(d.seconds) - Math.log(lo)) / (Math.log(hi) - Math.log(lo)) * pxW; },
|
|
3210
3266
|
function (d) { return (1 - d.recall) / (1 - y0) * pxH; });
|
|
3211
3267
|
var labels = pts.map(function (d, i) { return offs[i] == null ? null : {model: d.model, seconds: d.seconds, ty: d.recall - offs[i] * (1 - y0) / pxH}; })
|
|
3212
3268
|
.filter(Boolean);
|
|
3213
3269
|
return Plot.plot({
|
|
3214
|
-
width: w, height:
|
|
3270
|
+
width: w, height: 436, style: style, grid: true, inset: 16, marginRight: 50, marginTop: 30, marginBottom: 46,
|
|
3215
3271
|
x: {type: 'log', domain: [lo, hi], label: 'Median seconds per answer on our tasks →'},
|
|
3216
3272
|
y: {domain: [y0, 1], label: '↑ Findings located', tickFormat: '.0%'},
|
|
3217
3273
|
marks: [
|
|
3218
|
-
//
|
|
3219
|
-
|
|
3220
|
-
Plot.rect([{x1: lo, x2: Math.sqrt(lo * hi), y1: y0 + (1 - y0) * 3 / 4, y2: 1}], {x1: 'x1', x2: 'x2', y1: 'y1', y2: 'y2',
|
|
3274
|
+
// Outlined, so it reads on a dark ground too.
|
|
3275
|
+
Plot.rect([{x1: lo, x2: zoneS, y1: zoneR, y2: 1}], {x1: 'x1', x2: 'x2', y1: 'y1', y2: 'y2',
|
|
3221
3276
|
fill: good, fillOpacity: 0.12, stroke: good, strokeOpacity: 0.5, strokeDasharray: '4 3'}),
|
|
3222
3277
|
Plot.dot(C.speed, {x: 'seconds', y: 'recall', r: 6, fill: sky, stroke: ink, strokeWidth: 0.5}),
|
|
3223
3278
|
Plot.text(labels, {x: 'seconds', y: 'ty', text: 'model', fontSize: 11, fill: ink,
|