@wendellresearch-ai/benchwarmers 0.5.0 → 0.5.1
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/PRIVACY.md +9 -2
- package/README.md +9 -5
- package/USAGE.md +51 -19
- package/VERSION +1 -1
- package/metrics/catalog.py +5 -0
- package/metrics/learn.py +25 -1
- package/metrics/quota.py +41 -4
- package/metrics/quotaview.py +366 -34
- package/package.json +1 -1
- package/scripts/test_serve_edit.py +9 -0
- package/serve.py +35 -11
- package/ui/bench.css +31 -10
- package/ui/bench.js +151 -64
package/serve.py
CHANGED
|
@@ -686,6 +686,17 @@ def remove_model(alias: str, reason: str = "", who: str = "ui") -> dict:
|
|
|
686
686
|
return {"ok": True, "errors": [], "changed": changed, "sync": _sync_cli()}
|
|
687
687
|
|
|
688
688
|
|
|
689
|
+
def dismiss_alert(key: str) -> dict:
|
|
690
|
+
"""Got it on a notice (an alert raised with dismiss=True, such as NVIDIA
|
|
691
|
+
changing its stated limits). A state that is still wrong is not dismissed:
|
|
692
|
+
its check clears it when it is put right."""
|
|
693
|
+
import alerts as _alerts
|
|
694
|
+
if not (_alerts.active().get(key) or {}).get("dismiss"):
|
|
695
|
+
return {"ok": False, "errors": [f"{key!r} is not a notice that can be dismissed"]}
|
|
696
|
+
_alerts.clear(key)
|
|
697
|
+
return {"ok": True, "errors": []}
|
|
698
|
+
|
|
699
|
+
|
|
689
700
|
def retry_model(alias: str, who: str = "ui") -> dict:
|
|
690
701
|
"""One fresh call to a model on a retiring alert. An answer is written to
|
|
691
702
|
perf/history.csv as evidence (scripts/auto_retire.py reads it, so the
|
|
@@ -2038,6 +2049,8 @@ def problems() -> str:
|
|
|
2038
2049
|
acts.append(f"<button class='btn small' data-act='open' data-alias='{esc(alias)}'>Open</button>")
|
|
2039
2050
|
acts.append(f"<button class='btn small danger' data-act='retire' data-alias='{esc(alias)}'"
|
|
2040
2051
|
f" data-why='{esc(a.get('message', ''))}'>Retire now</button>")
|
|
2052
|
+
if a.get("dismiss"): # a notice, not a state: it goes once seen
|
|
2053
|
+
acts.append(f"<button class='btn small' data-act='dismiss' data-key='{esc(key)}'>Got it</button>")
|
|
2041
2054
|
items.append((key, a.get("kind", "?"), KIND.get(a.get("kind"), "warn"),
|
|
2042
2055
|
a.get("message", ""), why, "".join(acts)))
|
|
2043
2056
|
cutoff = (today - datetime.timedelta(days=7)).isoformat()
|
|
@@ -3170,8 +3183,10 @@ def collections_tab() -> str:
|
|
|
3170
3183
|
# The panel as it will sit, drawn as the Collections list draws it
|
|
3171
3184
|
# (bench.js recount): a seat per ticked member, explorer seats dashed.
|
|
3172
3185
|
"<div id='c-seats' class='seats' aria-live='polite'></div>"
|
|
3173
|
-
"<input type='search' id='c-member-filter' class='member-filter' spellcheck='false'
|
|
3174
|
-
" placeholder='Filter by model, provider or served name' aria-controls='c-members'
|
|
3186
|
+
"<div class='member-find'><input type='search' id='c-member-filter' class='member-filter' spellcheck='false'"
|
|
3187
|
+
" autocomplete='off' placeholder='Filter by model, provider or served name' aria-controls='c-members'"
|
|
3188
|
+
" aria-describedby='c-count'><select id='c-member-provider' aria-label='Provider' aria-controls='c-members'>"
|
|
3189
|
+
"<option value=''>Every provider</option></select></div>"
|
|
3175
3190
|
"<div id='c-members' class='members' role='group' aria-label='Members'></div>"
|
|
3176
3191
|
"<p class='rules' id='c-count'></p>"
|
|
3177
3192
|
"<label class='field' for='c-shape'>Explorer shape</label><select id='c-shape'></select>"
|
|
@@ -3745,15 +3760,21 @@ def quota_tiles() -> list[dict]:
|
|
|
3745
3760
|
|
|
3746
3761
|
|
|
3747
3762
|
def quota_op(which: str, tag) -> dict:
|
|
3748
|
-
"""Refresh
|
|
3749
|
-
|
|
3763
|
+
"""Refresh on one tile, which picks a free read, one short call or the
|
|
3764
|
+
records (quotaview.refresh): {ok, errors, note, tile}; "order" saves the
|
|
3765
|
+
tiles' order (a list of tags, empty for the default sort)."""
|
|
3750
3766
|
import quotaview
|
|
3751
3767
|
if which == "order":
|
|
3752
3768
|
return quotaview.save_order(tag)
|
|
3753
3769
|
if tag not in {t["id"] for t in quota_tiles()}:
|
|
3754
3770
|
return {"ok": False, "errors": [f"no tile called {tag!r}"]}
|
|
3771
|
+
if which == "about": # the Models popup: how it is reached
|
|
3772
|
+
try:
|
|
3773
|
+
return {"ok": True, "errors": [], **quotaview.about(tag)}
|
|
3774
|
+
except (ValueError, OSError) as exc:
|
|
3775
|
+
return {"ok": False, "errors": [str(exc)]}
|
|
3755
3776
|
try:
|
|
3756
|
-
r =
|
|
3777
|
+
r = quotaview.refresh(tag)
|
|
3757
3778
|
except (ValueError, OSError) as exc: # OSError: URLError and timeouts
|
|
3758
3779
|
return {"ok": False, "errors": [str(exc)]}
|
|
3759
3780
|
if not r.get("ok"):
|
|
@@ -3764,8 +3785,8 @@ def quota_op(which: str, tag) -> dict:
|
|
|
3764
3785
|
|
|
3765
3786
|
def quotas_page() -> str:
|
|
3766
3787
|
"""A tile per provider: what is left and when it resets, shaped by how the
|
|
3767
|
-
provider limits (metrics/quotaview.py), who is resting, Refresh
|
|
3768
|
-
|
|
3788
|
+
provider limits (metrics/quotaview.py), who is resting, one Refresh; the
|
|
3789
|
+
resets of the next week; what the bench sent (its usage ledger,
|
|
3769
3790
|
metrics/ledger.py); every limit known. bench.js draws tiles and charts."""
|
|
3770
3791
|
import learn
|
|
3771
3792
|
import ledger
|
|
@@ -3808,9 +3829,10 @@ def quotas_page() -> str:
|
|
|
3808
3829
|
" Retries count. Calls through the llm CLI are not in the ledger.</p>")
|
|
3809
3830
|
return (
|
|
3810
3831
|
"<div class='panel'><h2>Quota</h2><p class='sub'>What is left with each provider and when it"
|
|
3811
|
-
" resets, and who is resting after a rate limit.
|
|
3812
|
-
"
|
|
3813
|
-
"
|
|
3832
|
+
" resets, and who is resting after a rate limit. Refresh asks the provider for free where it can"
|
|
3833
|
+
" (CLI Proxy API's credentials, OpenRouter); where a provider states its limits only in its replies,"
|
|
3834
|
+
" it sends one 16-token request to read them; elsewhere it reads the bench's own records again."
|
|
3835
|
+
" Hover a Refresh to see which it will do."
|
|
3814
3836
|
" Drag a tile, or move it with the arrow keys, to keep it where you want it.</p>"
|
|
3815
3837
|
"<div class='list-actions'><button class='btn' id='q-all'>Refresh all live</button>"
|
|
3816
3838
|
"<button class='btn' id='q-reorder'>Sort: out first, then soonest reset</button>"
|
|
@@ -3832,6 +3854,7 @@ def quotas_page() -> str:
|
|
|
3832
3854
|
else "<p class='muted'>None known yet.</p>")
|
|
3833
3855
|
+ "</details>"
|
|
3834
3856
|
"<dialog id='q-models' aria-labelledby='q-models-h'><h3 id='q-models-h'></h3>"
|
|
3857
|
+
"<div class='sub q-reach' id='q-models-about'></div>"
|
|
3835
3858
|
"<p class='sub' id='q-models-sub'></p><div class='scroll' id='q-models-list'></div>"
|
|
3836
3859
|
"<div class='list-actions'><button class='btn' id='q-models-close'>Close</button></div></dialog>"
|
|
3837
3860
|
f"<script type='application/json' id='quota-data'>{data}</script>")
|
|
@@ -4347,7 +4370,8 @@ class Handler(gateway.Handler):
|
|
|
4347
4370
|
"/api/gateway-key/remove": lambda p: revoke_gateway_key(str(p.get("name") or "")),
|
|
4348
4371
|
"/api/theme": lambda p: save_theme(str(p.get("theme") or "")),
|
|
4349
4372
|
"/api/quota/refresh": lambda p: quota_op("refresh", str(p.get("tag") or "")),
|
|
4350
|
-
"/api/quota/
|
|
4373
|
+
"/api/quota/about": lambda p: quota_op("about", str(p.get("tag") or "")),
|
|
4374
|
+
"/api/alerts/dismiss": lambda p: dismiss_alert(str(p.get("key") or "")),
|
|
4351
4375
|
"/api/quota/order": lambda p: quota_op("order", p.get("order")),
|
|
4352
4376
|
"/api/restart": lambda p: restart(p.get("force") is True)}
|
|
4353
4377
|
# SAME ORIGIN ONLY. Any page open in the browser can POST a plain form
|
package/ui/bench.css
CHANGED
|
@@ -299,6 +299,12 @@ dialog h2 { margin: 0 0 .2rem; font-size: 1.375rem; font-weight: 700; }
|
|
|
299
299
|
#c-form .seats { min-height: 2rem; }
|
|
300
300
|
#c-form .seat .k.after { margin: 0 0 0 .4rem; }
|
|
301
301
|
:is(dialog, .form-panel) input.member-filter { font-size: .9375rem; padding: .4rem .6rem; margin-bottom: .2rem; }
|
|
302
|
+
#c-form .member-find { display: flex; gap: .5rem; }
|
|
303
|
+
#c-form .member-find input { flex: 1; min-width: 0; }
|
|
304
|
+
#c-form .member-find select { flex: none; width: auto; max-width: 45%; margin-bottom: .2rem; }
|
|
305
|
+
#c-form .seat .seat-x { border: 0; background: none; padding: 0 0 0 .45rem; font: inherit; font-size: 1rem; line-height: 1;
|
|
306
|
+
color: var(--muted); cursor: pointer; }
|
|
307
|
+
#c-form .seat .seat-x:hover, #c-form .seat .seat-x:focus-visible { color: var(--err); }
|
|
302
308
|
:is(dialog, .form-panel) .members input:disabled + span { color: var(--muted); }
|
|
303
309
|
/* Ruled out by the collection's tags (bench.js applyTags): greyed; a ticked
|
|
304
310
|
one stays tickable and is marked, so it can be taken off. */
|
|
@@ -400,21 +406,35 @@ pre.log.answer { max-height: 40vh; }
|
|
|
400
406
|
/* Quota: a tile per provider, as many columns as fit; a group of windows (a
|
|
401
407
|
5-hour and a weekly limit of one model group) boxed together; a meter of
|
|
402
408
|
ten segments filled like a bar. */
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
/* Tiles in a row share a height, so every footer sits on one line; nothing in
|
|
410
|
+
a tile is wider than it: names shorten, values never wrap. */
|
|
411
|
+
.q-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: .75rem; align-items: stretch; margin-top: .75rem; }
|
|
412
|
+
.q-tile { display: flex; flex-direction: column; gap: .5rem; padding: .75rem .85rem; background: var(--panel); min-width: 0;
|
|
405
413
|
border: 1px solid var(--line); border-radius: var(--radius); cursor: grab; }
|
|
406
414
|
.q-tile:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
407
415
|
.q-tile.out { border-color: var(--err); }
|
|
408
416
|
.q-tile.dragging { opacity: .45; }
|
|
409
|
-
.q-tile .q-head { display: flex; justify-content: space-between; align-items:
|
|
410
|
-
.q-tile .nm { font-size: 1.0625rem; }
|
|
411
|
-
.q-
|
|
412
|
-
.q-
|
|
417
|
+
.q-tile .q-head { display: flex; justify-content: space-between; align-items: center; gap: .5rem; min-width: 0; }
|
|
418
|
+
.q-tile .nm { font-size: 1.0625rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
419
|
+
.q-tile .q-head .chip { flex: none; white-space: nowrap; font-size: .75rem; }
|
|
420
|
+
.q-about { display: flex; flex-direction: column; gap: .1rem; margin-top: -.3rem; font-size: .8125rem; color: var(--muted); }
|
|
421
|
+
.q-about .q-from { font-style: italic; }
|
|
422
|
+
.q-small { font-size: .8125rem; color: var(--muted); }
|
|
423
|
+
.q-body { display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
|
|
424
|
+
.q-group { border: 1px solid var(--line); border-radius: var(--radius); padding: .45rem .55rem .55rem; display: flex; flex-direction: column; gap: .45rem; }
|
|
413
425
|
.q-group.bare { border: 0; padding: 0; }
|
|
414
426
|
.q-group h4 { margin: 0; font-size: .8125rem; font-weight: 500; color: var(--muted); }
|
|
415
|
-
.q-
|
|
427
|
+
.q-meter { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
|
|
428
|
+
.q-line { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; font-size: .875rem; min-width: 0; }
|
|
416
429
|
.q-line b { font-weight: 600; }
|
|
417
|
-
.q-line
|
|
430
|
+
.q-line > :first-child { min-width: 0; }
|
|
431
|
+
.q-line > a.model { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
432
|
+
.q-line .q-val { flex: none; white-space: nowrap; }.segs.none span { background: var(--step-1); opacity: .6; }
|
|
433
|
+
/* Calls: green for what answered, red for what failed, out of every call. */
|
|
434
|
+
.segs.calls span { background: var(--err); }
|
|
435
|
+
.segs.calls i { background: var(--ok); }
|
|
436
|
+
.q-calls { white-space: nowrap; }
|
|
437
|
+
.q-calls .segs { display: inline-flex; width: 5.5rem; margin-right: .5rem; vertical-align: middle; }
|
|
418
438
|
.segs { display: flex; gap: 2px; }
|
|
419
439
|
.segs span { flex: 1; height: .5rem; border-radius: 2px; background: var(--step-1); overflow: hidden; }
|
|
420
440
|
.segs i { display: block; height: 100%; background: var(--ok); }
|
|
@@ -424,12 +444,13 @@ pre.log.answer { max-height: 40vh; }
|
|
|
424
444
|
.warn-t { color: var(--warn); }
|
|
425
445
|
.q-msg { font-size: .8125rem; }
|
|
426
446
|
.q-msg:empty { display: none; }
|
|
427
|
-
.q-tile .q-foot { display: flex;
|
|
447
|
+
.q-tile .q-foot { display: flex; gap: .4rem; justify-content: space-between; margin-top: auto;
|
|
428
448
|
padding-top: .5rem; border-top: 1px solid var(--line); }
|
|
429
449
|
.q-tile .q-foot .chip { margin-left: .2rem; font-size: .75rem; }
|
|
430
450
|
.q-legend { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .875rem; margin: 0 0 .4rem; }
|
|
431
451
|
.q-legend i { display: inline-block; width: .8rem; height: .7rem; border-radius: 2px; margin-right: .35rem; vertical-align: -1px; }
|
|
432
|
-
#q-models {
|
|
452
|
+
#q-models { width: min(48rem, 94vw); max-width: none; }
|
|
453
|
+
#q-models td:first-child { white-space: nowrap; }
|
|
433
454
|
|
|
434
455
|
/* Benchmarks charts (Observable Plot) */
|
|
435
456
|
.chart { margin: .5rem 0 .75rem; overflow-x: auto; }
|
package/ui/bench.js
CHANGED
|
@@ -411,6 +411,14 @@
|
|
|
411
411
|
var act = b.dataset.act, aliases = (b.dataset.aliases || b.dataset.alias || '').split(' ').filter(Boolean);
|
|
412
412
|
var status = b.closest('.problem').querySelector('.why');
|
|
413
413
|
if (act === 'open') { openModel(aliases[0]); return; }
|
|
414
|
+
if (act === 'dismiss') { // a notice: gone once seen
|
|
415
|
+
b.disabled = true;
|
|
416
|
+
post('/api/alerts/dismiss', {key: b.dataset.key}).then(function (res) {
|
|
417
|
+
if (!res.ok) { status.textContent = res.errors.join('; '); status.classList.add('err'); b.disabled = false; return; }
|
|
418
|
+
location.reload();
|
|
419
|
+
}).catch(function (err) { status.textContent = String(err); b.disabled = false; });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
414
422
|
if (act === 'retry') { // no confirm: one call, and only an answer changes anything
|
|
415
423
|
b.disabled = true; b.textContent = 'Asking…';
|
|
416
424
|
post('/api/retry', {alias: aliases[0]}).then(function (res) {
|
|
@@ -1330,7 +1338,7 @@
|
|
|
1330
1338
|
head: $('c-form-head'), maxCalls: $('g-max-calls'), maxExp: $('g-max-explorers'),
|
|
1331
1339
|
gErrors: $('g-errors'), gDone: $('g-done'), form: $('c-form'), status: $('c-status'),
|
|
1332
1340
|
del: $('c-delete'), reply: $('c-reply'), merger: $('c-merger'), find: $('c-member-filter'),
|
|
1333
|
-
seats: $('c-seats')};
|
|
1341
|
+
seats: $('c-seats'), prov: $('c-member-provider')};
|
|
1334
1342
|
var C = null, open = null; // the collection whose details are showing
|
|
1335
1343
|
function explorersFor(n) {
|
|
1336
1344
|
return Math.max(0, Math.min(C.settings.max_explorers, C.settings.max_calls - n));
|
|
@@ -1370,12 +1378,20 @@
|
|
|
1370
1378
|
return {name: row ? row.dataset.name : by[f][0], on: by[f].map(function (a) { return fact(a).provider || a; })};
|
|
1371
1379
|
});
|
|
1372
1380
|
}
|
|
1373
|
-
// The panel as it will sit, as the Collections list draws it
|
|
1381
|
+
// The panel as it will sit, as the Collections list draws it; a member's ×
|
|
1382
|
+
// unticks it, as unticking its chip does.
|
|
1374
1383
|
function drawSeats(ex) {
|
|
1375
1384
|
c.seats.textContent = '';
|
|
1376
1385
|
picked().forEach(function (a) {
|
|
1377
1386
|
var seat = el('span', {class: 'seat'}, a);
|
|
1378
1387
|
if (fact(a).provider) seat.appendChild(el('span', {class: 'k after'}, fact(a).provider));
|
|
1388
|
+
var x = el('button', {type: 'button', class: 'seat-x', 'aria-label': 'Remove ' + a + ' from the collection',
|
|
1389
|
+
title: 'Remove from the collection'}, '×');
|
|
1390
|
+
x.addEventListener('click', function () {
|
|
1391
|
+
var box = c.members.querySelector('input[value="' + CSS.escape(a) + '"]');
|
|
1392
|
+
if (box) { box.checked = false; applyTags(); }
|
|
1393
|
+
});
|
|
1394
|
+
seat.appendChild(x);
|
|
1379
1395
|
c.seats.appendChild(seat);
|
|
1380
1396
|
});
|
|
1381
1397
|
for (var i = 0; i < ex; i++) {
|
|
@@ -1425,18 +1441,20 @@
|
|
|
1425
1441
|
filterMembers();
|
|
1426
1442
|
}
|
|
1427
1443
|
// The member search: the filter boxes' match (any text, case-insensitive),
|
|
1428
|
-
// over the model's name and each chip's alias, served name and provider
|
|
1429
|
-
// It only hides chips, and never a ticked
|
|
1430
|
-
// in view and every count still includes
|
|
1444
|
+
// over the model's name and each chip's alias, served name and provider,
|
|
1445
|
+
// and the provider select beside it. It only hides chips, and never a ticked
|
|
1446
|
+
// one, so what is picked is always in view and every count still includes
|
|
1447
|
+
// it; a row goes when all its chips do.
|
|
1431
1448
|
function filterMembers() {
|
|
1432
|
-
var q = c.find.value.trim().toLowerCase();
|
|
1449
|
+
var q = c.find.value.trim().toLowerCase(), prov = c.prov.value;
|
|
1433
1450
|
c.members.querySelectorAll('.m-row').forEach(function (row) {
|
|
1434
1451
|
var named = !q || row.dataset.name.toLowerCase().indexOf(q) !== -1, any = false;
|
|
1435
1452
|
row.querySelectorAll('label.check').forEach(function (lab) {
|
|
1436
1453
|
var box = lab.querySelector('input');
|
|
1437
1454
|
var text = (box.value + ' ' + (lab.dataset.served || '') + ' ' + (fact(box.value).connection || '') + ' ' +
|
|
1438
1455
|
lab.textContent).toLowerCase();
|
|
1439
|
-
lab.hidden = !box.checked && (!!lab.dataset.paid || !named && text.indexOf(q) === -1
|
|
1456
|
+
lab.hidden = !box.checked && (!!lab.dataset.paid || !named && text.indexOf(q) === -1
|
|
1457
|
+
|| !!prov && (fact(box.value).provider || '') !== prov);
|
|
1440
1458
|
any = any || !lab.hidden;
|
|
1441
1459
|
});
|
|
1442
1460
|
row.hidden = !any;
|
|
@@ -1444,6 +1462,7 @@
|
|
|
1444
1462
|
recount();
|
|
1445
1463
|
}
|
|
1446
1464
|
c.find.addEventListener('input', filterMembers);
|
|
1465
|
+
c.prov.addEventListener('change', filterMembers);
|
|
1447
1466
|
// Escape in a box with text clears it; only an empty box lets Escape close the popup.
|
|
1448
1467
|
c.find.addEventListener('keydown', function (e) {
|
|
1449
1468
|
if (e.key === 'Escape' && c.find.value) { e.preventDefault(); e.stopPropagation(); c.find.value = ''; filterMembers(); }
|
|
@@ -1487,6 +1506,9 @@
|
|
|
1487
1506
|
c.desc.value = row ? row.description : ''; c.tags.value = row ? row.tags.join(', ') : '';
|
|
1488
1507
|
drawTagPicks();
|
|
1489
1508
|
c.members.textContent = ''; c.find.value = '';
|
|
1509
|
+
c.prov.length = 1; // "Every provider", then each one a model is on
|
|
1510
|
+
Array.from(new Set(C.models.map(function (a) { return fact(a).provider; }).filter(Boolean))).sort()
|
|
1511
|
+
.forEach(function (p) { c.prov.appendChild(el('option', {value: p}, p)); });
|
|
1490
1512
|
// One row per model, named by its shortest alias, with what the first
|
|
1491
1513
|
// route says it holds and can do; each provider it is on is a chip that
|
|
1492
1514
|
// ticks that alias. A chip whose window differs from the row's says its own.
|
|
@@ -1698,91 +1720,135 @@
|
|
|
1698
1720
|
var s = (Date.now() - new Date(iso)) / 1000;
|
|
1699
1721
|
return s < 90 ? 'just now' : s < 5400 ? Math.round(s / 60) + ' min ago' : s < 129600 ? Math.round(s / 3600) + ' h ago' : Math.round(s / 86400) + ' days ago';
|
|
1700
1722
|
}
|
|
1701
|
-
function qTone(
|
|
1702
|
-
|
|
1703
|
-
|
|
1723
|
+
function qTone(pct) { return pct == null ? 'none' : pct >= 70 ? 'ok' : pct >= 25 ? 'warn' : 'err'; }
|
|
1724
|
+
// `pct` null draws the ten segments empty: nothing to count yet. Calls
|
|
1725
|
+
// ("answered") fill green over red, the failed share showing as red.
|
|
1726
|
+
function qSegs(pct, what) {
|
|
1727
|
+
var tone = pct != null && what === 'answered' ? 'calls' : qTone(pct);
|
|
1728
|
+
var box = el('div', {class: 'segs ' + tone, role: 'img',
|
|
1729
|
+
'aria-label': pct == null ? 'nothing yet' : pct + '% ' + (what || 'left')});
|
|
1704
1730
|
for (var i = 0; i < 10; i++) {
|
|
1705
1731
|
var s = el('span'), f = el('i');
|
|
1706
|
-
f.style.width = Math.round(Math.min(Math.max(
|
|
1732
|
+
f.style.width = Math.round(Math.min(Math.max((pct || 0) / 10 - i, 0), 1) * 100) + '%';
|
|
1707
1733
|
s.appendChild(f); box.appendChild(s);
|
|
1708
1734
|
}
|
|
1709
1735
|
return box;
|
|
1710
1736
|
}
|
|
1711
|
-
function qLine(label, right) {
|
|
1712
|
-
var row = el('div', {class: 'q-line'});
|
|
1713
|
-
row.appendChild(el('span', {}, label));
|
|
1714
|
-
var r = el('span'); r.innerHTML = right; row.appendChild(r);
|
|
1715
|
-
return row;
|
|
1716
|
-
}
|
|
1717
1737
|
function qNum(n) { return Number(n).toLocaleString(); }
|
|
1738
|
+
function qAlias(a) { // as serve.named draws a model: its popup on a click
|
|
1739
|
+
var link = el('a', {href: '#', class: 'model', 'data-alias': a, draggable: 'false', title: a});
|
|
1740
|
+
link.appendChild(el('span', {class: 'alias mono'}, a));
|
|
1741
|
+
return link;
|
|
1742
|
+
}
|
|
1743
|
+
// 189, 19,997, 494k, 12.5M: whole below 100,000, then short.
|
|
1744
|
+
function qShort(n) {
|
|
1745
|
+
n = Number(n);
|
|
1746
|
+
if (n < 1e5) return Math.round(n * 100) / 100 >= 1000 ? Math.round(n).toLocaleString() : String(Math.round(n * 100) / 100);
|
|
1747
|
+
if (n < 1e6) return Math.round(n / 1e3) + 'k';
|
|
1748
|
+
return String(n >= 1e8 ? Math.round(n / 1e6) : Math.round(n / 1e5) / 10).replace(/\.0$/, '') + 'M';
|
|
1749
|
+
}
|
|
1750
|
+
// Every figure on a tile is one meter: what is left of what, in the
|
|
1751
|
+
// provider's own unit, on the left (it may wrap); when it resets on the
|
|
1752
|
+
// right (it never does); the segments under it.
|
|
1753
|
+
function qMeter(label, value, pct, what, title) {
|
|
1754
|
+
var box = el('div', {class: 'q-meter'}), row = el('div', {class: 'q-line'});
|
|
1755
|
+
var l = typeof label === 'string' ? el('span', {class: 'q-label'}) : label;
|
|
1756
|
+
if (typeof label === 'string') l.innerHTML = label;
|
|
1757
|
+
row.appendChild(l);
|
|
1758
|
+
var v = el('span', {class: 'q-val'}); v.innerHTML = value; row.appendChild(v);
|
|
1759
|
+
if (title) box.title = title;
|
|
1760
|
+
box.appendChild(row); box.appendChild(qSegs(pct, what));
|
|
1761
|
+
return box;
|
|
1762
|
+
}
|
|
1763
|
+
function qReset(iso) {
|
|
1764
|
+
return iso ? '<span title="Resets ' + new Date(iso).toLocaleString() + '">' + qWhen(iso) + '</span>' : '';
|
|
1765
|
+
}
|
|
1718
1766
|
function qWindow(w) {
|
|
1719
|
-
var
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1767
|
+
var raw = w.of != null && w.remaining != null;
|
|
1768
|
+
// The percentage leads every meter, raw or not, so they line up.
|
|
1769
|
+
return qMeter('<b>' + w.left + '%</b> left · ' + (raw ? '<b>' + qShort(w.remaining) + '</b> of '
|
|
1770
|
+
+ qShort(w.of) + ' ' + w.label : w.label),
|
|
1771
|
+
qReset(w.reset_at), w.left, 'left',
|
|
1772
|
+
(raw ? qNum(w.remaining) + ' of ' + qNum(w.of) + ' left, ' : '') + w.left + '%');
|
|
1773
|
+
}
|
|
1774
|
+
// How much of a last day answered: a model's (its link), or `label`'s.
|
|
1775
|
+
function qCalls(m, label) {
|
|
1776
|
+
var n = m.ok + m.failed, pct = n ? Math.round(100 * m.ok / n) : null;
|
|
1777
|
+
var value = qResting(m) ? '<span class="warn-t">resting · ' + qWhen(m.until) + '</span>'
|
|
1778
|
+
: n ? '<b>' + m.ok + '</b> of ' + n + ' answered' : '<span class="muted">no calls</span>';
|
|
1779
|
+
return qMeter(label || qAlias(m.alias), value, pct, 'answered',
|
|
1780
|
+
n ? m.ok + ' answered, ' + m.failed + ' failed in the last 24 hours' : 'not called in the last 24 hours');
|
|
1725
1781
|
}
|
|
1726
1782
|
function qResting(m) { return m.until && new Date(m.until) > Date.now(); }
|
|
1727
1783
|
function qTile(t) {
|
|
1728
1784
|
var node = el('article', {class: 'q-tile' + (t.out ? ' out' : ''), draggable: 'true', tabindex: '0',
|
|
1729
1785
|
'data-id': t.id, 'aria-label': t.name + ': drag, or use the arrow keys, to move it'});
|
|
1730
1786
|
var head = el('div', {class: 'q-head'});
|
|
1731
|
-
head.appendChild(el('b', {class: 'nm'}, t.name));
|
|
1732
|
-
var f = t.fresh, how =
|
|
1733
|
-
head.appendChild(el('span', {class: 'chip ' + (f.how === 'live' ? 'ok' : 'via')
|
|
1787
|
+
head.appendChild(el('b', {class: 'nm', title: t.name}, t.name));
|
|
1788
|
+
var f = t.fresh, how = {live: 'read ', seen: 'seen ', answered: 'answered '}[f.how] || '';
|
|
1789
|
+
head.appendChild(el('span', {class: 'chip ' + (f.how === 'live' ? 'ok' : 'via'),
|
|
1790
|
+
title: {live: 'Last asked of the provider', seen: 'Last stated in a reply\'s headers',
|
|
1791
|
+
answered: 'Last answer from one of its models'}[f.how] || 'Nothing known yet'},
|
|
1792
|
+
f.at ? how + qAgo(f.at) : 'nothing yet'));
|
|
1734
1793
|
node.appendChild(head);
|
|
1735
|
-
|
|
1794
|
+
// Every word about the figures in one place, under the name: where they
|
|
1795
|
+
// come from, its plan, your per-minute caps, what a live read said. The
|
|
1796
|
+
// connection's own name and address are in Models.
|
|
1797
|
+
var about = el('div', {class: 'q-about'});
|
|
1798
|
+
var src = t.source || [];
|
|
1799
|
+
about.appendChild(el('div', {class: 'q-from'}, (src.length && src[0] !== 'nothing stated'
|
|
1800
|
+
? 'From ' + src.join('; ') : 'No limit stated') + (t.plan ? ' · ' + t.plan + ' plan' : '')));
|
|
1801
|
+
if (t.conn_limit) about.appendChild(el('div', {}, 'Your cap: ' + t.conn_limit + '.'));
|
|
1802
|
+
(t.notes || []).forEach(function (n) { about.appendChild(el('div', {}, n)); });
|
|
1803
|
+
node.appendChild(about);
|
|
1804
|
+
var body = el('div', {class: 'q-body'});
|
|
1805
|
+
// Day and month caps, counted by the bench: yours, else what the provider
|
|
1806
|
+
// documents. The From line names the source; the title says the rest.
|
|
1807
|
+
(t.caps || []).forEach(function (g) {
|
|
1808
|
+
var box = el('section', {class: 'q-group bare', title: g.title || ''});
|
|
1809
|
+
g.windows.forEach(function (w) { box.appendChild(qWindow(w)); });
|
|
1810
|
+
body.appendChild(box);
|
|
1811
|
+
});
|
|
1736
1812
|
if (t.kind === 'groups') {
|
|
1737
1813
|
t.groups.forEach(function (g) {
|
|
1738
1814
|
var box = el('section', {class: g.label ? 'q-group' : 'q-group bare'});
|
|
1739
1815
|
if (g.label) box.appendChild(el('h4', {}, g.label));
|
|
1740
1816
|
g.windows.forEach(function (w) { box.appendChild(qWindow(w)); });
|
|
1741
|
-
|
|
1817
|
+
body.appendChild(box);
|
|
1742
1818
|
});
|
|
1743
1819
|
} else if (t.kind === 'pool') {
|
|
1744
1820
|
t.pool.forEach(function (p) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
} else {
|
|
1751
|
-
box.appendChild(qLine(p.label, '<b>' + qNum(p.used) + '</b> sent · limit not known yet'));
|
|
1752
|
-
}
|
|
1753
|
-
node.appendChild(box);
|
|
1754
|
-
});
|
|
1755
|
-
} else {
|
|
1756
|
-
var shown = t.models.slice(0, 6);
|
|
1757
|
-
shown.forEach(function (m) {
|
|
1758
|
-
var right = qResting(m) ? '<span class="warn-t">resting · ' + qWhen(m.until) + '</span>'
|
|
1759
|
-
: m.ok + m.failed ? m.ok + ' ok · ' + m.failed + ' failed' : 'no calls today';
|
|
1760
|
-
var row = qLine('', right); row.firstChild.className = 'mono'; row.firstChild.textContent = m.alias;
|
|
1761
|
-
node.appendChild(row);
|
|
1821
|
+
body.appendChild(p.of
|
|
1822
|
+
? qMeter('<b>' + p.left + '%</b> left · <b>' + qShort(Math.max(0, p.of - p.used)) + '</b> of '
|
|
1823
|
+
+ qShort(p.of) + ' ' + p.label,
|
|
1824
|
+
qReset(p.reset_at), p.left, 'left')
|
|
1825
|
+
: qMeter('<b>' + qShort(p.used) + '</b> ' + p.label + ' · limit not known', qReset(p.reset_at), null));
|
|
1762
1826
|
});
|
|
1763
|
-
|
|
1764
|
-
|
|
1827
|
+
} else if (!(t.caps || []).length) {
|
|
1828
|
+
// Nothing stated to count against: how the provider's calls went, all
|
|
1829
|
+
// its models together (each one's are in Models).
|
|
1830
|
+
var all = t.models.reduce(function (s, m) { return {ok: s.ok + m.ok, failed: s.failed + m.failed}; }, {ok: 0, failed: 0});
|
|
1831
|
+
body.appendChild(qCalls({alias: '', ok: all.ok, failed: all.failed, until: null}, 'Calls, last 24 hours'));
|
|
1765
1832
|
}
|
|
1766
1833
|
if (t.kind !== 'models') {
|
|
1767
1834
|
t.models.filter(qResting).forEach(function (m) {
|
|
1768
|
-
|
|
1835
|
+
var r = el('div', {class: 'q-rest'});
|
|
1836
|
+
r.appendChild(qAlias(m.alias)); r.appendChild(document.createTextNode(' resting · back ' + qWhen(m.until)));
|
|
1837
|
+
body.appendChild(r);
|
|
1769
1838
|
});
|
|
1770
1839
|
}
|
|
1771
|
-
|
|
1772
|
-
(t.notes || []).forEach(function (n) { node.appendChild(el('div', {class: 'muted q-small'}, n)); });
|
|
1840
|
+
node.appendChild(body);
|
|
1773
1841
|
var msg = el('div', {class: 'q-msg', 'aria-live': 'polite'});
|
|
1774
1842
|
node.appendChild(msg);
|
|
1843
|
+
// One Refresh: the server picks a free read, one short call or the records.
|
|
1775
1844
|
var foot = el('div', {class: 'q-foot'});
|
|
1776
1845
|
var rb = el('button', {class: 'btn small', type: 'button', 'aria-label': 'Refresh ' + t.name,
|
|
1777
|
-
title:
|
|
1846
|
+
title: {live: 'Asks the provider what is left (free)',
|
|
1847
|
+
check: 'Sends one 16-token request; the reply\'s headers say what is left'}[t.refresh]
|
|
1848
|
+
|| 'Reads the bench\'s own records again (this provider states no limits to ask about)'});
|
|
1778
1849
|
rb.innerHTML = '↻ Refresh';
|
|
1779
|
-
rb.addEventListener('click', function () { qAct(
|
|
1850
|
+
rb.addEventListener('click', function () { qAct(t.id, node); });
|
|
1780
1851
|
foot.appendChild(rb);
|
|
1781
|
-
if (t.check) {
|
|
1782
|
-
var cb = el('button', {class: 'btn small', type: 'button', title: 'One short call to one of its models; its reply says what is left'}, 'Check now · 1 request');
|
|
1783
|
-
cb.addEventListener('click', function () { qAct('check', t.id, node); });
|
|
1784
|
-
foot.appendChild(cb);
|
|
1785
|
-
}
|
|
1786
1852
|
var mb = el('button', {class: 'btn small', type: 'button'}, 'Models ');
|
|
1787
1853
|
mb.appendChild(el('span', {class: 'chip ' + (t.live === t.bench ? 'ok' : t.live ? 'warn' : 'err')}, t.live + '/' + t.bench));
|
|
1788
1854
|
mb.title = t.live + ' of the ' + t.bench + ' bench models on it are free to call';
|
|
@@ -1791,10 +1857,10 @@
|
|
|
1791
1857
|
node.appendChild(foot);
|
|
1792
1858
|
return node;
|
|
1793
1859
|
}
|
|
1794
|
-
function qAct(
|
|
1860
|
+
function qAct(id, node) {
|
|
1795
1861
|
var msg = node.querySelector('.q-msg');
|
|
1796
|
-
msg.className = 'q-msg muted'; msg.textContent =
|
|
1797
|
-
return post('/api/quota/'
|
|
1862
|
+
msg.className = 'q-msg muted'; msg.textContent = 'Refreshing…';
|
|
1863
|
+
return post('/api/quota/refresh', {tag: id}).then(function (r) {
|
|
1798
1864
|
if (!r.ok) { msg.className = 'q-msg errors'; msg.textContent = r.errors.join('; '); return; }
|
|
1799
1865
|
var i = QD.tiles.findIndex(function (t) { return t.id === id; });
|
|
1800
1866
|
if (r.tile && i >= 0) {
|
|
@@ -1809,17 +1875,37 @@
|
|
|
1809
1875
|
function qModels(t) {
|
|
1810
1876
|
$('q-models-h').textContent = t.name + ': ' + t.bench + ' on the bench';
|
|
1811
1877
|
$('q-models-sub').textContent = t.live + ' free to call now · the last 24 hours of calls';
|
|
1878
|
+
// How it is reached, asked when the popup opens (it reads Open WebUI's connections).
|
|
1879
|
+
var reach = $('q-models-about');
|
|
1880
|
+
function reachLine(after) {
|
|
1881
|
+
reach.textContent = '';
|
|
1882
|
+
var line = el('div'); line.appendChild(el('span', {class: 'mono'}, t.tag));
|
|
1883
|
+
if (after) line.appendChild(document.createTextNode(' · ' + after));
|
|
1884
|
+
reach.appendChild(line);
|
|
1885
|
+
}
|
|
1886
|
+
reachLine('finding how it is reached…');
|
|
1887
|
+
post('/api/quota/about', {tag: t.id}).then(function (r) {
|
|
1888
|
+
if (!r.ok) { reachLine(r.errors.join('; ')); return; }
|
|
1889
|
+
reachLine(r.routes.length ? '' : 'no endpoint or Open WebUI connection by this name');
|
|
1890
|
+
r.routes.forEach(function (x) {
|
|
1891
|
+
var d = el('div', {}, 'Through ' + x.via + ': ');
|
|
1892
|
+
d.appendChild(el('code', {}, x.url)); reach.appendChild(d);
|
|
1893
|
+
});
|
|
1894
|
+
}).catch(function (e) { reachLine(String(e)); });
|
|
1812
1895
|
var list = $('q-models-list'); list.textContent = '';
|
|
1813
1896
|
var tb = el('table'), hd = el('tr');
|
|
1814
1897
|
['Model', 'Now', 'Last 24 h', 'Last answered', 'Known limit'].forEach(function (h) { hd.appendChild(el('th', {}, h)); });
|
|
1815
1898
|
tb.appendChild(el('thead')).appendChild(hd);
|
|
1816
1899
|
var body = tb.appendChild(el('tbody'));
|
|
1817
1900
|
t.models.forEach(function (m) {
|
|
1818
|
-
var tr = el('tr');
|
|
1819
|
-
|
|
1901
|
+
var tr = el('tr'), name = el('td');
|
|
1902
|
+
name.appendChild(qAlias(m.alias)); tr.appendChild(name);
|
|
1820
1903
|
var now = el('td'); now.appendChild(el('span', {class: 'chip ' + (qResting(m) ? 'warn' : 'ok')}, qResting(m) ? 'resting · ' + qWhen(m.until) : 'ready'));
|
|
1821
1904
|
tr.appendChild(now);
|
|
1822
|
-
|
|
1905
|
+
var n = m.ok + m.failed, calls = el('td', {class: 'q-calls', title: m.failed + ' failed'});
|
|
1906
|
+
calls.appendChild(qSegs(n ? Math.round(100 * m.ok / n) : null, 'answered'));
|
|
1907
|
+
calls.appendChild(el('span', {}, n ? m.ok + ' of ' + n + ' answered' : 'no calls'));
|
|
1908
|
+
tr.appendChild(calls);
|
|
1823
1909
|
tr.appendChild(el('td', {}, m.last_ok ? qAgo(m.last_ok) : '—'));
|
|
1824
1910
|
tr.appendChild(el('td', {}, m.limit || '—'));
|
|
1825
1911
|
body.appendChild(tr);
|
|
@@ -1888,7 +1974,7 @@
|
|
|
1888
1974
|
live.reduce(function (p, t) {
|
|
1889
1975
|
return p.then(function () {
|
|
1890
1976
|
var node = qBox.querySelector('.q-tile[data-id="' + CSS.escape(t.id) + '"]');
|
|
1891
|
-
return node ? qAct(
|
|
1977
|
+
return node ? qAct(t.id, node) : null;
|
|
1892
1978
|
});
|
|
1893
1979
|
}, Promise.resolve()).then(function () { $('q-done').textContent = 'Refreshed ' + live.length + '.'; });
|
|
1894
1980
|
});
|
|
@@ -1931,6 +2017,7 @@
|
|
|
1931
2017
|
QD.tiles.forEach(function (t) {
|
|
1932
2018
|
var wins = [];
|
|
1933
2019
|
t.groups.forEach(function (g) { g.windows.forEach(function (w) { wins.push({label: (g.label ? g.label + ' · ' : '') + w.label, at: w.reset_at}); }); });
|
|
2020
|
+
(t.caps || []).forEach(function (g) { g.windows.forEach(function (w) { wins.push({label: w.label + ' (' + g.label + ')', at: w.reset_at, daily: / day$/.test(w.label)}); }); });
|
|
1934
2021
|
t.pool.forEach(function (p) { if (p.reset_at) wins.push({label: p.label, at: p.reset_at, daily: true}); });
|
|
1935
2022
|
wins.forEach(function (w) {
|
|
1936
2023
|
var at = +new Date(w.at), daily = w.daily || / day$/.test(w.label);
|