@wendellresearch-ai/benchwarmers 0.6.5 → 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 +81 -235
- package/ui/bench.css +18 -1
- package/ui/bench.js +106 -28
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
|
@@ -2150,9 +2150,10 @@ def first_run() -> str:
|
|
|
2150
2150
|
text = ("Pick a model your endpoints serve. Onboarding registers it, probes it (4 calls) and runs"
|
|
2151
2151
|
" the internal suite, so it takes minutes and spends calls.")
|
|
2152
2152
|
elif not bench_collections.load():
|
|
2153
|
-
step, head, label = "collection", "Make your first collection", "
|
|
2154
|
-
text = ("The models a panel always asks, and what it is for.
|
|
2155
|
-
|
|
2153
|
+
step, head, label = "collection", "Make your first collection", "Make bench/core"
|
|
2154
|
+
text = ("The models a panel always asks, and what it is for. It opens filled in as bench/core, the"
|
|
2155
|
+
" name the set-ups on Clients use, with the models you onboarded ticked: change anything,"
|
|
2156
|
+
f" then save. Callers ask for it through the gateway at {esc(gateway_url())}.")
|
|
2156
2157
|
else:
|
|
2157
2158
|
return ""
|
|
2158
2159
|
return (f"<section class='problems' id='first-run' data-step='{step}' aria-labelledby='first-head'>"
|
|
@@ -2369,10 +2370,11 @@ SHARE = ("<details class='share'><summary>Share</summary><div class='share-menu'
|
|
|
2369
2370
|
+ "</span></div></details>")
|
|
2370
2371
|
|
|
2371
2372
|
|
|
2372
|
-
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 = "",
|
|
2373
2374
|
retired: bool = True, after: str = "") -> str:
|
|
2374
2375
|
"""A Benchmarks panel: the chart first (drawn by bench.js with Observable
|
|
2375
|
-
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
|
|
2376
2378
|
the chart, `after` right under it."""
|
|
2377
2379
|
return (f"<div class='panel'>{SHARE}<h2>{title}</h2><p class='sub'>{sub}</p>{extra}"
|
|
2378
2380
|
f"<div class='chart' id='{chart_id}'></div>{after}"
|
|
@@ -2380,7 +2382,7 @@ def chart_card(title: str, sub: str, chart_id: str, table: str, key: str, extra:
|
|
|
2380
2382
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
2381
2383
|
+ ("<label><input type='checkbox' data-retired> Show retired</label>" if retired else "") + "</div>"
|
|
2382
2384
|
f"<div class='scroll'>{table}</div></details>"
|
|
2383
|
-
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>")
|
|
2384
2386
|
|
|
2385
2387
|
|
|
2386
2388
|
def _family(task: str) -> str:
|
|
@@ -2440,16 +2442,22 @@ def wrong_table() -> dict:
|
|
|
2440
2442
|
return seen[1]
|
|
2441
2443
|
|
|
2442
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
|
+
|
|
2443
2455
|
def charts_data() -> dict:
|
|
2444
2456
|
"""Everything the Benchmarks charts draw, for bench.js. Registered models
|
|
2445
2457
|
only: the tables keep retired ones behind "Show retired", a chart has no toggle."""
|
|
2446
|
-
import route
|
|
2447
2458
|
import wrong
|
|
2448
2459
|
live = on_bench()
|
|
2449
|
-
|
|
2450
|
-
overview = [{"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(live) if recall.get(m) and recall[m][1] and wr.get(m, {}).get("answers")]
|
|
2460
|
+
wr = wrong_table()
|
|
2453
2461
|
heat = []
|
|
2454
2462
|
for (task, m), v in sorted(suite_scores().items()):
|
|
2455
2463
|
if m in live:
|
|
@@ -2461,7 +2469,7 @@ def charts_data() -> dict:
|
|
|
2461
2469
|
totals = {m: {"rate": r["wrong"] / r["answers"], "answers": r["answers"], "wrong": r["wrong"]}
|
|
2462
2470
|
for m, r in wr.items() if m in live and r["answers"]}
|
|
2463
2471
|
hunt = [{"model": m, **r["hunt"]} for m, r in sorted(wr.items()) if m in live and any(r["hunt"].values())]
|
|
2464
|
-
return {"overview":
|
|
2472
|
+
return {"overview": overview_rows(), "heat": heat, "families": ["hand-built", "claims", "seeded, function",
|
|
2465
2473
|
"seeded, whole file", "real bugs"],
|
|
2466
2474
|
"wrong": bars, "wrongTotals": totals, "kinds": list(wrong.KINDS), "hunt": hunt,
|
|
2467
2475
|
"work": work_rows(), "vision": vision_rows(), "uptime": uptime_data(), "trend": trend_data(),
|
|
@@ -2687,42 +2695,18 @@ def vision_card() -> str:
|
|
|
2687
2695
|
+ (f"<td class='num'>{r['seconds']:.1f}</td>" if r.get("seconds") else "<td class='num muted'>—</td>")
|
|
2688
2696
|
+ "</tr>")
|
|
2689
2697
|
body.append("</tbody></table>")
|
|
2690
|
-
key = """<dl>
|
|
2691
|
-
<dt>The vision tasks</dt><dd>Grouped by where the pictures come from: <b>made</b> with their answer
|
|
2692
|
-
key (<code>suite/vision_tasks.py</code>), your <b>own</b> photos keyed by hand, and items sampled
|
|
2693
|
-
from public datasets with people's answers (<code>suite/vision_sample.py</code>): photographed
|
|
2694
|
-
<b>receipts</b>, app <b>screens</b>, <b>tables</b>, and <b>objects</b> in photos. Each field is
|
|
2695
|
-
<b>right</b>, <b>misread</b> (another value), <b>left out</b>, or a <b>trap taken</b>: a value for
|
|
2696
|
-
something the picture does not show, where the right answer is <code>not shown</code> (a dot marks
|
|
2697
|
-
those fields). They run for every model that takes images; a vision model gets only these, never
|
|
2698
|
-
the code tasks.</dd>
|
|
2699
|
-
<dt>Real work on images</dt><dd>Every seat of a vision panel through the gateway. <b>Checked</b> means
|
|
2700
|
-
its verdict lists claims, each compared with the picture by hand: the claims right and wrong
|
|
2701
|
-
come from these alone. <b>Useful, no claims</b> is a verdict without them, which on thicken's
|
|
2702
|
-
photos means the seat agreed with another seat. Agreement is not a check, since seats fail
|
|
2703
|
-
together, so it is never counted as right.</dd>
|
|
2704
|
-
<dt>The real-work chart</dt><dd>Read as Real work is. The dot is the share of its judged seats your
|
|
2705
|
-
verdict called useful (checked and helped, or useful with no claims), bigger for more judged; the
|
|
2706
|
-
line is where that share likely sits (95%), and the order is by its low end, so a lot of evidence
|
|
2707
|
-
outranks a little. Green when even the low end is over half, red when even the high end is under,
|
|
2708
|
-
grey when there are too few to say. Beside it: useful of judged, how often it answered at all, and
|
|
2709
|
-
the claims checked by hand, the only count of right and wrong. Seats not judged or not answered
|
|
2710
|
-
are in those words and the table, not the dot.</dd>
|
|
2711
|
-
<dt>The number beside a name</dt><dd>How much there is: fields asked on the tasks.</dd>
|
|
2712
|
-
</dl>"""
|
|
2713
2698
|
held = (f"<p class='hint'>The vision tasks wait until {esc(v['held'])}: no run picks them before then"
|
|
2714
2699
|
" (<code>vision_from</code> in <code>config/suite.json</code>; remove it to start them now).</p>"
|
|
2715
2700
|
if v["held"] else "")
|
|
2716
|
-
return (f"<div class='panel'>{SHARE}<h2>Vision</h2><p class='sub'>Reading images: fields read right on pictures
|
|
2717
|
-
" 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 +
|
|
2718
2703
|
"<h3 class='chart-head'>On the vision tasks, field by field</h3><div class='chart' id='chart-vision-fields'></div>"
|
|
2719
2704
|
"<h3 class='chart-head'>On real work with images: judged useful, and how sure that is</h3>"
|
|
2720
2705
|
"<div class='chart' id='chart-vision-work'></div>"
|
|
2721
2706
|
"<details class='key'><summary>Table</summary><div class='toolbar'><input type='search'"
|
|
2722
2707
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
2723
2708
|
"<label><input type='checkbox' data-retired> Show retired</label></div>"
|
|
2724
|
-
f"<div class='scroll'>{''.join(body)}</div></details>"
|
|
2725
|
-
f"<details class='key'><summary>How to read this</summary>{key}</details></div>")
|
|
2709
|
+
f"<div class='scroll'>{''.join(body)}</div></details></div>")
|
|
2726
2710
|
|
|
2727
2711
|
|
|
2728
2712
|
def perf_charts() -> dict:
|
|
@@ -2891,35 +2875,8 @@ def trend_card() -> str:
|
|
|
2891
2875
|
pick = ("<div class='toolbar'><label>Window <select data-trend-days>"
|
|
2892
2876
|
+ "".join(f"<option value='{w}'>last {w} days</option>" for w in TREND_DAYS)
|
|
2893
2877
|
+ f"</select></label></div>{legend}")
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
scored in both: each task as a share of its findings, so a 1-finding claim and an 11-finding file count
|
|
2897
|
-
the same, then averaged over those tasks. Left is lower, right is higher; the shaded band is 10 points
|
|
2898
|
-
either side. Hover for each task's move.</dd>
|
|
2899
|
-
<dt>The groups</dt><dd><b>Lower</b> (or <b>higher</b>) when, on average, the scores moved 10 points or
|
|
2900
|
-
more, on at least two tasks and twice as many as moved the other way, in at least two separate runs.
|
|
2901
|
-
<b>Steady</b> otherwise: a steady bar past the band moved on average, but not on enough tasks or runs.
|
|
2902
|
-
<b>Too little to say</b>, with no bar, when fewer than three tasks can be compared, or fewer than two
|
|
2903
|
-
runs in the window or before it. One bad run, or one task, is never a trend, and nor is one run to
|
|
2904
|
-
compare against. Biggest move first in each group.</dd>
|
|
2905
|
-
<dt>Beside the bar</dt><dd>The evidence: the tasks compared and how many went down or up, then the runs
|
|
2906
|
-
in the window against the runs before it. A model with no task scored both in the window and before it
|
|
2907
|
-
is named under the chart.</dd>
|
|
2908
|
-
<dt>The window</dt><dd>{first} days by default: a paid model is scored about once a week and a free one
|
|
2909
|
-
about every 15 days, six tasks a run, so 7 days holds one run at most and could only say what one
|
|
2910
|
-
afternoon did. {first} days holds 2 to 4 runs. Longer windows hold more runs but leave less before them
|
|
2911
|
-
to compare with (the scores start {esc(t['first'] or 'with the first run')}).</dd>
|
|
2912
|
-
<dt>Runs</dt><dd>Days a model was scored (a task asked twice in a day counts once, as the mean of its
|
|
2913
|
-
scores). In the window, how many sat below or above its earlier scores.</dd>
|
|
2914
|
-
<dt>What is counted</dt><dd>Scores as the run notes recorded them (<code>runs/suite-*.md</code>), under
|
|
2915
|
-
each model's current name; timeouts, errors and refusals are not scores and are left out (Uptime counts
|
|
2916
|
-
them). A task whose number of findings changed starts again. The answers behind the scores are kept
|
|
2917
|
-
from 2026-11-08 in <code>runs/answers/<task>/<model>/</code>; older ones were replaced by
|
|
2918
|
-
the next run and are gone.</dd>
|
|
2919
|
-
</dl>"""
|
|
2920
|
-
return chart_card("Trend", "Who scores lower, or higher, than it used to? Each model against its own earlier"
|
|
2921
|
-
" scores on the same tasks; flagged only when the move holds over several tasks and runs.",
|
|
2922
|
-
"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)
|
|
2923
2880
|
|
|
2924
2881
|
|
|
2925
2882
|
# ---- Uptime: was the provider there when the bench called it? --------------
|
|
@@ -3310,31 +3267,8 @@ def uptime_card() -> str:
|
|
|
3310
3267
|
+ "".join(f"<option value='{w}'>last {w}</option>" for w in UPTIME_DAYS)
|
|
3311
3268
|
+ "</select></label><label>By <select data-uptime-by><option value='providers'>provider</option>"
|
|
3312
3269
|
f"<option value='models'>model</option></select></label></div>{legend}")
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
429), unreachable (a broken stream, no reply), timed out (the gateway's 420 s included), or
|
|
3316
|
-
failed in a way no rule recognises yet, listed under the chart so a rule can be added. A
|
|
3317
|
-
shape a decision model was asked about counts as it said, marked "decided by" with the model
|
|
3318
|
-
and how sure it was (at 0.6 or more; below that it stays unrecognised).</dd>
|
|
3319
|
-
<dt>Left out</dt><dd>Neither up nor down, since it says nothing about the provider: our own
|
|
3320
|
-
quota or allowance used up (a day, a month, credits), our own per-minute or concurrency
|
|
3321
|
-
limit, a call the bench's pacing never sent, our client failing, our LAN hub refusing the
|
|
3322
|
-
connection, and setup problems (sign-in, no such model, no access, our own request).</dd>
|
|
3323
|
-
<dt>Up</dt><dd>It answered. An empty reply still counts as an answer.</dd>
|
|
3324
|
-
<dt>A cell</dt><dd>One day: answered of the calls counted that day. Hollow when nothing was
|
|
3325
|
-
counted that day; hover for the day, the count and the main reasons.</dd>
|
|
3326
|
-
<dt>The % and (n)</dt><dd>Answered of counted over the days shown, and how many were counted:
|
|
3327
|
-
weighted by calls, so a provider's row is every call to any of its models.</dd>
|
|
3328
|
-
<dt>What is counted</dt><dd>Only calls the bench makes anyway: the daily probe, every gateway
|
|
3329
|
-
seat, the suite's own calls once the client's ledger keeps their error text
|
|
3330
|
-
(2026-11-08 on), and every decision model call (<code>metrics/decisions.jsonl</code>), on the
|
|
3331
|
-
connection behind its served name. Each is classified from its error text when this is drawn,
|
|
3332
|
-
so a better rule changes the past too. It is shown only: nothing is routed or retired by it.</dd>
|
|
3333
|
-
</dl>"""
|
|
3334
|
-
return chart_card("Uptime", "Whether the provider was there when the bench called it: the daily probe"
|
|
3335
|
-
" and every real call, one cell a day. Busy, unreachable and timed out count as down;"
|
|
3336
|
-
" our own quota, our own pacing and setup problems are left out.",
|
|
3337
|
-
"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),
|
|
3338
3272
|
extra=pick + "<div class='chart' id='chart-uptime-providers'></div>",
|
|
3339
3273
|
after="".join(odd(w) for w in UPTIME_DAYS))
|
|
3340
3274
|
|
|
@@ -3431,42 +3365,8 @@ def decision_card() -> str:
|
|
|
3431
3365
|
f"<p class='hint'>{'Measured up to ' + esc(when[:16].replace('T', ' ')) + '.' if when else 'Not measured yet.'}"
|
|
3432
3366
|
" The scheduler's <code>decisions</code> job asks each model one question a set a day;"
|
|
3433
3367
|
" <code>python metrics/decide.py bench</code> runs a pass by hand.</p>")
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
<b>Error classes</b>: failed calls' error text (redacted: no keys, ids or emails) and the class the
|
|
3437
|
-
uptime rules give it. <b>Collections</b>: gateway requests and the collection the caller named (not
|
|
3438
|
-
ones a decision model chose). <b>Useful answers</b>: seat answers you judged, with their request,
|
|
3439
|
-
a yes/no question.</dd>
|
|
3440
|
-
<dt>Right of asked</dt><dd>Each item's latest answer; a failed call counts as asked and not right.
|
|
3441
|
-
<b>Accuracy</b> is of the answers given; F1 is the mean over the options of how well each was
|
|
3442
|
-
picked, so always saying the commonest option scores low.</dd>
|
|
3443
|
-
<dt>Brier</dt><dd>How far its probabilities sat from the truth: 0 is perfect; for a choice it is
|
|
3444
|
-
summed over the options, so up to 2. Lower is better, and a sure wrong answer costs most.</dd>
|
|
3445
|
-
<dt>The bars</dt><dd>Right of asked for each model on each set, from 0%. A faint bar is a model
|
|
3446
|
-
under {low} items on that set: shown, not ranked yet. Hover a bar for its Brier.</dd>
|
|
3447
|
-
<dt>Calibration</dt><dd>The second chart, once a model answers at two or more levels of sureness
|
|
3448
|
-
on a set: answers grouped by the probability the model gave its answer
|
|
3449
|
-
(x), against how often those answers were right (y); both axes start just under the lowest
|
|
3450
|
-
answer, not at 0%. On the diagonal, its 80% answers come true
|
|
3451
|
-
80% of the time; under it, it is surer than it should be. A bigger dot is more answers. The gap is
|
|
3452
|
-
the average distance from the diagonal, weighted by answers.</dd>
|
|
3453
|
-
<dt>At {decide.CONFIDENT}</dt><dd>The gateway acts on a choice only when the model is this sure: how
|
|
3454
|
-
many answers were that sure, and how many of those were right.</dd>
|
|
3455
|
-
<dt>Rank</dt><dd>Among models with {low} or more items asked on the set, by right answers
|
|
3456
|
-
smoothed for sample size ((right + 1) / (asked + 2)). Below {low} a model is shown but not ranked,
|
|
3457
|
-
and a use keeps its default (the first onboarded model that takes the question).</dd>
|
|
3458
|
-
<dt>Who each use asks</dt><dd>The shape of a request and a new model's role are ranked on the
|
|
3459
|
-
collection set (one described option of several, for a text), the notice check on useful answers
|
|
3460
|
-
(the only yes/no set). A use asked per request never goes to a <code>:free</code> model while
|
|
3461
|
-
another can answer: their calls come out of OpenRouter's free daily budget, which the suite and
|
|
3462
|
-
the probe share.</dd>
|
|
3463
|
-
<dt>Failures</dt><dd>Calls that came back with no answer, by the uptime classes (setup: no such
|
|
3464
|
-
model, or paywalled). The pass stops a model at its first failure.</dd>
|
|
3465
|
-
</dl>"""
|
|
3466
|
-
return chart_card("Decision models", "The models that answer typed questions (a choice, a yes or no) with"
|
|
3467
|
-
" a probability: how often they are right on the bench's own labelled questions, how far"
|
|
3468
|
-
" to trust their probabilities, and which one each use asks.",
|
|
3469
|
-
"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)
|
|
3470
3370
|
|
|
3471
3371
|
|
|
3472
3372
|
def bughunt_card() -> str:
|
|
@@ -3482,19 +3382,8 @@ def bughunt_card() -> str:
|
|
|
3482
3382
|
+ "".join(f"<td class='num' data-sort='{h[k]}'>{h[k]}/{clean}</td>" for k in ("clean", "false alarm"))
|
|
3483
3383
|
+ "</tr>")
|
|
3484
3384
|
body.append("</tbody></table>")
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
file before a real fix): green found the changed line, yellow named another line,
|
|
3488
|
-
grey said there was no bug.</dd>
|
|
3489
|
-
<dt>Left of the line</dt><dd>The same files untouched: red is a false alarm, claiming a
|
|
3490
|
-
bug in clean code. The longer the red, the less to trust its "there's a bug here".</dd>
|
|
3491
|
-
<dt>Order</dt><dd>On what is proven, best at the top: the low end of the found share's likely
|
|
3492
|
-
range (95%) less the high end of the false alarms'. A few tasks give wide ranges, so they
|
|
3493
|
-
rank low; a model never shown clean code could be raising alarms on all of it.</dd>
|
|
3494
|
-
<dt>Faint bars</dt><dd>Under 10 tasks: shown, but little to go on.</dd>
|
|
3495
|
-
</dl>"""
|
|
3496
|
-
return chart_card("Bug hunt", "Every task with an answer key: bugs found against false alarms on the"
|
|
3497
|
-
" 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))
|
|
3498
3387
|
|
|
3499
3388
|
|
|
3500
3389
|
def _wilson(k: int, n: int, z: float = 1.96) -> tuple[float, float]:
|
|
@@ -3518,27 +3407,21 @@ def work_card() -> str:
|
|
|
3518
3407
|
f"<td class='num' data-sort='{lo:.3f}'>{lo * 100:.0f}–{hi * 100:.0f}%</td>"
|
|
3519
3408
|
f"<td class='num'>{r['real']}</td><td class='num'>{r['false']}</td></tr>")
|
|
3520
3409
|
body.append("</tbody></table>")
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
enumerations) handed to the model and checked afterwards. Benchmarks never count here.</dd>
|
|
3524
|
-
<dt>The dot and its line</dt><dd>The dot is the share that helped, bigger for more judged. The
|
|
3525
|
-
line is where the true share likely sits (a 95% Wilson interval): long for 3 of 3, short for
|
|
3526
|
-
40 of 50. Green when even its low end is over half, red when even its high end is under,
|
|
3527
|
-
grey when there are too few to say.</dd>
|
|
3528
|
-
<dt>Order</dt><dd>By the low end of that line, so volume counts: 40 of 50 ranks above 3 of 3.</dd>
|
|
3529
|
-
<dt>The dashed line</dt><dd>Every model's judged offloads together, for comparison.</dd>
|
|
3530
|
-
<dt>Findings and false claims</dt><dd>Verified one by one in reviews, recorded since 2026-10-17.</dd>
|
|
3531
|
-
</dl>"""
|
|
3532
|
-
return chart_card("Real work", "When a model was given real work and the result was checked: how often"
|
|
3533
|
-
" 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))
|
|
3534
3412
|
|
|
3535
3413
|
|
|
3536
3414
|
def overview_card() -> str:
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
"
|
|
3540
|
-
|
|
3541
|
-
|
|
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)
|
|
3542
3425
|
|
|
3543
3426
|
|
|
3544
3427
|
def suite_card() -> str:
|
|
@@ -3574,22 +3457,8 @@ def suite_card() -> str:
|
|
|
3574
3457
|
+ "".join(cells) + flag + "</tr>")
|
|
3575
3458
|
body.append("</tbody></table>")
|
|
3576
3459
|
body = "".join(body)
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
for that task. The answers were established from source before any
|
|
3580
|
-
model saw them.</dd>
|
|
3581
|
-
<dt>Invented</dt><dd><strong>The column that matters.</strong> Something
|
|
3582
|
-
asserted that is not true of the code. A confident wrong answer costs
|
|
3583
|
-
more than a silent miss.</dd>
|
|
3584
|
-
<dt>timeout / refused / error</dt><dd>Not a score. The model did not
|
|
3585
|
-
answer — a filter, an overloaded service and a slow model are three
|
|
3586
|
-
different facts and are never merged.</dd>
|
|
3587
|
-
<dt>(tag)</dt><dd>The connection it arrives through: its Open WebUI tag, or its endpoint. Models
|
|
3588
|
-
sharing one share its rate limit.</dd>
|
|
3589
|
-
</dl>"""
|
|
3590
|
-
return chart_card("Our code", "Every task on our own code, one square each: green found everything,"
|
|
3591
|
-
" red found nothing, grey did not answer; a dot means it invented something. Hover"
|
|
3592
|
-
" 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)
|
|
3593
3462
|
|
|
3594
3463
|
|
|
3595
3464
|
EXTERNAL = (("LiveBench", "https://livebench.ai/", "contamination-limited questions, refreshed monthly"),
|
|
@@ -3604,8 +3473,8 @@ def external_card() -> str:
|
|
|
3604
3473
|
"""Links, not scores: the External tab showed FuncBench numbers nobody
|
|
3605
3474
|
refreshed and was dropped (2026-10-16). Public leaderboards are read there."""
|
|
3606
3475
|
rows = "".join(f"<li>{ext_link(u, n)} <span class='muted'>— {esc(why)}</span></li>" for n, u, why in EXTERNAL)
|
|
3607
|
-
return ("<div class='panel'><h2>External</h2><p class='sub'>Public leaderboards, for comparison
|
|
3608
|
-
"
|
|
3476
|
+
return ("<div class='panel'><h2>External</h2><p class='sub'>Public leaderboards, for comparison;"
|
|
3477
|
+
" routing uses the bench's own tests.</p>"
|
|
3609
3478
|
f"<ul class='links'>{rows}</ul></div>")
|
|
3610
3479
|
|
|
3611
3480
|
|
|
@@ -3638,26 +3507,8 @@ def wrong_card() -> str:
|
|
|
3638
3507
|
+ (f"<td class='num' data-sort='{r['review_false']}'>{r['review_false']} in {r['reviews']}</td>"
|
|
3639
3508
|
if r["reviews"] else "<td class='num muted' data-sort='-1'>—</td>") + "</tr>")
|
|
3640
3509
|
body.append("</tbody></table>")
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
that could have. Being wrong is why the bench asks several models; asserting a bug
|
|
3644
|
-
that is not there is what sends someone to fix working code. Lower is better.</dd>
|
|
3645
|
-
<dt>Control</dt><dd>A seeded or real-bug control (the clean file) answered FAULT.</dd>
|
|
3646
|
-
<dt>Wrong line</dt><dd>A seeded or real-bug task answered FAULT at a line the change did
|
|
3647
|
-
not touch. Saying NO FAULT on a bug is a miss and is not counted.</dd>
|
|
3648
|
-
<dt>False claim</dt><dd>A claim task whose claim is false, answered VERDICT: TRUE.</dd>
|
|
3649
|
-
<dt>Trap</dt><dd>A hand-built task's planted falsehood, asserted.</dd>
|
|
3650
|
-
<dt>Reviews</dt><dd>False claims recorded in verdicts (<code>offload.py verdict|add
|
|
3651
|
-
--false "..."</code>), over judged offloads. Recorded in structured form only since
|
|
3652
|
-
2026-10-17, so older reviews show none.</dd>
|
|
3653
|
-
<dt>The thin line</dt><dd>Where the true share likely sits (95% range). Long for a few
|
|
3654
|
-
answers: 0 of 2 is not a clean record, and the line shows how bad it could be.</dd>
|
|
3655
|
-
<dt>Order</dt><dd>Worst proven at the top: by the low end of that line. Under 10 answers a
|
|
3656
|
-
model's bar is faint.</dd>
|
|
3657
|
-
</dl>"""
|
|
3658
|
-
return chart_card("Confidently wrong", "Who asserts things that are not true: false alarms on clean"
|
|
3659
|
-
" code, wrong lines, false claims confirmed, traps taken. Shorter is better.",
|
|
3660
|
-
"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))
|
|
3661
3512
|
|
|
3662
3513
|
|
|
3663
3514
|
def benchmarks_tab() -> str:
|
|
@@ -3669,7 +3520,7 @@ def benchmarks_tab() -> str:
|
|
|
3669
3520
|
("trend", "Trend", trend_card), ("performance", "Performance", perf_card),
|
|
3670
3521
|
("work", "Real work", work_card), ("hunt", "Bug hunt", bughunt_card),
|
|
3671
3522
|
("wrong", "Confidently wrong", wrong_card), ("code", "Our code", suite_card),
|
|
3672
|
-
("vision", "Vision", vision_card), ("decisions", "Decision
|
|
3523
|
+
("vision", "Vision", vision_card), ("decisions", "Decision", decision_card),
|
|
3673
3524
|
("external", "External", external_card))
|
|
3674
3525
|
links = "".join(f"<a href='#report-{key}'>{esc(label)}</a>" for key, label, _ in reports)
|
|
3675
3526
|
cards = "".join(card().replace("<div class='panel'", f"<div class='panel report' id='report-{key}'", 1)
|
|
@@ -3707,34 +3558,8 @@ def perf_card() -> str:
|
|
|
3707
3558
|
f"title='p50 {p50:.1f}s'><span style='width:100%'></span></span></td></tr>")
|
|
3708
3559
|
body.append("</tbody></table>")
|
|
3709
3560
|
body = "".join(body)
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
this table.</strong> A full-width bar is the slowest here, not a score
|
|
3713
|
-
out of anything.</dd>
|
|
3714
|
-
<dt>p50 / p95</dt><dd>Median and 95th-percentile seconds to a complete
|
|
3715
|
-
answer. Medians, not means — one timeout makes a mean fiction. p95 is
|
|
3716
|
-
where rate limiting shows itself.</dd>
|
|
3717
|
-
<dt>tiny / large</dt><dd>A one-word prompt measures overhead and
|
|
3718
|
-
queueing; an 18 KB prompt measures throughput. They answer different
|
|
3719
|
-
questions.</dd>
|
|
3720
|
-
<dt>ch/s</dt><dd>Characters per second, from the large prompt only. A
|
|
3721
|
-
proxy for tokens/s, affected by how verbose a model is.</dd>
|
|
3722
|
-
<dt>tok/s</dt><dd>Output tokens per second from the provider's own count, first token to
|
|
3723
|
-
last, large prompt only. Only where the built-in client made the call and the provider
|
|
3724
|
-
reported usage; blank otherwise, never estimated.</dd>
|
|
3725
|
-
<dt>Outcomes</dt><dd>answered / refused / error / timeout.</dd>
|
|
3726
|
-
<dt>Weight it LEAST</dt><dd>Seconds are a wash. Speed only vetoes a model
|
|
3727
|
-
when it is slow enough to change how you work.</dd>
|
|
3728
|
-
</dl>"""
|
|
3729
|
-
key = key.replace("</dl>", """
|
|
3730
|
-
<dt>Did it answer?</dt><dd>Is on Uptime, by day, with our own quota and setup problems left out.</dd>
|
|
3731
|
-
<dt>Speed vs quality</dt><dd>Median seconds per answer on our own suite tasks (real prompts,
|
|
3732
|
-
not the one-word probe) against the share of findings located. Top left is fast and good:
|
|
3733
|
-
the green box is the faster half and the top quarter of findings.</dd>
|
|
3734
|
-
</dl>""")
|
|
3735
|
-
return chart_card("Performance", "What the serving path does — not what the models are worth. One"
|
|
3736
|
-
" reading is weather; the history is climate.", "chart-speed", body, key,
|
|
3737
|
-
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)
|
|
3738
3563
|
|
|
3739
3564
|
|
|
3740
3565
|
def favicon(theme: str) -> str:
|
|
@@ -4016,6 +3841,8 @@ def discover_card() -> str:
|
|
|
4016
3841
|
which asks /api/discover-model for the rest: its routes are where Add is."""
|
|
4017
3842
|
all_rows = _discover_rows()
|
|
4018
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)
|
|
4019
3846
|
hidden = len(all_rows) - len(rows)
|
|
4020
3847
|
reach_css = {"on bench": "ok", "connected": "ok", "easy to add": "warn", "other": ""}
|
|
4021
3848
|
body = ["<table><thead><tr>", th("Model"), th("Rank", True), th("Provider"), th("Reach"), th("Price"),
|
|
@@ -4038,8 +3865,9 @@ def discover_card() -> str:
|
|
|
4038
3865
|
add = ("<button class='btn small' type='button'>Add…</button>"
|
|
4039
3866
|
if any(not x["on_bench_as"] for x in r["routes"]) else "")
|
|
4040
3867
|
body.append(
|
|
4041
|
-
f"<tr class='pick-row
|
|
4042
|
-
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>"
|
|
4043
3871
|
f"<a href='#' class='dmodel'><span class='alias'>{esc(r['name'])}</span></a>"
|
|
4044
3872
|
+ (" " + chip("new", "new", f"first seen {r['first_seen']}") if r["new"] else "")
|
|
4045
3873
|
+ f"<span class='conn'>{esc(r['model'])}"
|
|
@@ -4062,9 +3890,23 @@ def discover_card() -> str:
|
|
|
4062
3890
|
import discover
|
|
4063
3891
|
import sources
|
|
4064
3892
|
ages = [a for a in map(sources.age_s, [*sources.FEEDS, discover.LISTS]) if a is not None]
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
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'>"
|
|
4068
3910
|
"<button class='btn small' type='button' id='d-refresh' title='Fetch FreeLLMAPI, models.dev and"
|
|
4069
3911
|
" every Open WebUI connection's own model list now, instead of waiting for the daily fetch'>"
|
|
4070
3912
|
"Refresh lists</button><span class='muted' id='d-refreshed'>"
|
|
@@ -4083,7 +3925,7 @@ def discover_card() -> str:
|
|
|
4083
3925
|
<dt>Price</dt><dd>Free if FreeLLMAPI lists it (a catalog of free tiers) or models.dev
|
|
4084
3926
|
prices it at 0: models.dev shows the PAID price even where a free tier exists. Otherwise
|
|
4085
3927
|
input / output per million tokens. <em>Unknown</em> where only the provider's own list has
|
|
4086
|
-
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>
|
|
4087
3929
|
<dt>Limits</dt><dd>The free tier's, from FreeLLMAPI: requests and tokens per minute (rpm,
|
|
4088
3930
|
tpm) and per day (rpd, tpd), and roughly how many tokens a month that allows.</dd>
|
|
4089
3931
|
<dt>New</dt><dd>First seen in either list in the last 7 days.</dd>
|
|
@@ -4110,7 +3952,9 @@ def discover_card() -> str:
|
|
|
4110
3952
|
f" models.dev and your providers' own lists, deduped; {hidden:,} paid models on providers you have no route"
|
|
4111
3953
|
f" to are left out. Click a model for everything known about it and the routes it can be"
|
|
4112
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>"
|
|
4113
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>"
|
|
4114
3958
|
f"<details class='key'><summary>How to read this</summary>{key}</details></div>")
|
|
4115
3959
|
|
|
4116
3960
|
|
|
@@ -4218,7 +4062,7 @@ def collections_tab() -> str:
|
|
|
4218
4062
|
" names none</label>"
|
|
4219
4063
|
"<div class='settings'><div><label class='field' for='g-pick-conf'>Only when the model is at least"
|
|
4220
4064
|
"</label><input id='g-pick-conf' type='number' min='0.05' max='1' step='0.05'></div></div>"
|
|
4221
|
-
"<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"
|
|
4222
4066
|
" asks) chooses from the collections' descriptions and tags, and the reply says which and why"
|
|
4223
4067
|
" (<code>bench.picked</code>, <code>X-Bench-Picked</code>). Less sure than this, or switched off,"
|
|
4224
4068
|
" the request is refused with each collection's description and tags, so the caller can choose."
|
|
@@ -5790,7 +5634,9 @@ class Handler(gateway.Handler):
|
|
|
5790
5634
|
if self.path == "/api/needs": # after an action there: that page, not the whole one
|
|
5791
5635
|
with locked.snapshot():
|
|
5792
5636
|
groups, auto = open_actions()
|
|
5793
|
-
|
|
5637
|
+
# `first`: first run's next step now ("" when set up), so a step done goes at once.
|
|
5638
|
+
self._json(200, {"html": actions_page(groups, auto), "count": open_count(groups),
|
|
5639
|
+
"first": first_run()})
|
|
5794
5640
|
return
|
|
5795
5641
|
if path == "/api/pane": # a tab's page, when first opened (bench.js load)
|
|
5796
5642
|
key = q.get("tab", [""])[0]
|
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
|
|
@@ -864,6 +914,7 @@
|
|
|
864
914
|
count.textContent = n.count;
|
|
865
915
|
tab.setAttribute('aria-label', tab.firstChild.textContent + ', ' + n.count + ' open');
|
|
866
916
|
}
|
|
917
|
+
if ('first' in n) showFirst(n.first);
|
|
867
918
|
}).catch(function () { location.reload(); }); // could not ask: the old way
|
|
868
919
|
}
|
|
869
920
|
// A retired model's rows grey out and hide behind Show retired, as the
|
|
@@ -1945,15 +1996,33 @@
|
|
|
1945
1996
|
// ---- first run: no endpoint yet, no model, no collection (serve.first_run) -
|
|
1946
1997
|
// Each waits for its page: it may not have arrived yet.
|
|
1947
1998
|
var first = $('first-run');
|
|
1999
|
+
// A step done (an endpoint added, a model onboarded, a collection saved) moves first run on
|
|
2000
|
+
// at once: refreshNeeds hands its next step, or "" when the install is set up.
|
|
2001
|
+
function showFirst(html) {
|
|
2002
|
+
var now = $('first-run');
|
|
2003
|
+
if ((now ? now.outerHTML : '') === (html || '')) return;
|
|
2004
|
+
if (now) now.remove();
|
|
2005
|
+
if (html) document.querySelector('main').insertAdjacentHTML('afterbegin', html);
|
|
2006
|
+
first = $('first-run');
|
|
2007
|
+
}
|
|
1948
2008
|
function firstStep() {
|
|
1949
2009
|
if (first.dataset.step === 'onboard') { $('onboard-open').click(); return; }
|
|
1950
|
-
|
|
2010
|
+
// The first collection comes filled in as bench/core, the name the docs, CLIENTS.md and
|
|
2011
|
+
// the Clients page use: up to four of the models just onboarded ticked (two to four is
|
|
2012
|
+
// what a panel wants), never an image generator. Nothing is made until it is saved.
|
|
2013
|
+
if (first.dataset.step === 'collection') {
|
|
2014
|
+
show('collections').then(function () {
|
|
2015
|
+
var members = C.models.filter(function (a) { return fact(a).role !== 'image'; }).slice(0, 4);
|
|
2016
|
+
fillForm(null, {name: 'core', description: 'General second opinions: any question, several models at once.',
|
|
2017
|
+
tags: ['general', 'second-opinion'], members: members});
|
|
2018
|
+
c.name.focus();
|
|
2019
|
+
});
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
1951
2022
|
show('endpoints').then(function () { openEndpoint('add'); });
|
|
1952
2023
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
if (first.dataset.step === 'endpoint') firstStep(); // straight into Add: an empty bench offers nothing else
|
|
1956
|
-
}
|
|
2024
|
+
on('click', 'first-go', firstStep); // on the document: a new step brings a new button
|
|
2025
|
+
if (first && first.dataset.step === 'endpoint') firstStep(); // straight into Add: an empty bench offers nothing else
|
|
1957
2026
|
|
|
1958
2027
|
// ---- collections: what the bench gateway runs together (its own tab) -----
|
|
1959
2028
|
// Its elements, the popup's too, are in the page: found again each time it arrives.
|
|
@@ -2150,13 +2219,16 @@
|
|
|
2150
2219
|
open = null;
|
|
2151
2220
|
}
|
|
2152
2221
|
on('close', 'c-form', function () { open = null; });
|
|
2153
|
-
|
|
2222
|
+
// `row`: a saved collection, opened to edit. `draft`: a new one's values filled in
|
|
2223
|
+
// beforehand (first run's core), still new until saved.
|
|
2224
|
+
function fillForm(row, draft) {
|
|
2154
2225
|
c.errors.textContent = ''; c.done.textContent = ''; c.status.textContent = '';
|
|
2155
2226
|
open = row ? row.name : null; c.del.hidden = !row;
|
|
2156
2227
|
c.head.textContent = row ? 'bench/' + row.name : 'New collection';
|
|
2157
2228
|
if (!c.form.open) c.form.showModal();
|
|
2158
|
-
|
|
2159
|
-
c.
|
|
2229
|
+
var src = row || draft;
|
|
2230
|
+
c.name.value = src ? src.name : ''; c.name.readOnly = !!row;
|
|
2231
|
+
c.desc.value = src ? src.description : ''; c.tags.value = src ? src.tags.join(', ') : '';
|
|
2160
2232
|
c.adHoc.checked = !!row && row.ad_hoc;
|
|
2161
2233
|
c.ownCalls.value = row && row.max_calls !== undefined ? row.max_calls : '';
|
|
2162
2234
|
c.ownExp.value = row && row.max_explorers !== undefined ? row.max_explorers : '';
|
|
@@ -2164,7 +2236,7 @@
|
|
|
2164
2236
|
drawTagPicks();
|
|
2165
2237
|
c.find.value = ''; c.show.value = ''; c.prov.value = '';
|
|
2166
2238
|
$('c-refresh-out').textContent = '';
|
|
2167
|
-
drawMembers(
|
|
2239
|
+
drawMembers(src ? src.members : []);
|
|
2168
2240
|
c.shape.textContent = '';
|
|
2169
2241
|
c.shape.appendChild(el('option', {value: ''}, 'automatic'));
|
|
2170
2242
|
C.shapes.forEach(function (s) {
|
|
@@ -2299,6 +2371,7 @@
|
|
|
2299
2371
|
// above the list, as onboarding and retiring do.
|
|
2300
2372
|
function finished(message) {
|
|
2301
2373
|
c.done.textContent = message; c.status.textContent = message; loadCollections(true);
|
|
2374
|
+
refreshNeeds(); // first run's last step may be done
|
|
2302
2375
|
setTimeout(closeForm, 1200);
|
|
2303
2376
|
}
|
|
2304
2377
|
on('click', 'c-delete', function () {
|
|
@@ -3068,12 +3141,13 @@
|
|
|
3068
3141
|
// Who to trust: finds real bugs (right) vs makes things up (up = less).
|
|
3069
3142
|
put('chart-overview', function (w) {
|
|
3070
3143
|
if (!C.overview.length) return el('p', {class: 'hint'}, 'No model has both suite scores and answers yet.');
|
|
3071
|
-
var worst = Math.max(0.
|
|
3144
|
+
var worst = Math.max(0.15, Math.max.apply(null, C.overview.map(function (d) { return d.wrong; })));
|
|
3072
3145
|
var x0 = Math.max(0, Math.floor((Math.min.apply(null, C.overview.map(function (d) { return d.recall; })) - 0.05) * 20) / 20);
|
|
3073
3146
|
var pxW = w - 110, pxH = 370;
|
|
3074
3147
|
// Named: the models in or next to the zone (the ones to pick from), then the clearest
|
|
3075
3148
|
// warnings (most made up). The rest are on hover: named all at once they ran together.
|
|
3076
|
-
|
|
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;
|
|
3077
3151
|
var near = function (d) { return d.recall >= zoneX - (1 - x0) * 0.08 && d.wrong <= zoneY * 1.4; };
|
|
3078
3152
|
var worstFew = C.overview.slice().sort(function (a, b) { return b.wrong - a.wrong; }).slice(0, 3);
|
|
3079
3153
|
var pts = C.overview.filter(function (d) { return near(d) || worstFew.indexOf(d) !== -1; })
|
|
@@ -3083,12 +3157,13 @@
|
|
|
3083
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}; })
|
|
3084
3158
|
.filter(Boolean);
|
|
3085
3159
|
return Plot.plot({
|
|
3086
|
-
|
|
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,
|
|
3087
3162
|
x: {label: 'Finds real bugs in our code →', domain: [x0, 1], tickFormat: '.0%'},
|
|
3088
3163
|
y: {label: '↑ Makes fewer things up', domain: [0, worst], reverse: true, tickFormat: '.0%'},
|
|
3089
3164
|
marks: [
|
|
3090
3165
|
// The zone a reviewer should be in: outlined, so it reads on a dark ground too.
|
|
3091
|
-
Plot.rect([{x1:
|
|
3166
|
+
Plot.rect([{x1: zoneX, x2: 1, y1: 0, y2: zoneY}], {x1: 'x1', x2: 'x2', y1: 'y1', y2: 'y2',
|
|
3092
3167
|
fill: good, fillOpacity: 0.12, stroke: good, strokeOpacity: 0.5, strokeDasharray: '4 3'}),
|
|
3093
3168
|
Plot.dot(C.overview, {x: 'recall', y: 'wrong', r: 6, fill: good, stroke: ink, strokeWidth: 0.5}),
|
|
3094
3169
|
// A halo in the panel's colour keeps each name legible over dots, grid lines and the zone.
|
|
@@ -3182,19 +3257,22 @@
|
|
|
3182
3257
|
var lo = Math.max(1, Math.min.apply(null, s) * 0.8), hi = Math.max.apply(null, s) * 1.25;
|
|
3183
3258
|
var y0 = Math.max(0, Math.floor((Math.min.apply(null, C.speed.map(function (d) { return d.recall; })) - 0.05) * 20) / 20);
|
|
3184
3259
|
var pxW = w - 110, pxH = 340;
|
|
3185
|
-
|
|
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; });
|
|
3186
3265
|
var offs = spread(pts, function (d) { return (Math.log(d.seconds) - Math.log(lo)) / (Math.log(hi) - Math.log(lo)) * pxW; },
|
|
3187
3266
|
function (d) { return (1 - d.recall) / (1 - y0) * pxH; });
|
|
3188
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}; })
|
|
3189
3268
|
.filter(Boolean);
|
|
3190
3269
|
return Plot.plot({
|
|
3191
|
-
width: w, height:
|
|
3270
|
+
width: w, height: 436, style: style, grid: true, inset: 16, marginRight: 50, marginTop: 30, marginBottom: 46,
|
|
3192
3271
|
x: {type: 'log', domain: [lo, hi], label: 'Median seconds per answer on our tasks →'},
|
|
3193
3272
|
y: {domain: [y0, 1], label: '↑ Findings located', tickFormat: '.0%'},
|
|
3194
3273
|
marks: [
|
|
3195
|
-
//
|
|
3196
|
-
|
|
3197
|
-
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',
|
|
3198
3276
|
fill: good, fillOpacity: 0.12, stroke: good, strokeOpacity: 0.5, strokeDasharray: '4 3'}),
|
|
3199
3277
|
Plot.dot(C.speed, {x: 'seconds', y: 'recall', r: 6, fill: sky, stroke: ink, strokeWidth: 0.5}),
|
|
3200
3278
|
Plot.text(labels, {x: 'seconds', y: 'ty', text: 'model', fontSize: 11, fill: ink,
|