@wendellresearch-ai/benchwarmers 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -1
- package/GATEWAY.md +149 -6
- package/README.md +42 -2
- package/USAGE.md +16 -0
- package/VERSION +1 -1
- package/cli.py +9 -1
- package/gateway.py +275 -91
- package/metrics/catalog.py +10 -1
- package/metrics/client.py +537 -127
- package/metrics/cost.py +38 -19
- package/metrics/formats.py +1021 -0
- package/metrics/guard.py +37 -7
- package/metrics/learn.py +19 -6
- package/metrics/ledger.py +148 -0
- package/metrics/model_roles.py +13 -3
- package/metrics/pacing.py +248 -0
- package/metrics/quota.py +383 -0
- package/metrics/route.py +5 -3
- package/package.json +2 -1
- package/perf/run.py +30 -7
- package/scripts/check.py +5 -0
- package/scripts/onboard.py +10 -1
- package/scripts/test_client.py +50 -6
- package/scripts/test_formats.py +549 -0
- package/scripts/test_merged.py +4 -3
- package/scripts/test_quota.py +334 -0
- package/scripts/test_serve_edit.py +10 -5
- package/scripts/test_server_json.py +79 -0
- package/scripts/test_verdict.py +5 -1
- package/scripts/test_vision.py +6 -3
- package/serve.py +104 -36
- package/ui/bench.js +3 -2
package/serve.py
CHANGED
|
@@ -683,7 +683,7 @@ def retry_model(alias: str, who: str = "ui") -> dict:
|
|
|
683
683
|
with (DATA / "perf" / "history.csv").open("a", encoding="utf-8", newline="") as fh:
|
|
684
684
|
csv.writer(fh).writerow([datetime.datetime.now().isoformat(timespec="seconds"), alias, "retry", 1,
|
|
685
685
|
f"{seconds:.2f}", f"{seconds:.2f}", f"{seconds:.2f}", f"{seconds:.2f}",
|
|
686
|
-
0.0, 1, 0, 0, 0, "", "retry from the dashboard"])
|
|
686
|
+
0.0, 1, 0, 0, 0, "", "retry from the dashboard", ""])
|
|
687
687
|
import alerts as _alerts
|
|
688
688
|
served = {a: s for s, a in names.items()}.get(alias, "")
|
|
689
689
|
# Exactly this model's: auto_retire.py raises model:pending:<served> with its alias.
|
|
@@ -1244,10 +1244,11 @@ def connect_page(out: dict) -> str:
|
|
|
1244
1244
|
f"<p><a href='{html.escape(home)}'>Back to Benchwarmers</a>. You can close this tab.</p>")
|
|
1245
1245
|
|
|
1246
1246
|
|
|
1247
|
-
def _clean_limits(raw) -> tuple[dict, list[str]]:
|
|
1248
|
-
"""rpm/rpd/tpm/tpd as positive whole numbers;
|
|
1247
|
+
def _clean_limits(raw, keys=catalog.LIMIT_KEYS) -> tuple[dict, list[str]]:
|
|
1248
|
+
"""rpm/rpd/tpm/tpd (and a connection's rpmo) as positive whole numbers;
|
|
1249
|
+
blank clears one."""
|
|
1249
1250
|
out, errors = {}, []
|
|
1250
|
-
for k in
|
|
1251
|
+
for k in keys:
|
|
1251
1252
|
v = (raw or {}).get(k)
|
|
1252
1253
|
if v in (None, ""):
|
|
1253
1254
|
continue
|
|
@@ -1355,7 +1356,7 @@ def save_connection_limits(connection: str, limits: dict, who: str = "ui") -> di
|
|
|
1355
1356
|
if connection not in known:
|
|
1356
1357
|
return {"ok": False, "errors": [f"{connection!r} is not a connection any model uses;"
|
|
1357
1358
|
f" known: {', '.join(sorted(known))}"]}
|
|
1358
|
-
clean, errors = _clean_limits(limits)
|
|
1359
|
+
clean, errors = _clean_limits(limits, catalog.CONNECTION_LIMIT_KEYS)
|
|
1359
1360
|
if errors:
|
|
1360
1361
|
return {"ok": False, "errors": errors}
|
|
1361
1362
|
data = catalog._read(catalog.LIMITS)
|
|
@@ -1376,7 +1377,8 @@ def connections_view() -> dict:
|
|
|
1376
1377
|
for a in _chat_aliases():
|
|
1377
1378
|
by.setdefault(catalog.connection_of(a), []).append(a)
|
|
1378
1379
|
lim = catalog.connection_limits()
|
|
1379
|
-
return {"ok": True, "errors": [], "keys": list(catalog.
|
|
1380
|
+
return {"ok": True, "errors": [], "keys": list(catalog.CONNECTION_LIMIT_KEYS),
|
|
1381
|
+
"labels": {k: catalog.LIMIT_LABELS[k] for k in catalog.CONNECTION_LIMIT_KEYS},
|
|
1380
1382
|
"connections": [{"name": c, "models": sorted(m), "limits": lim.get(c, {})}
|
|
1381
1383
|
for c, m in sorted(by.items())]}
|
|
1382
1384
|
|
|
@@ -1511,7 +1513,15 @@ def save_gateway_settings(max_calls, max_explorers, who: str = "ui") -> dict:
|
|
|
1511
1513
|
return {"ok": True, "errors": [], "changed": ["config/gateway.json"]}
|
|
1512
1514
|
|
|
1513
1515
|
|
|
1514
|
-
|
|
1516
|
+
# How much an onboarding spends. Full: the 4-call probe, then the suite now.
|
|
1517
|
+
# Quick: the probe only; the scheduled `suite --next` passes fill the suite in,
|
|
1518
|
+
# six tasks a run, so the same calls are spread out, not saved. Quick + ad
|
|
1519
|
+
# hoc: the probe only, and the bench never calls it on its own: the only one
|
|
1520
|
+
# that saves calls, for a metered, capped or trial key.
|
|
1521
|
+
ONBOARD_MODES = {"full": [], "quick": ["--skip-suite"], "quick-adhoc": ["--skip-suite", "--ad-hoc"]}
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def start_onboard(served: str, endpoint: str = "", who: str = "ui", mode: str = "full") -> dict:
|
|
1515
1525
|
"""Kick off `scripts/onboard.py` for a served model, detached, logging to runs/.
|
|
1516
1526
|
|
|
1517
1527
|
The script does its own checks in the right order (is it served, derive
|
|
@@ -1521,12 +1531,12 @@ def start_onboard(served: str, endpoint: str = "", who: str = "ui") -> dict:
|
|
|
1521
1531
|
hands the rest to the script. It runs for minutes and spends calls, so it
|
|
1522
1532
|
runs in the background and the page tails its log.
|
|
1523
1533
|
"""
|
|
1524
|
-
return _spawn_onboard(served, endpoint, who)[0]
|
|
1534
|
+
return _spawn_onboard(served, endpoint, who, mode)[0]
|
|
1525
1535
|
|
|
1526
1536
|
|
|
1527
|
-
def _onboard_refusal(served: str, endpoint: str) -> list[str]:
|
|
1537
|
+
def _onboard_refusal(served: str, endpoint: str, mode: str = "full") -> list[str]:
|
|
1528
1538
|
"""What start_onboard refuses without a call (its docstring)."""
|
|
1529
|
-
errors = []
|
|
1539
|
+
errors = [] if mode in ONBOARD_MODES else [f"mode must be one of {', '.join(ONBOARD_MODES)}; got {mode!r}"]
|
|
1530
1540
|
# It goes into a shell command line below, so only the characters model
|
|
1531
1541
|
# ids actually use -- not merely "no spaces".
|
|
1532
1542
|
if not re.fullmatch(r"[\w./:@+-]{1,200}", served):
|
|
@@ -1549,18 +1559,20 @@ def _onboard_refusal(served: str, endpoint: str) -> list[str]:
|
|
|
1549
1559
|
return errors
|
|
1550
1560
|
|
|
1551
1561
|
|
|
1552
|
-
def _spawn_onboard(served: str, endpoint: str = "", who: str = "ui"
|
|
1562
|
+
def _spawn_onboard(served: str, endpoint: str = "", who: str = "ui",
|
|
1563
|
+
mode: str = "full") -> tuple[dict, subprocess.Popen | None]:
|
|
1553
1564
|
"""start_onboard's result, and the process (None when refused) for a caller that waits on it."""
|
|
1554
1565
|
served = (served or "").strip()
|
|
1555
1566
|
endpoint = (endpoint or "").strip() or endpoints.default()
|
|
1556
|
-
errors = _onboard_refusal(served, endpoint)
|
|
1567
|
+
errors = _onboard_refusal(served, endpoint, mode)
|
|
1557
1568
|
if errors:
|
|
1558
1569
|
return {"ok": False, "errors": errors}, None
|
|
1559
1570
|
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
1560
1571
|
safe = re.sub(r"[^A-Za-z0-9._-]+", "-", served)
|
|
1561
1572
|
log = DATA / "runs" / f"onboard-{stamp}-{safe}.log"
|
|
1562
1573
|
from run import bench_python
|
|
1563
|
-
cmd = [bench_python(), str(ROOT / "scripts" / "onboard.py"), served, "--endpoint", endpoint
|
|
1574
|
+
cmd = [bench_python(), str(ROOT / "scripts" / "onboard.py"), served, "--endpoint", endpoint,
|
|
1575
|
+
*ONBOARD_MODES[mode]]
|
|
1564
1576
|
# The log handle goes straight to the child, unbuffered, so the page sees
|
|
1565
1577
|
# each step as it prints. The old `cmd /c ... > log & echo` wrapper with
|
|
1566
1578
|
# DETACHED_PROCESS sent the output to a console window instead, so every
|
|
@@ -1583,7 +1595,7 @@ def _spawn_onboard(served: str, endpoint: str = "", who: str = "ui") -> tuple[di
|
|
|
1583
1595
|
|
|
1584
1596
|
threading.Thread(target=sentinel, daemon=True).start()
|
|
1585
1597
|
_log_change(datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
1586
|
-
who, "", "onboard", None, served)
|
|
1598
|
+
who, "", "onboard" + ("" if mode == "full" else f" ({mode})"), None, served)
|
|
1587
1599
|
return {"ok": True, "errors": [], "log": log.name}, proc
|
|
1588
1600
|
|
|
1589
1601
|
|
|
@@ -1627,20 +1639,21 @@ def queue_onboard(items, who: str = "ui") -> dict:
|
|
|
1627
1639
|
it = it if isinstance(it, dict) else {}
|
|
1628
1640
|
served = str(it.get("served") or "").strip()
|
|
1629
1641
|
endpoint = str(it.get("endpoint") or "").strip() or endpoints.default()
|
|
1630
|
-
|
|
1631
|
-
|
|
1642
|
+
mode = str(it.get("mode") or "full")
|
|
1643
|
+
errors += _onboard_refusal(served, endpoint, mode)
|
|
1644
|
+
picked.append((served, endpoint, mode))
|
|
1632
1645
|
with _OQ_LOCK:
|
|
1633
1646
|
waiting = {(i["served"], i["endpoint"]) for i in _OQ["items"] if i["state"] in ("queued", "running")}
|
|
1634
|
-
errors += [f"{s} from {e} is already in the queue" for s, e in picked if (s, e) in waiting]
|
|
1635
|
-
if len(
|
|
1647
|
+
errors += [f"{s} from {e} is already in the queue" for s, e, _ in picked if (s, e) in waiting]
|
|
1648
|
+
if len({(s, e) for s, e, _ in picked}) < len(picked):
|
|
1636
1649
|
errors.append("a model is picked twice")
|
|
1637
1650
|
if errors:
|
|
1638
1651
|
return {"ok": False, "errors": errors}
|
|
1639
1652
|
if _OQ["worker"] is None:
|
|
1640
1653
|
_OQ["items"] = []
|
|
1641
1654
|
_OQ["stop"] = False # added after a Stop: these run
|
|
1642
|
-
_OQ["items"] += [{"served": s, "endpoint": e, "state": "queued", "alias": None, "log": None,
|
|
1643
|
-
"exit": None, "note": ""} for s, e in picked]
|
|
1655
|
+
_OQ["items"] += [{"served": s, "endpoint": e, "mode": m, "state": "queued", "alias": None, "log": None,
|
|
1656
|
+
"exit": None, "note": ""} for s, e, m in picked]
|
|
1644
1657
|
if _OQ["worker"] is None:
|
|
1645
1658
|
_OQ["worker"] = threading.Thread(target=_onboard_worker, args=(who,), daemon=True)
|
|
1646
1659
|
_OQ["worker"].start()
|
|
@@ -1692,7 +1705,7 @@ def _onboard_worker(who: str) -> None:
|
|
|
1692
1705
|
return
|
|
1693
1706
|
nxt["state"] = "running"
|
|
1694
1707
|
try:
|
|
1695
|
-
res, proc = _spawn_onboard(nxt["served"], nxt["endpoint"], who)
|
|
1708
|
+
res, proc = _spawn_onboard(nxt["served"], nxt["endpoint"], who, nxt.get("mode", "full"))
|
|
1696
1709
|
if proc is None: # refused now: the bench changed since it was queued
|
|
1697
1710
|
state, note, code = "failed", "; ".join(res["errors"]), None
|
|
1698
1711
|
else:
|
|
@@ -2398,7 +2411,7 @@ def perf_card() -> str:
|
|
|
2398
2411
|
if r["model"] in live}
|
|
2399
2412
|
widest = max((float(r["p50_s"]) for r in latest.values()), default=1) or 1
|
|
2400
2413
|
body = ["<table><thead><tr>" + th("Model") + th("Prompt") + th("p50", True)
|
|
2401
|
-
+ th("p95", True) + th("ch/s", True) + th("Outcomes")
|
|
2414
|
+
+ th("p95", True) + th("ch/s", True) + th("tok/s", True) + th("Outcomes")
|
|
2402
2415
|
+ "<th>p50, relative</th></tr></thead><tbody>"]
|
|
2403
2416
|
for (model, prompt), row in sorted(latest.items()):
|
|
2404
2417
|
p50 = float(row["p50_s"])
|
|
@@ -2411,6 +2424,7 @@ def perf_card() -> str:
|
|
|
2411
2424
|
f"<td class='num'>{p50:.1f}s</td>"
|
|
2412
2425
|
f"<td class='num'>{float(row['p95_s']):.1f}s</td>"
|
|
2413
2426
|
f"<td class='num'>{esc(row['chars_per_s'])}</td>"
|
|
2427
|
+
f"<td class='num'>{esc(row.get('tokens_per_s') or '—')}</td>"
|
|
2414
2428
|
f"<td class='{'err' if bad else ''}'>{esc(oc)}</td>"
|
|
2415
2429
|
f"<td><span class='meter' style='width:{width}px' "
|
|
2416
2430
|
f"title='p50 {p50:.1f}s'><span style='width:100%'></span></span></td></tr>")
|
|
@@ -2428,6 +2442,9 @@ def perf_card() -> str:
|
|
|
2428
2442
|
questions.</dd>
|
|
2429
2443
|
<dt>ch/s</dt><dd>Characters per second, from the large prompt only. A
|
|
2430
2444
|
proxy for tokens/s, affected by how verbose a model is.</dd>
|
|
2445
|
+
<dt>tok/s</dt><dd>Output tokens per second from the provider's own count, first token to
|
|
2446
|
+
last, large prompt only. Only where the built-in client made the call and the provider
|
|
2447
|
+
reported usage; blank otherwise, never estimated.</dd>
|
|
2431
2448
|
<dt>Outcomes</dt><dd>answered / refused / error / timeout.</dd>
|
|
2432
2449
|
<dt>Weight it LEAST</dt><dd>Seconds are a wash. Speed only vetoes a model
|
|
2433
2450
|
when it is slow enough to change how you work.</dd>
|
|
@@ -2549,8 +2566,9 @@ def gateway_card(limit: int = 30) -> str:
|
|
|
2549
2566
|
explorers, then the merger if the reply was merged. Green answered, amber came back empty, red failed. ✓ judged
|
|
2550
2567
|
useful, ✗ judged not useful; no mark means the verdict is still owed.
|
|
2551
2568
|
Hover a chip for its seat, timing and call id; click it to read the answer
|
|
2552
|
-
and record a verdict. A grey chip with a dash is a member not asked
|
|
2553
|
-
|
|
2569
|
+
and record a verdict. A grey chip with a dash is a member not asked: the request
|
|
2570
|
+
carried images it cannot take, or it is a vision specialist and the request
|
|
2571
|
+
carried none. Hover it for why.</dd>
|
|
2554
2572
|
<dt>Asked by</dt><dd>The gateway key that made the request, or <code>local</code>
|
|
2555
2573
|
when no key is asked for.</dd>
|
|
2556
2574
|
<dt>Owed</dt><dd>Seats without a verdict. Each is recorded as an unjudged
|
|
@@ -3219,7 +3237,9 @@ def endpoints_page() -> str:
|
|
|
3219
3237
|
"<div class='panel form-panel'><h2>Rate limits per connection</h2>"
|
|
3220
3238
|
"<p class='sub'>What a key behind each connection allows, from the provider's docs or"
|
|
3221
3239
|
" freellmapi.co/models. Models on that connection share it; a model's own limits (its"
|
|
3222
|
-
" popup) win, and limits the bench learns from errors only ever lower these
|
|
3240
|
+
" popup) win, and limits the bench learns from errors only ever lower these. rpmo is calls"
|
|
3241
|
+
" a calendar month (UTC), for a trial key's monthly cap. The built-in client keeps to them:"
|
|
3242
|
+
" when one is used up it waits if the call can still finish in time, else it does not send.</p>"
|
|
3223
3243
|
"<div id='l-list'></div><div class='errors' id='l-errors' role='alert'></div>"
|
|
3224
3244
|
"<div class='done' id='l-done'></div></div>")
|
|
3225
3245
|
|
|
@@ -3270,11 +3290,37 @@ def models_page() -> str:
|
|
|
3270
3290
|
|
|
3271
3291
|
|
|
3272
3292
|
def quotas_page() -> str:
|
|
3273
|
-
"""What limits each connection,
|
|
3274
|
-
|
|
3275
|
-
|
|
3293
|
+
"""What limits each connection, who is resting after one, what providers
|
|
3294
|
+
last said was left (the built-in client's quota records, metrics/quota.py)
|
|
3295
|
+
and what the bench itself sent (its usage ledger, metrics/ledger.py)."""
|
|
3276
3296
|
import learn
|
|
3297
|
+
import ledger
|
|
3298
|
+
import quota
|
|
3277
3299
|
now = datetime.datetime.now().astimezone()
|
|
3300
|
+
rec_rows = []
|
|
3301
|
+
for r in sorted(quota.live(), key=lambda r: (r.get("connection", ""), r.get("alias", ""), r.get("metric", ""))):
|
|
3302
|
+
try:
|
|
3303
|
+
age = int((now - datetime.datetime.fromisoformat(r["observed_at"])).total_seconds() // 60)
|
|
3304
|
+
reset = datetime.datetime.fromisoformat(r["reset_at"]).astimezone().strftime("%Y-%m-%d %H:%M")
|
|
3305
|
+
except (KeyError, TypeError, ValueError):
|
|
3306
|
+
continue
|
|
3307
|
+
num = lambda v: f"{v:,}" if isinstance(v, int) else "—" # noqa: E731
|
|
3308
|
+
rec_rows.append(f"<tr><td>{esc(r.get('connection', ''))}</td><td>{named(r.get('alias', ''))}</td>"
|
|
3309
|
+
f"<td>{esc(r.get('metric', ''))} / {esc(r.get('window', ''))}</td>"
|
|
3310
|
+
f"<td class='num'>{num(r.get('remaining'))}</td><td class='num'>{num(r.get('limit'))}</td>"
|
|
3311
|
+
f"<td class='mono'>{esc(reset)}</td>"
|
|
3312
|
+
f"<td>{esc(r.get('source', ''))}, {age} min ago</td></tr>")
|
|
3313
|
+
t = now.timestamp()
|
|
3314
|
+
sent: dict = {}
|
|
3315
|
+
for row in ledger.rows(min(t - 86400, ledger.month_start(t))):
|
|
3316
|
+
s = sent.setdefault(row.get("connection") or "?", [0, 0, 0, 0])
|
|
3317
|
+
n, tok = row.get("attempts") or 0, (row.get("tokens_in") or 0) + (row.get("tokens_out") or 0)
|
|
3318
|
+
if row["t"] >= t - 86400:
|
|
3319
|
+
s[0], s[1] = s[0] + n, s[1] + tok
|
|
3320
|
+
if row["t"] >= ledger.month_start(t):
|
|
3321
|
+
s[2], s[3] = s[2] + n, s[3] + tok
|
|
3322
|
+
sent_rows = [f"<tr><td>{esc(c)}</td>" + "".join(f"<td class='num'>{v:,}</td>" for v in s) + "</tr>"
|
|
3323
|
+
for c, s in sorted(sent.items())]
|
|
3278
3324
|
cool_rows = []
|
|
3279
3325
|
for alias, c in sorted(learn.cooldowns().items(), key=lambda kv: kv[1].get("until", "")):
|
|
3280
3326
|
try:
|
|
@@ -3285,7 +3331,7 @@ def quotas_page() -> str:
|
|
|
3285
3331
|
f"<td class='num' data-sort='{left}'>{left} min</td>"
|
|
3286
3332
|
f"<td>{'stated by the provider' if c.get('stated') else 'default wait'}</td></tr>")
|
|
3287
3333
|
entered = catalog.connection_limits()
|
|
3288
|
-
keys = catalog.
|
|
3334
|
+
keys = catalog.CONNECTION_LIMIT_KEYS
|
|
3289
3335
|
lim_rows = [f"<tr><td>{esc(conn)}</td>" + "".join(
|
|
3290
3336
|
f"<td class='num'>{lim[k]:,}</td>" if lim.get(k) else "<td class='num muted'>—</td>" for k in keys)
|
|
3291
3337
|
+ "<td>you</td></tr>" for conn, lim in sorted(entered.items()) if any(lim.get(k) for k in keys)]
|
|
@@ -3300,10 +3346,23 @@ def quotas_page() -> str:
|
|
|
3300
3346
|
+ f"<td>learned {esc(ev.get('at', '')[:10])} ({esc(ev.get('where', ''))})</td></tr>")
|
|
3301
3347
|
return (
|
|
3302
3348
|
"<div class='panel'><h2>Quota management</h2><p class='sub'>What is left on each connection and"
|
|
3303
|
-
" when it resets,
|
|
3304
|
-
"
|
|
3305
|
-
"
|
|
3306
|
-
" in CLI Proxy API's own console.</p></div>"
|
|
3349
|
+
" when it resets, from the rate-limit headers and 429 replies the built-in client sees, and what"
|
|
3350
|
+
" the bench sent itself, from its usage ledger; who is resting after a rate limit, and every"
|
|
3351
|
+
" limit known. The built-in client keeps to all of these. Calls through the llm CLI are in"
|
|
3352
|
+
" neither record. Quotas of OAuth credentials stay in CLI Proxy API's own console.</p></div>"
|
|
3353
|
+
"<div class='panel'><h3 class='chart-head'>What providers said is left · " + str(len(rec_rows)) + "</h3>"
|
|
3354
|
+
+ ("<table><thead><tr>" + th("Connection") + th("Model") + th("Counts") + th("Left", True)
|
|
3355
|
+
+ th("Of", True) + th("Resets") + th("From") + f"</tr></thead><tbody>{''.join(rec_rows)}</tbody></table>"
|
|
3356
|
+
if rec_rows else "<p class='muted'>Nothing in force: no provider has stated a limit whose reset"
|
|
3357
|
+
" is still ahead.</p>")
|
|
3358
|
+
+ "</div>"
|
|
3359
|
+
"<div class='panel'><h3 class='chart-head'>What the bench sent</h3><p class='sub'>Requests"
|
|
3360
|
+
" (retries included) and tokens per connection, from the usage ledger.</p>"
|
|
3361
|
+
+ ("<table><thead><tr>" + th("Connection") + th("Requests, 24 h", True) + th("Tokens, 24 h", True)
|
|
3362
|
+
+ th("Requests, this month", True) + th("Tokens, this month", True)
|
|
3363
|
+
+ f"</tr></thead><tbody>{''.join(sent_rows)}</tbody></table>"
|
|
3364
|
+
if sent_rows else "<p class='muted'>No call in the ledger this month.</p>")
|
|
3365
|
+
+ "</div>"
|
|
3307
3366
|
"<div class='panel'><h3 class='chart-head'>Resting after a rate limit · " + str(len(cool_rows)) + "</h3>"
|
|
3308
3367
|
+ ("<table><thead><tr>" + th("Model") + th("Connection") + th("Back in", True) + th("Wait")
|
|
3309
3368
|
+ f"</tr></thead><tbody>{''.join(cool_rows)}</tbody></table>" if cool_rows
|
|
@@ -3476,8 +3535,10 @@ def editor() -> str:
|
|
|
3476
3535
|
"<div class='actions'><button class='btn' id='co-save' type='button'>Save options</button></div>"
|
|
3477
3536
|
"</details>"
|
|
3478
3537
|
"<label class='check'><input type='checkbox' id='m-adhoc'> Ad hoc only</label>"
|
|
3479
|
-
"<p class='rules'>
|
|
3480
|
-
"
|
|
3538
|
+
"<p class='rules'>Never chosen by the bench on its own: no explorer seat, no router"
|
|
3539
|
+
" pick, no scheduled suite, perf or availability runs. Still called whenever you choose"
|
|
3540
|
+
" it: as a collection member (or merger), with <code>with</code>, or with"
|
|
3541
|
+
" <code>--model</code>. For metered, capped or trial keys.</p>"
|
|
3481
3542
|
"</div>"
|
|
3482
3543
|
"<div class='errors' id='m-errors' role='alert'></div><div class='done' id='m-done'></div>"
|
|
3483
3544
|
"<div class='actions'><button class='btn' id='m-close'>Close</button>"
|
|
@@ -3509,6 +3570,12 @@ def editor() -> str:
|
|
|
3509
3570
|
"<input type='search' id='o-find' placeholder='Search models or connections'"
|
|
3510
3571
|
" aria-label='Search models'>"
|
|
3511
3572
|
"<div class='picker' id='o-picker' role='group' aria-label='Served models'></div>"
|
|
3573
|
+
# serve.ONBOARD_MODES: how much each onboarding spends.
|
|
3574
|
+
"<label class='check'>Onboard as <select id='o-mode'>"
|
|
3575
|
+
"<option value='full'>Full: probe, then the suite now</option>"
|
|
3576
|
+
"<option value='quick'>Quick: probe now, the suite through the scheduled passes</option>"
|
|
3577
|
+
"<option value='quick-adhoc'>Quick + ad hoc: probe only; the bench never calls it on its own"
|
|
3578
|
+
" (metered, capped or trial keys)</option></select></label>"
|
|
3512
3579
|
"</div>"
|
|
3513
3580
|
"<div id='o-run' hidden><p class='served' id='o-result'></p><ol class='steps' id='o-steps'></ol>"
|
|
3514
3581
|
"<details><summary class='muted'>Full log</summary><pre class='log' id='o-log'></pre></details></div>"
|
|
@@ -3755,7 +3822,8 @@ class Handler(gateway.Handler):
|
|
|
3755
3822
|
str(p.get("model") or ""),
|
|
3756
3823
|
endpoint=str(p.get("endpoint") or "")),
|
|
3757
3824
|
"/api/onboard": lambda p: start_onboard(str(p.get("served", "")),
|
|
3758
|
-
str(p.get("endpoint", ""))
|
|
3825
|
+
str(p.get("endpoint", "")),
|
|
3826
|
+
mode=str(p.get("mode") or "full")),
|
|
3759
3827
|
"/api/onboard-queue": lambda p: queue_onboard(p.get("items")),
|
|
3760
3828
|
"/api/onboard-queue/stop": lambda p: stop_onboard_queue(),
|
|
3761
3829
|
"/api/onboard-queue/clear": lambda p: clear_onboard_queue(),
|
package/ui/bench.js
CHANGED
|
@@ -573,7 +573,8 @@
|
|
|
573
573
|
// model at a time. The popup closes: the header's chip shows the batch, and
|
|
574
574
|
// its Details the queue. Refused, it stays open with the reasons.
|
|
575
575
|
o.start.addEventListener('click', function () {
|
|
576
|
-
var
|
|
576
|
+
var mode = $('o-mode').value;
|
|
577
|
+
var items = Object.keys(chosen).map(function (k) { return Object.assign({mode: mode}, chosen[k]); });
|
|
577
578
|
if (!items.length) return;
|
|
578
579
|
o.errors.textContent = ''; o.start.disabled = o.dismiss.disabled = true; o.start.textContent = 'Starting…';
|
|
579
580
|
post('/api/onboard-queue', {items: items}).then(function (res) {
|
|
@@ -1016,7 +1017,7 @@
|
|
|
1016
1017
|
left.appendChild(el('div', {class: 'base'}, c.models.join(', ')));
|
|
1017
1018
|
var inputs = el('div', {class: 'limits'});
|
|
1018
1019
|
res.keys.forEach(function (k) {
|
|
1019
|
-
var lab = el('label', {}, k + ' ');
|
|
1020
|
+
var lab = el('label', {title: (res.labels || {})[k] || k}, k + ' ');
|
|
1020
1021
|
var box = el('input', {type: 'number', min: '1', 'data-k': k, placeholder: '—'});
|
|
1021
1022
|
if (c.limits[k]) box.value = c.limits[k];
|
|
1022
1023
|
lab.appendChild(box); inputs.appendChild(lab);
|