dsh-agy 0.3.2 → 0.4.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/README.md +21 -10
- package/lib/{accounts-CKWybAfc.mjs → accounts-BjdDqUq1.mjs} +425 -76
- package/lib/accounts-BjdDqUq1.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +4 -2
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +406 -36
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-B5dyfnMH.mjs → plugin-common-B7OSfnyl.mjs} +354 -24
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/{translate-BQu8HLvB.mjs → translate-YhGDUuVa.mjs} +157 -23
- package/lib/translate-YhGDUuVa.mjs.map +1 -0
- package/lib/web/plugin.d.mts +28 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +63 -25
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +4 -3
- package/lib/accounts-CKWybAfc.mjs.map +0 -1
- package/lib/plugin-common-B5dyfnMH.mjs.map +0 -1
- package/lib/translate-BQu8HLvB.mjs.map +0 -1
package/lib/client.js
CHANGED
|
@@ -103,6 +103,17 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
rounded rect inset 2px from the card edge and transparent at rest. A
|
|
104
104
|
full-bleed rectangle reads as a slab and fights the card's own radius. */
|
|
105
105
|
.agy-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
106
|
+
/* The live line: one status strip above the rows, present ONLY while upstream
|
|
107
|
+
requests are in flight — an idle pool renders no strip, so quiet stays quiet.
|
|
108
|
+
The pulsing dot is the host StateDot primitive ('ongoing'), so the animation
|
|
109
|
+
is the platform's; this rule is layout and tone only. Sits OUTSIDE .agy-rows,
|
|
110
|
+
so the master list's scroll cap does not scroll the status away. */
|
|
111
|
+
.agy-live {
|
|
112
|
+
display: flex; align-items: center; gap: 7px;
|
|
113
|
+
margin: 2px 2px 6px; padding: 7px 8px; border-radius: 10px;
|
|
114
|
+
font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b);
|
|
115
|
+
background: var(--dsw-alias-bg-layer-2, #f4f5f7);
|
|
116
|
+
}
|
|
106
117
|
/* Master/detail: the account list beside the selected account's detail, so a
|
|
107
118
|
* row and the panel it opens stay in view together.
|
|
108
119
|
*
|
|
@@ -314,6 +325,12 @@ window.__ModuleLoader__.load({
|
|
|
314
325
|
color: var(--dsw-alias-label-primary, #1f2329); }
|
|
315
326
|
.agy-limit-reset { text-align: right; font: var(--dsw-font-xxxs-11);
|
|
316
327
|
color: var(--dsw-alias-label-tertiary, #8f959e); }
|
|
328
|
+
/* The burn projection: indented to align with the bar (58px label + 10px gap),
|
|
329
|
+
warn-tinted because "this window runs dry before it resets" is the one
|
|
330
|
+
projection that asks the reader to act. */
|
|
331
|
+
.agy-limit-burn { padding: 0 0 4px 68px;
|
|
332
|
+
font: var(--dsw-font-xxxs-11);
|
|
333
|
+
color: var(--dsw-alias-state-warn-primary, #f59e0b); }
|
|
317
334
|
|
|
318
335
|
/* ── Dense breakdown tables (Usage tab only) ─────────────────────────────── */
|
|
319
336
|
.agy-table-wrap { padding: 6px 0 2px; }
|
|
@@ -360,6 +377,19 @@ window.__ModuleLoader__.load({
|
|
|
360
377
|
/* Danger has no primitive variant; keep the ghost skin and tint the label. */
|
|
361
378
|
.agy-btn-danger { color: var(--dsw-alias-state-error-primary, #ec1313) !important; }
|
|
362
379
|
|
|
380
|
+
/* ── Recent activity ring ──────────────────────────────────────────────────
|
|
381
|
+
The "what just happened" list. Only the result cell carries color — ok
|
|
382
|
+
inherits the table's neutral, and a wall of tinted rows would read as an
|
|
383
|
+
alarm rather than a log. */
|
|
384
|
+
.agy-recent-state { font: var(--dsw-font-xxs-12); }
|
|
385
|
+
.agy-recent-state[data-kind="fail"] { color: var(--dsw-alias-state-error-primary, #ec1313); }
|
|
386
|
+
.agy-recent-state[data-kind="limited"] { color: var(--dsw-alias-state-warn-primary, #f59e0b); }
|
|
387
|
+
.agy-recent-state[data-kind="rotation"] { color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
|
|
388
|
+
/* The recent list is a standalone disclosure on the tab root, not one block
|
|
389
|
+
inside a card body — the separator border-top the disclosure idiom uses
|
|
390
|
+
between sibling blocks would draw a stray line across nothing here. */
|
|
391
|
+
.agy-recent.agy-disclosure { border-top: 0; }
|
|
392
|
+
|
|
363
393
|
.agy-toolbar { display: flex; align-items: center; gap: 8px; }
|
|
364
394
|
.agy-textarea { width: 100%; min-height: 88px; resize: vertical; outline: none;
|
|
365
395
|
padding: 9px 10px; font: var(--dsw-font-xxs-12); font-family: var(--ds-font-family-code);
|
|
@@ -401,16 +431,35 @@ window.__ModuleLoader__.load({
|
|
|
401
431
|
tabModels: "模型",
|
|
402
432
|
tabUsage: "用量",
|
|
403
433
|
tabCredentials: "凭据",
|
|
404
|
-
stateActive: "
|
|
434
|
+
stateActive: "可用",
|
|
405
435
|
stateCooling: "冷却中",
|
|
406
436
|
stateVerificationRequired: "待验证",
|
|
407
437
|
stateDisabled: "已停用",
|
|
408
438
|
coolingUntil: "冷却至",
|
|
439
|
+
verificationRetry: "待验证 · {time} 重试",
|
|
409
440
|
currentAccount: "当前账号",
|
|
441
|
+
fieldDisabled: "停用",
|
|
442
|
+
disabledCredentials: "凭据失效",
|
|
443
|
+
disabledSince: "停用于 {ago}",
|
|
444
|
+
disabledHint: "凭据已被上游拒绝。运行该账号所在行的「验证」尝试恢复;若仍失败,请重新登录导入。",
|
|
410
445
|
noProject: "—",
|
|
411
446
|
valueUnknown: "—",
|
|
412
447
|
colAccount: "账号",
|
|
413
448
|
colRequests: "请求",
|
|
449
|
+
colTime: "时间",
|
|
450
|
+
colResult: "结果",
|
|
451
|
+
colDuration: "耗时",
|
|
452
|
+
recentTitle: "最近请求",
|
|
453
|
+
recentHelp: "本进程内存中的最近 200 条(含轮换事件),不落盘、不跨进程合并。",
|
|
454
|
+
recentEmpty: "暂无最近记录",
|
|
455
|
+
recentOk: "成功",
|
|
456
|
+
rowRequestsTotal: "累计请求 {n}",
|
|
457
|
+
lastActive: "{ago}活跃",
|
|
458
|
+
liveOne: "正在通过 {email} 生成 · {count} 个并发",
|
|
459
|
+
liveMany: "正在生成 · {count} 个并发 · {accounts} 个账号",
|
|
460
|
+
fieldThroughput: "吞吐",
|
|
461
|
+
throughputValue: "≈ {n} token/s",
|
|
462
|
+
throughputNote: "首 token 后 · 累计平均",
|
|
414
463
|
colActions: "操作",
|
|
415
464
|
emptyAccounts: "还没有账号。切到「凭据」标签导入,或点击上方「登录」。",
|
|
416
465
|
detailTitle: "已选账号",
|
|
@@ -420,6 +469,7 @@ window.__ModuleLoader__.load({
|
|
|
420
469
|
limitsRefreshOk: "已刷新 {measured} 个账号的限额",
|
|
421
470
|
limitsRefreshFresh: "限额仍是新鲜的,无需刷新",
|
|
422
471
|
limitsRefreshFailed: "{failed} 个账号的限额刷新失败",
|
|
472
|
+
limitBurnWarn: "按此速度{value}后耗尽",
|
|
423
473
|
quotaWindow5h: "5 小时",
|
|
424
474
|
quotaWindowWeekly: "每周",
|
|
425
475
|
fieldProject: "项目",
|
|
@@ -447,13 +497,14 @@ window.__ModuleLoader__.load({
|
|
|
447
497
|
quotaResetIn: "重置 {value}",
|
|
448
498
|
relNow: "即将",
|
|
449
499
|
relJustNow: "刚刚",
|
|
500
|
+
relSeconds: "{n} 秒",
|
|
450
501
|
relAgo: "{value}前",
|
|
451
502
|
relMinutes: "{n} 分钟",
|
|
452
503
|
relHours: "{n} 小时",
|
|
453
504
|
relDays: "{n} 天",
|
|
454
505
|
relMonths: "{n} 个月",
|
|
455
506
|
relYears: "{n} 年",
|
|
456
|
-
actionActivate: "
|
|
507
|
+
actionActivate: "设为当前",
|
|
457
508
|
actionVerify: "验证",
|
|
458
509
|
actionDelete: "删除",
|
|
459
510
|
actionTest: "测试调用",
|
|
@@ -494,6 +545,8 @@ window.__ModuleLoader__.load({
|
|
|
494
545
|
fieldRateLimitRotation: "限流 / 轮换",
|
|
495
546
|
byModel: "按模型",
|
|
496
547
|
byAccount: "按账号",
|
|
548
|
+
trendTitle: "近 7 天",
|
|
549
|
+
colDay: "日期",
|
|
497
550
|
colModel: "模型",
|
|
498
551
|
colTokenShare: "Token 占比",
|
|
499
552
|
colOutput: "输出",
|
|
@@ -561,16 +614,35 @@ window.__ModuleLoader__.load({
|
|
|
561
614
|
tabModels: "Models",
|
|
562
615
|
tabUsage: "Usage",
|
|
563
616
|
tabCredentials: "Credentials",
|
|
564
|
-
stateActive: "
|
|
617
|
+
stateActive: "Ready",
|
|
565
618
|
stateCooling: "Cooling down",
|
|
566
619
|
stateVerificationRequired: "Needs verification",
|
|
567
620
|
stateDisabled: "Disabled",
|
|
568
621
|
coolingUntil: "Cooling until",
|
|
569
|
-
|
|
622
|
+
verificationRetry: "Needs verification · retries {time}",
|
|
623
|
+
currentAccount: "Current",
|
|
624
|
+
fieldDisabled: "Disabled",
|
|
625
|
+
disabledCredentials: "Credentials rejected",
|
|
626
|
+
disabledSince: "disabled {ago}",
|
|
627
|
+
disabledHint: "The credentials were rejected by upstream. Run Verify on this account's row to try restoring it; if that fails, sign in again to re-import.",
|
|
570
628
|
noProject: "—",
|
|
571
629
|
valueUnknown: "—",
|
|
572
630
|
colAccount: "Account",
|
|
573
631
|
colRequests: "Requests",
|
|
632
|
+
colTime: "Time",
|
|
633
|
+
colResult: "Result",
|
|
634
|
+
colDuration: "Duration",
|
|
635
|
+
recentTitle: "Recent requests",
|
|
636
|
+
recentHelp: "Last 200 records in this process's memory (rotation events included); never persisted, never merged across processes.",
|
|
637
|
+
recentEmpty: "No recent activity",
|
|
638
|
+
recentOk: "ok",
|
|
639
|
+
rowRequestsTotal: "{n} requests in total",
|
|
640
|
+
lastActive: "active {ago}",
|
|
641
|
+
liveOne: "Generating via {email} · {count} in flight",
|
|
642
|
+
liveMany: "Generating · {count} in flight across {accounts} account(s)",
|
|
643
|
+
fieldThroughput: "Throughput",
|
|
644
|
+
throughputValue: "≈ {n} token/s",
|
|
645
|
+
throughputNote: "after first token · cumulative",
|
|
574
646
|
colActions: "Actions",
|
|
575
647
|
emptyAccounts: "No accounts yet. Import one from the Credentials tab, or use Sign in above.",
|
|
576
648
|
detailTitle: "Selected account",
|
|
@@ -580,6 +652,7 @@ window.__ModuleLoader__.load({
|
|
|
580
652
|
limitsRefreshOk: "Refreshed limits for {measured} account(s)",
|
|
581
653
|
limitsRefreshFresh: "Limits are already fresh — nothing to refresh",
|
|
582
654
|
limitsRefreshFailed: "Limit refresh failed for {failed} account(s)",
|
|
655
|
+
limitBurnWarn: "at this rate, empty in {value}",
|
|
583
656
|
quotaWindow5h: "5 hours",
|
|
584
657
|
quotaWindowWeekly: "Weekly",
|
|
585
658
|
fieldProject: "Project",
|
|
@@ -607,13 +680,14 @@ window.__ModuleLoader__.load({
|
|
|
607
680
|
quotaResetIn: "resets {value}",
|
|
608
681
|
relNow: "shortly",
|
|
609
682
|
relJustNow: "just now",
|
|
683
|
+
relSeconds: "{n}s",
|
|
610
684
|
relAgo: "{value} ago",
|
|
611
685
|
relMinutes: "{n} min",
|
|
612
686
|
relHours: "{n} h",
|
|
613
687
|
relDays: "{n} d",
|
|
614
688
|
relMonths: "{n} mo",
|
|
615
689
|
relYears: "{n} y",
|
|
616
|
-
actionActivate: "
|
|
690
|
+
actionActivate: "Set as current",
|
|
617
691
|
actionVerify: "Verify",
|
|
618
692
|
actionDelete: "Delete",
|
|
619
693
|
actionTest: "Test call",
|
|
@@ -654,6 +728,8 @@ window.__ModuleLoader__.load({
|
|
|
654
728
|
fieldRateLimitRotation: "Rate limits / rotations",
|
|
655
729
|
byModel: "By model",
|
|
656
730
|
byAccount: "By account",
|
|
731
|
+
trendTitle: "Last 7 days",
|
|
732
|
+
colDay: "Day",
|
|
657
733
|
colModel: "Model",
|
|
658
734
|
colTokenShare: "Token share",
|
|
659
735
|
colOutput: "Output",
|
|
@@ -782,6 +858,14 @@ window.__ModuleLoader__.load({
|
|
|
782
858
|
* it — see `actionError` in `AgySettings`.
|
|
783
859
|
*/
|
|
784
860
|
const ACTION_MESSAGE_TTL_MS = 3500;
|
|
861
|
+
/**
|
|
862
|
+
* How often the section re-reads the pool's in-flight snapshot.
|
|
863
|
+
*
|
|
864
|
+
* The call is a pure in-memory read on the host (no token refresh, no quota
|
|
865
|
+
* probe), so a tight-ish interval is free; 3s makes the live line feel live
|
|
866
|
+
* without a visible request cost.
|
|
867
|
+
*/
|
|
868
|
+
const POOL_POLL_INTERVAL_MS = 3e3;
|
|
785
869
|
function h(tag, props, ...children) {
|
|
786
870
|
return (0, react.createElement)(tag, props ?? null, ...children);
|
|
787
871
|
}
|
|
@@ -854,6 +938,28 @@ window.__ModuleLoader__.load({
|
|
|
854
938
|
return counters.input + counters.output + counters.cacheRead + counters.cacheWrite;
|
|
855
939
|
}
|
|
856
940
|
/**
|
|
941
|
+
* Cumulative average OUTPUT rate for one scope, in tokens per second.
|
|
942
|
+
*
|
|
943
|
+
* Pure and exported for a direct unit test. The denominator is the STREAMING
|
|
944
|
+
* window (average latency minus average time-to-first-token), NOT the wall
|
|
945
|
+
* clock: on this channel the pre-first-token wait (upstream queue, routing,
|
|
946
|
+
* prompt processing, silent thinking) is ~90% of the request — measured 7.5s
|
|
947
|
+
* of an 8.3s average — so dividing by wall time understated the decode rate
|
|
948
|
+
* about 8× (40 tok/s shown where the account really streamed ~300).
|
|
949
|
+
*
|
|
950
|
+
* Per-request AVERAGES, not raw sums: a failed request carries wall time but
|
|
951
|
+
* never a first token (`latencyN` > `ttftN`), so subtracting raw sums would
|
|
952
|
+
* hand its wait to the decode window. Both averages must exist — a scope with
|
|
953
|
+
* no timed request or no reported first token gets null, hiding the row rather
|
|
954
|
+
* than showing a fake rate.
|
|
955
|
+
*/
|
|
956
|
+
function throughputTokenPerSecond(totals) {
|
|
957
|
+
if (totals.latencyN === 0 || totals.ttftN === 0 || totals.output <= 0) return null;
|
|
958
|
+
const decodeMs = totals.latencyMs / totals.latencyN - totals.ttftMs / totals.ttftN;
|
|
959
|
+
if (decodeMs <= 0) return null;
|
|
960
|
+
return Math.round(totals.output / totals.latencyN / (decodeMs / 1e3));
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
857
963
|
* The whole prompt side: everything the model read, cached or not.
|
|
858
964
|
*
|
|
859
965
|
* The LEDGER keeps `input` as the uncached portion alone, because that is DSH's
|
|
@@ -918,28 +1024,58 @@ window.__ModuleLoader__.load({
|
|
|
918
1024
|
}
|
|
919
1025
|
}
|
|
920
1026
|
/**
|
|
1027
|
+
* Humanize a burn horizon (hours until a window runs dry at the sampled rate).
|
|
1028
|
+
*
|
|
1029
|
+
* Reuses the shared `rel*` magnitudes so the phrasing matches every other
|
|
1030
|
+
* duration on the panel; under an hour falls to minutes rather than rounding
|
|
1031
|
+
* to a false zero.
|
|
1032
|
+
*/
|
|
1033
|
+
function burnHorizon(hours, t) {
|
|
1034
|
+
if (hours < 1) return t("relMinutes", { n: Math.max(1, Math.round(hours * 60)) });
|
|
1035
|
+
if (hours < 48) return t("relHours", { n: Math.round(hours) });
|
|
1036
|
+
return t("relDays", { n: Math.round(hours / 24) });
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
921
1039
|
* A wall-clock moment for a state label (a cooldown end).
|
|
922
1040
|
*
|
|
923
1041
|
* Time-of-day alone is enough while the wall is today; past midnight it must
|
|
924
1042
|
* carry the date, or a 24h quota cooldown reads as though it ends in a few
|
|
925
1043
|
* minutes. (`untilText` is the relative form, used where "how long from now" is
|
|
926
1044
|
* the question rather than "when".)
|
|
1045
|
+
*
|
|
1046
|
+
* `lang` is the UI language ('zh' | 'en'), not the browser locale: the words on
|
|
1047
|
+
* this panel follow the host's language setting, so the dates must too — a zh
|
|
1048
|
+
* panel rendering `9/24/2026` (the browser's en-US ordering) was the symptom.
|
|
1049
|
+
* `undefined` degrades to the browser default, the pre-`lang` behaviour.
|
|
927
1050
|
*/
|
|
928
|
-
function clockTime(iso) {
|
|
1051
|
+
function clockTime(iso, lang) {
|
|
929
1052
|
if (iso === null) return "—";
|
|
930
1053
|
const date = new Date(iso);
|
|
931
1054
|
if (Number.isNaN(date.getTime())) return "—";
|
|
932
1055
|
const now = /* @__PURE__ */ new Date();
|
|
933
|
-
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(
|
|
1056
|
+
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(lang, {
|
|
934
1057
|
hour: "2-digit",
|
|
935
1058
|
minute: "2-digit"
|
|
936
|
-
}) : date.toLocaleString(
|
|
1059
|
+
}) : date.toLocaleString(lang, {
|
|
937
1060
|
month: "short",
|
|
938
1061
|
day: "numeric",
|
|
939
1062
|
hour: "2-digit",
|
|
940
1063
|
minute: "2-digit"
|
|
941
1064
|
});
|
|
942
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* A ledger day key ('YYYY-MM-DD', local by construction) as a short label.
|
|
1068
|
+
* Parsed as LOCAL date parts — a UTC parse would shift the label a day for
|
|
1069
|
+
* half the planet.
|
|
1070
|
+
*/
|
|
1071
|
+
function dayLabel(day, lang) {
|
|
1072
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
1073
|
+
if (y === void 0 || m === void 0 || d === void 0 || Number.isNaN(y)) return day;
|
|
1074
|
+
return new Date(y, m - 1, d).toLocaleDateString(lang, {
|
|
1075
|
+
month: "short",
|
|
1076
|
+
day: "numeric"
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
943
1079
|
const MINUTE_MS = 6e4;
|
|
944
1080
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
945
1081
|
const DAY_MS = 24 * HOUR_MS;
|
|
@@ -993,6 +1129,34 @@ window.__ModuleLoader__.load({
|
|
|
993
1129
|
return t("relAgo", { value: diff < HOUR_MS ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < DAY_MS ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 30 * DAY_MS ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 365 * DAY_MS ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
|
|
994
1130
|
}
|
|
995
1131
|
/**
|
|
1132
|
+
* Ago label with seconds resolution, for the recent list. `agoText` collapses
|
|
1133
|
+
* the whole first minute into "just now", which is too coarse when the reader
|
|
1134
|
+
* is watching live activity — here the seconds carry the information.
|
|
1135
|
+
*/
|
|
1136
|
+
function recentAgo(at, now, t) {
|
|
1137
|
+
const diff = now - at;
|
|
1138
|
+
if (diff < 1e4) return t("relJustNow");
|
|
1139
|
+
if (diff < MINUTE_MS) return t("relSeconds", { n: Math.floor(diff / 1e3) });
|
|
1140
|
+
return agoText(new Date(at).toISOString(), t, now);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Middle-truncate an identity for the recent list.
|
|
1144
|
+
*
|
|
1145
|
+
* Both ends carry the signal — an email's domain, a model id's tier suffix —
|
|
1146
|
+
* so the cut is taken from the MIDDLE, and bounding the rendered length (not
|
|
1147
|
+
* relying on CSS clipping of a `table-layout: fixed` cell) is what keeps the
|
|
1148
|
+
* columns honest. The full value stays on the cell's title.
|
|
1149
|
+
* @param text - the full identity.
|
|
1150
|
+
* @param max - rendered character budget including the ellipsis.
|
|
1151
|
+
* @returns the truncated display string.
|
|
1152
|
+
*/
|
|
1153
|
+
function truncateIdentity(text, max = 22) {
|
|
1154
|
+
if (text.length <= max) return text;
|
|
1155
|
+
const head = Math.ceil((max - 1) / 2);
|
|
1156
|
+
const tail = max - 1 - head;
|
|
1157
|
+
return `${text.slice(0, head)}…${text.slice(-tail)}`;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
996
1160
|
* A host-styled button.
|
|
997
1161
|
*
|
|
998
1162
|
* Delegates to the host's `Button` so focus rings, disabled states, size tiers
|
|
@@ -1061,36 +1225,44 @@ window.__ModuleLoader__.load({
|
|
|
1061
1225
|
const [proxyDraft, setProxyDraft] = (0, react.useState)("");
|
|
1062
1226
|
const usage = account.usage;
|
|
1063
1227
|
const now = Date.now();
|
|
1228
|
+
const throughput = usage === null ? null : throughputTokenPerSecond(usage.totals);
|
|
1064
1229
|
const identityRows = [
|
|
1065
1230
|
[t("fieldProject"), account.projectId ?? t("noProject")],
|
|
1066
1231
|
[t("fieldProxy"), h("span", { className: "agy-mono" }, account.proxy ?? t("proxyDirect"))],
|
|
1067
1232
|
[t("fieldFingerprint"), account.fingerprint === null ? t("fingerprintNone") : t("fingerprintRegenerated", {
|
|
1068
1233
|
count: account.fingerprintHistory,
|
|
1069
|
-
date: new Date(account.fingerprint.createdAt).toLocaleDateString()
|
|
1234
|
+
date: new Date(account.fingerprint.createdAt).toLocaleDateString(props.lang)
|
|
1070
1235
|
})],
|
|
1071
|
-
|
|
1236
|
+
...account.cooldownReason === null ? [] : [[t("fieldCooldownReason"), `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`]],
|
|
1072
1237
|
[t("fieldSources"), usage === null ? t("noProject") : t("sourcesSummary", {
|
|
1073
1238
|
chat: usage.sources.chat,
|
|
1074
1239
|
cli: usage.sources.cli,
|
|
1075
1240
|
verify: usage.sources.verify,
|
|
1076
1241
|
test: usage.sources.test
|
|
1077
1242
|
})],
|
|
1078
|
-
[t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`]
|
|
1243
|
+
[t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`],
|
|
1244
|
+
...throughput === null ? [] : [[t("fieldThroughput"), `${t("throughputValue", { n: throughput })} · ${t("throughputNote")}`]]
|
|
1079
1245
|
];
|
|
1246
|
+
if (account.state === "disabled") identityRows.push([t("fieldDisabled"), account.disabledAt === null ? t("disabledCredentials") : `${t("disabledCredentials")} · ${t("disabledSince", { ago: agoText(account.disabledAt, t, now) })}`]);
|
|
1080
1247
|
if (account.verificationRequired) identityRows.push([t("fieldVerification"), account.verificationUrl === null ? t("verificationNoUrl") : h("a", {
|
|
1081
1248
|
className: "agy-link",
|
|
1082
1249
|
href: account.verificationUrl,
|
|
1083
1250
|
target: "_blank",
|
|
1084
1251
|
rel: "noreferrer noopener"
|
|
1085
1252
|
}, t("verificationOpen"))]);
|
|
1086
|
-
const identity = card(t("detailTitle"), defs(identityRows), account.email ?? `#${account.index}`);
|
|
1253
|
+
const identity = card(t("detailTitle"), h("div", null, defs(identityRows), account.state === "disabled" ? hint(t("disabledHint")) : null), account.email ?? `#${account.index}`);
|
|
1087
1254
|
const actions = card(t("colActions"), h("div", { className: "agy-actions" }, button(t("actionTest"), () => {
|
|
1088
1255
|
handlers.onTest(account.index);
|
|
1089
1256
|
}, { disabled: busy }), button(t("actionExport"), () => {
|
|
1090
1257
|
handlers.onExport(account.index);
|
|
1091
1258
|
}, { disabled: busy }), button(t("actionRegenerateFingerprint"), () => {
|
|
1092
1259
|
handlers.onRegenerateFingerprint(account.index);
|
|
1093
|
-
}, { disabled: busy })))
|
|
1260
|
+
}, { disabled: busy }), button(t("actionDelete"), () => {
|
|
1261
|
+
handlers.onDelete(account.index);
|
|
1262
|
+
}, {
|
|
1263
|
+
variant: "danger",
|
|
1264
|
+
disabled: busy
|
|
1265
|
+
})));
|
|
1094
1266
|
/**
|
|
1095
1267
|
* The 5-hour / weekly windows, placed ABOVE the cumulative usage card.
|
|
1096
1268
|
*
|
|
@@ -1111,13 +1283,14 @@ window.__ModuleLoader__.load({
|
|
|
1111
1283
|
key: group.name
|
|
1112
1284
|
}, h("div", { className: "agy-limit-group-name" }, group.name), ...group.windows.map((window) => {
|
|
1113
1285
|
const fraction = window.remainingFraction;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1286
|
+
const burn = account.limitBurn?.[window.bucketId];
|
|
1287
|
+
const hoursLeft = fraction !== null && burn !== void 0 && burn > 0 ? fraction / burn : null;
|
|
1288
|
+
const resetHours = window.resetTime === null ? null : (new Date(window.resetTime).getTime() - now) / HOUR_MS;
|
|
1289
|
+
const exhaustsFirst = hoursLeft !== null && resetHours !== null && hoursLeft < resetHours;
|
|
1290
|
+
return h("div", { key: window.bucketId }, h("div", { className: "agy-limit-row" }, h("span", { className: "agy-limit-k" }, windowLabel(window.window, t)), h("span", { className: "agy-limit-track" }, fraction === null ? null : h("i", { style: {
|
|
1118
1291
|
width: `${Math.round(fraction * 100)}%`,
|
|
1119
1292
|
background: quotaColor(fraction)
|
|
1120
|
-
} })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now)));
|
|
1293
|
+
} })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now))), exhaustsFirst ? h("div", { className: "agy-limit-burn" }, t("limitBurnWarn", { value: burnHorizon(hoursLeft, t) })) : null);
|
|
1121
1294
|
})))));
|
|
1122
1295
|
const usageBlock = usage === null ? null : card(t("usageCumulative"), metrics([
|
|
1123
1296
|
metric(t("kpiInput"), promptTokens(usage.totals), t("kpiInputMissed", { tokens: tokenText(usage.totals.input) })),
|
|
@@ -1153,14 +1326,69 @@ window.__ModuleLoader__.load({
|
|
|
1153
1326
|
});
|
|
1154
1327
|
})())));
|
|
1155
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Whether the row should offer "set as current".
|
|
1331
|
+
*
|
|
1332
|
+
* A pure rule, exported for a direct unit test. Two cases hide the action:
|
|
1333
|
+
* the account is already the pool's preference, or it is DISABLED — `activate`
|
|
1334
|
+
* only writes the preference and cannot re-enable, so on a disabled row the
|
|
1335
|
+
* button was a no-op the user would read as broken. The repair path for a
|
|
1336
|
+
* button was a no-op the user would read as broken. The repair path for a
|
|
1337
|
+
* disabled account is `actionVerify`, which stays on the row.
|
|
1338
|
+
*/
|
|
1339
|
+
function canActivateAccount(account) {
|
|
1340
|
+
return !account.active && account.state !== "disabled";
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Pick the selected account index: defaults to the active account (badge "current")
|
|
1344
|
+
* when present, otherwise falls back to index 0. Clamps to valid bounds.
|
|
1345
|
+
*/
|
|
1346
|
+
function resolveSelectedAccountIndex(accounts, selected) {
|
|
1347
|
+
if (accounts.length === 0) return 0;
|
|
1348
|
+
if (selected !== null) {
|
|
1349
|
+
if (selected >= 0 && selected < accounts.length) return selected;
|
|
1350
|
+
const activePos = accounts.findIndex((a) => a.active);
|
|
1351
|
+
return activePos >= 0 ? activePos : Math.min(Math.max(selected, 0), accounts.length - 1);
|
|
1352
|
+
}
|
|
1353
|
+
const activePos = accounts.findIndex((a) => a.active);
|
|
1354
|
+
return activePos >= 0 ? activePos : 0;
|
|
1355
|
+
}
|
|
1156
1356
|
function AccountsTab(props) {
|
|
1157
1357
|
const { accounts, busy, handlers, t } = props;
|
|
1158
|
-
const [selected, setSelected] = (0, react.useState)(
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1358
|
+
const [selected, setSelected] = (0, react.useState)(null);
|
|
1359
|
+
const selectedRef = (0, react.useRef)(null);
|
|
1360
|
+
const now = Date.now();
|
|
1361
|
+
/**
|
|
1362
|
+
* The live line: who is serving right now, and how much.
|
|
1363
|
+
*
|
|
1364
|
+
* Present ONLY while something is in flight — an idle pool renders no strip,
|
|
1365
|
+
* so the quiet state stays quiet. The dot is the host `StateDot`'s ongoing
|
|
1366
|
+
* state, so the animation is the platform's.
|
|
1367
|
+
*/
|
|
1368
|
+
const liveLine = props.busyNow.length === 0 ? null : (() => {
|
|
1369
|
+
const total = props.busyNow.reduce((sum, entry) => sum + entry.count, 0);
|
|
1370
|
+
const [first] = props.busyNow;
|
|
1371
|
+
const subject = first === void 0 ? "" : first.email ?? `#${first.index}`;
|
|
1372
|
+
return h("div", { className: "agy-live" }, h(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
|
|
1373
|
+
state: "ongoing",
|
|
1374
|
+
size: 8
|
|
1375
|
+
}), h("span", null, props.busyNow.length === 1 ? t("liveOne", {
|
|
1376
|
+
email: subject,
|
|
1377
|
+
count: total
|
|
1378
|
+
}) : t("liveMany", {
|
|
1379
|
+
count: total,
|
|
1380
|
+
accounts: props.busyNow.length
|
|
1381
|
+
})));
|
|
1382
|
+
})();
|
|
1383
|
+
const index = resolveSelectedAccountIndex(accounts, selected);
|
|
1161
1384
|
const current = accounts[index];
|
|
1385
|
+
(0, react.useEffect)(() => {
|
|
1386
|
+
selectedRef.current?.scrollIntoView?.({ block: "nearest" });
|
|
1387
|
+
}, [index]);
|
|
1388
|
+
if (accounts.length === 0) return card(t("colAccount"), h("div", { className: "agy-empty" }, t("emptyAccounts")));
|
|
1162
1389
|
const rows = accounts.map((account, at) => h("div", {
|
|
1163
1390
|
key: String(account.index),
|
|
1391
|
+
ref: at === index ? selectedRef : void 0,
|
|
1164
1392
|
className: "agy-rowitem",
|
|
1165
1393
|
"data-clickable": "true",
|
|
1166
1394
|
"data-selected": at === index,
|
|
@@ -1175,30 +1403,35 @@ window.__ModuleLoader__.load({
|
|
|
1175
1403
|
event.preventDefault();
|
|
1176
1404
|
setSelected(at);
|
|
1177
1405
|
}
|
|
1178
|
-
}, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("
|
|
1406
|
+
}, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("rowRequestsTotal", { n: account.usage.totals.requests })}`, account.usage === null || account.usage.lastUsedAt === null ? null : ` · ${t("lastActive", { ago: agoText(new Date(account.usage.lastUsedAt).toISOString(), t, now) })}`)), h("div", { className: "agy-rowactions" }, stateBadge(account.state, account.state === "cooling" ? `${t("coolingUntil")} ${clockTime(account.cooldownUntil, props.lang)}` : account.state === "verification-required" ? t("verificationRetry", { time: clockTime(account.cooldownUntil, props.lang) }) : stateLabel(account.state, t)), canActivateAccount(account) ? button(t("actionActivate"), () => {
|
|
1407
|
+
setSelected(at);
|
|
1179
1408
|
handlers.onActivate(account.index);
|
|
1180
1409
|
}, {
|
|
1181
1410
|
size: "sm",
|
|
1182
1411
|
disabled: busy
|
|
1183
|
-
}), button(t("actionVerify"), () => {
|
|
1412
|
+
}) : null, button(t("actionVerify"), () => {
|
|
1184
1413
|
handlers.onVerify(account.index);
|
|
1185
1414
|
}, {
|
|
1186
1415
|
size: "sm",
|
|
1187
1416
|
disabled: busy
|
|
1188
|
-
}), button(t("actionDelete"), () => {
|
|
1189
|
-
handlers.onDelete(account.index);
|
|
1190
|
-
}, {
|
|
1191
|
-
size: "sm",
|
|
1192
|
-
variant: "danger",
|
|
1193
|
-
disabled: busy
|
|
1194
1417
|
}))));
|
|
1195
|
-
return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", { className: "agy-rows" }, ...rows), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
|
|
1418
|
+
return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", null, liveLine, h("div", { className: "agy-rows" }, ...rows)), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
|
|
1196
1419
|
key: String(current.index),
|
|
1197
1420
|
account: current,
|
|
1198
1421
|
busy,
|
|
1199
|
-
handlers
|
|
1422
|
+
handlers: {
|
|
1423
|
+
...handlers,
|
|
1424
|
+
onDelete: (index) => {
|
|
1425
|
+
setSelected(null);
|
|
1426
|
+
handlers.onDelete(index);
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
lang: props.lang,
|
|
1200
1430
|
t
|
|
1201
|
-
}))
|
|
1431
|
+
})), h(RecentCard, {
|
|
1432
|
+
rpc: props.rpc,
|
|
1433
|
+
t
|
|
1434
|
+
})));
|
|
1202
1435
|
}
|
|
1203
1436
|
/**
|
|
1204
1437
|
* Models tab: per-model visibility switches, plus the account's model quota in a
|
|
@@ -1556,6 +1789,110 @@ window.__ModuleLoader__.load({
|
|
|
1556
1789
|
}))) : null);
|
|
1557
1790
|
return card(t("thinkingTitle"), block);
|
|
1558
1791
|
}
|
|
1792
|
+
/** Result vocabulary for one recent row; rotation events are their own kind. */
|
|
1793
|
+
function recentResultKind(entry) {
|
|
1794
|
+
if (entry.kind === "rotation") return "rotation";
|
|
1795
|
+
if (entry.rateLimited) return "limited";
|
|
1796
|
+
if (!entry.ok) return "fail";
|
|
1797
|
+
return "ok";
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Localized label for a failure-classification token, falling back to the raw
|
|
1801
|
+
* token so a classification added upstream still reads as something.
|
|
1802
|
+
*/
|
|
1803
|
+
function failureReasonLabel(reason, t) {
|
|
1804
|
+
switch (reason) {
|
|
1805
|
+
case "rate-limit": return t("colRateLimited");
|
|
1806
|
+
case "network-error": return t("cooldownReasonNetworkError");
|
|
1807
|
+
case "auth-failure": return t("disabledCredentials");
|
|
1808
|
+
case "verification-required": return t("cooldownReasonValidationRequired");
|
|
1809
|
+
case "quota-exhausted": return t("cooldownReasonQuotaExhausted");
|
|
1810
|
+
case "project-error": return t("cooldownReasonProjectError");
|
|
1811
|
+
default: return reason;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* The result cell's text. Rotation rows say WHY (`reason` rides the record
|
|
1816
|
+
* since the ring captures the classification); a failed request with a
|
|
1817
|
+
* non-rate-limit classification also names it — the rate-limit case is already
|
|
1818
|
+
* the whole RateLimited label, and doubling it reads as a stutter.
|
|
1819
|
+
*/
|
|
1820
|
+
function recentResultText(entry, t) {
|
|
1821
|
+
const kind = recentResultKind(entry);
|
|
1822
|
+
if (kind === "rotation") return entry.reason === null ? t("colRotations") : `${t("colRotations")} · ${failureReasonLabel(entry.reason, t)}`;
|
|
1823
|
+
if (kind === "limited") return t("colRateLimited");
|
|
1824
|
+
if (kind === "fail") return entry.reason === null || entry.reason === "rate-limit" ? t("colFailed") : `${t("colFailed")} · ${failureReasonLabel(entry.reason, t)}`;
|
|
1825
|
+
return t("recentOk");
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* The "what just happened" list: the most recent records this process saw,
|
|
1829
|
+
* newest first, collapsed by default and polled while open.
|
|
1830
|
+
*
|
|
1831
|
+
* A component (never called directly) — it owns hooks; see ThinkingSamples for
|
|
1832
|
+
* the hook-order rule this file has been bitten by twice. The poll is the same
|
|
1833
|
+
* 3s cadence as the live line and reads the host's memory ring, so an open
|
|
1834
|
+
* list costs nothing upstream.
|
|
1835
|
+
*/
|
|
1836
|
+
function RecentCard(props) {
|
|
1837
|
+
const { rpc, t } = props;
|
|
1838
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1839
|
+
const [recent, setRecent] = (0, react.useState)(null);
|
|
1840
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1841
|
+
const alive = (0, react.useRef)(true);
|
|
1842
|
+
(0, react.useEffect)(() => {
|
|
1843
|
+
alive.current = true;
|
|
1844
|
+
return () => {
|
|
1845
|
+
alive.current = false;
|
|
1846
|
+
};
|
|
1847
|
+
}, []);
|
|
1848
|
+
const load = (0, react.useCallback)(() => {
|
|
1849
|
+
rpc.call("pool.recent", {}).then((result) => {
|
|
1850
|
+
if (alive.current) {
|
|
1851
|
+
setRecent(result.recent);
|
|
1852
|
+
setError(void 0);
|
|
1853
|
+
}
|
|
1854
|
+
}).catch((caught) => {
|
|
1855
|
+
if (alive.current) setError(caught instanceof Error ? caught.message : String(caught));
|
|
1856
|
+
});
|
|
1857
|
+
}, [rpc]);
|
|
1858
|
+
(0, react.useEffect)(() => {
|
|
1859
|
+
if (open && recent === null) load();
|
|
1860
|
+
}, [
|
|
1861
|
+
open,
|
|
1862
|
+
recent,
|
|
1863
|
+
load
|
|
1864
|
+
]);
|
|
1865
|
+
(0, react.useEffect)(() => {
|
|
1866
|
+
if (!open) return;
|
|
1867
|
+
const timer = setInterval(() => {
|
|
1868
|
+
if (!document.hidden) load();
|
|
1869
|
+
}, POOL_POLL_INTERVAL_MS);
|
|
1870
|
+
return () => {
|
|
1871
|
+
clearInterval(timer);
|
|
1872
|
+
};
|
|
1873
|
+
}, [open, load]);
|
|
1874
|
+
const now = Date.now();
|
|
1875
|
+
return h("div", {
|
|
1876
|
+
className: "agy-disclosure agy-recent",
|
|
1877
|
+
"data-open": open
|
|
1878
|
+
}, h("button", {
|
|
1879
|
+
type: "button",
|
|
1880
|
+
className: "agy-disclosure-toggle",
|
|
1881
|
+
"aria-expanded": open,
|
|
1882
|
+
onClick: () => {
|
|
1883
|
+
setOpen(!open);
|
|
1884
|
+
}
|
|
1885
|
+
}, h("span", { className: "agy-caret" }), h("span", null, t("recentTitle")), recent === null ? null : h("span", { className: "agy-disclosure-meta" }, String(recent.length))), open === false ? null : h("div", { className: "agy-disclosure-body" }, error === void 0 ? null : h("div", { className: "agy-error" }, error), hint(t("recentHelp")), recent === null ? h("div", { className: "agy-empty" }, t("loading")) : recent.length === 0 ? h("div", { className: "agy-empty" }, t("recentEmpty")) : h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", { style: { width: "64px" } }, t("colTime")), h("th", { style: { width: "30%" } }, t("colAccount")), h("th", { style: { width: "24%" } }, t("colModel")), h("th", { style: { width: "96px" } }, t("colResult")), h("th", { style: { width: "56px" } }, t("colDuration")), h("th", { style: { width: "48px" } }, t("colOutput"))), recent.map((entry, index) => h("tr", { key: `${entry.at}-${index}` }, h("td", null, recentAgo(entry.at, now, t)), h("td", null, h("span", {
|
|
1886
|
+
className: "agy-mail",
|
|
1887
|
+
title: entry.account ?? void 0
|
|
1888
|
+
}, entry.account === null ? "—" : truncateIdentity(entry.account))), h("td", null, h("span", {
|
|
1889
|
+
className: "agy-mail",
|
|
1890
|
+
title: entry.model ?? void 0
|
|
1891
|
+
}, entry.model === null ? "—" : truncateIdentity(entry.model))), h("td", null, h("span", {
|
|
1892
|
+
className: "agy-recent-state",
|
|
1893
|
+
"data-kind": recentResultKind(entry)
|
|
1894
|
+
}, recentResultText(entry, t))), h("td", null, entry.latencyMs === null ? "—" : formatDuration(entry.latencyMs)), h("td", { className: "agy-num" }, entry.output === null ? "—" : tokenText(entry.output))))))));
|
|
1895
|
+
}
|
|
1559
1896
|
const RANGE_IDS = [
|
|
1560
1897
|
"today",
|
|
1561
1898
|
"week",
|
|
@@ -1667,7 +2004,7 @@ window.__ModuleLoader__.load({
|
|
|
1667
2004
|
onClick: () => {
|
|
1668
2005
|
setRange(id);
|
|
1669
2006
|
}
|
|
1670
|
-
}, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString() }))), card(t("usageTitle"), h("div", null, metrics([
|
|
2007
|
+
}, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString(props.lang) }))), card(t("usageTitle"), h("div", null, metrics([
|
|
1671
2008
|
metric(t("kpiTotalTokens"), total, t("kpiTotalDetail", {
|
|
1672
2009
|
requests: counters.requests,
|
|
1673
2010
|
failed: counters.failed
|
|
@@ -1680,7 +2017,7 @@ window.__ModuleLoader__.load({
|
|
|
1680
2017
|
[t("fieldRateLimitRotation"), `${counters.rateLimited} / ${counters.rotations}`],
|
|
1681
2018
|
[t("labelLatencyAverage"), formatDuration(average(counters.latencyMs, counters.latencyN))],
|
|
1682
2019
|
[t("labelTtft"), formatDuration(average(counters.ttftMs, counters.ttftN))]
|
|
1683
|
-
])), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
|
|
2020
|
+
])), card(t("trendTitle"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colDay")), numHeader(0, t("colRequests")), numHeader(1, t("colFailed")), numHeader(2, t("colRateLimited")), numHeader(3, t("colRotations"))), stats.days.map((row) => h("tr", { key: row.day }, h("td", null, dayLabel(row.day, props.lang)), h("td", { className: "agy-num" }, String(row.requests)), h("td", { className: "agy-num" }, String(row.failed)), h("td", { className: "agy-num" }, String(row.rateLimited)), h("td", { className: "agy-num" }, String(row.rotations))))))), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
|
|
1684
2021
|
}
|
|
1685
2022
|
function CredentialsTab(props) {
|
|
1686
2023
|
const { t } = props;
|
|
@@ -1720,6 +2057,15 @@ window.__ModuleLoader__.load({
|
|
|
1720
2057
|
/** Model ids whose own test call is in flight (see the per-row test button). */
|
|
1721
2058
|
const [modelTesting, setModelTesting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
1722
2059
|
const [stats, setStats] = (0, react.useState)(null);
|
|
2060
|
+
/**
|
|
2061
|
+
* Accounts with upstream requests in flight, from the polled `pool.status`.
|
|
2062
|
+
*
|
|
2063
|
+
* A poll, not a push: the section has no push channel, and the call is a pure
|
|
2064
|
+
* in-memory read on the host. A failed poll is display-only and stays silent —
|
|
2065
|
+
* the previous frame (including "idle") holds until the next tick, which is
|
|
2066
|
+
* the honest degradation for a status line.
|
|
2067
|
+
*/
|
|
2068
|
+
const [poolBusy, setPoolBusy] = (0, react.useState)([]);
|
|
1723
2069
|
const [error, setError] = (0, react.useState)(void 0);
|
|
1724
2070
|
/** A non-fatal outcome worth reporting (e.g. a partial credential import). */
|
|
1725
2071
|
const [notice, setNoticeState] = (0, react.useState)(void 0);
|
|
@@ -1821,7 +2167,8 @@ window.__ModuleLoader__.load({
|
|
|
1821
2167
|
return {
|
|
1822
2168
|
...account,
|
|
1823
2169
|
limits: entry.groups,
|
|
1824
|
-
limitsUpdatedAt: entry.updatedAt
|
|
2170
|
+
limitsUpdatedAt: entry.updatedAt,
|
|
2171
|
+
limitBurn: entry.burn ?? account.limitBurn
|
|
1825
2172
|
};
|
|
1826
2173
|
}));
|
|
1827
2174
|
if (report) {
|
|
@@ -1841,6 +2188,19 @@ window.__ModuleLoader__.load({
|
|
|
1841
2188
|
(0, react.useEffect)(() => {
|
|
1842
2189
|
refresh();
|
|
1843
2190
|
}, [refresh]);
|
|
2191
|
+
(0, react.useEffect)(() => {
|
|
2192
|
+
const tick = () => {
|
|
2193
|
+
if (document.hidden) return;
|
|
2194
|
+
rpc.call("pool.status", {}).then((result) => {
|
|
2195
|
+
if (alive.current) setPoolBusy(result.busy);
|
|
2196
|
+
}).catch(() => {});
|
|
2197
|
+
};
|
|
2198
|
+
tick();
|
|
2199
|
+
const timer = setInterval(tick, POOL_POLL_INTERVAL_MS);
|
|
2200
|
+
return () => {
|
|
2201
|
+
clearInterval(timer);
|
|
2202
|
+
};
|
|
2203
|
+
}, [rpc]);
|
|
1844
2204
|
(0, react.useEffect)(() => {
|
|
1845
2205
|
if (tab === "accounts" && accounts.length > 0) loadLimits();
|
|
1846
2206
|
}, [
|
|
@@ -2054,7 +2414,10 @@ window.__ModuleLoader__.load({
|
|
|
2054
2414
|
const body = tab === "accounts" ? h(AccountsTab, {
|
|
2055
2415
|
accounts,
|
|
2056
2416
|
busy,
|
|
2417
|
+
busyNow: poolBusy,
|
|
2057
2418
|
handlers,
|
|
2419
|
+
lang: props.lang,
|
|
2420
|
+
rpc,
|
|
2058
2421
|
t
|
|
2059
2422
|
}) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
|
|
2060
2423
|
models,
|
|
@@ -2131,6 +2494,7 @@ window.__ModuleLoader__.load({
|
|
|
2131
2494
|
loadModels();
|
|
2132
2495
|
}, { size: "sm" })) : tab === "usage" ? h(UsageTab, {
|
|
2133
2496
|
stats,
|
|
2497
|
+
lang: props.lang,
|
|
2134
2498
|
t
|
|
2135
2499
|
}) : h(CredentialsTab, {
|
|
2136
2500
|
busy,
|
|
@@ -2184,6 +2548,7 @@ window.__ModuleLoader__.load({
|
|
|
2184
2548
|
}
|
|
2185
2549
|
const rpc = createRpc(connection);
|
|
2186
2550
|
const t = ctx.locale.bind(NS);
|
|
2551
|
+
const lang = ctx.locale.getLocale?.().active;
|
|
2187
2552
|
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2188
2553
|
name: "settings.section",
|
|
2189
2554
|
id: "agy",
|
|
@@ -2192,15 +2557,20 @@ window.__ModuleLoader__.load({
|
|
|
2192
2557
|
label: () => t("title")
|
|
2193
2558
|
}, () => h(AgySettings, {
|
|
2194
2559
|
rpc,
|
|
2195
|
-
t
|
|
2560
|
+
t,
|
|
2561
|
+
lang
|
|
2196
2562
|
}))), "dsh-agy: Settings section");
|
|
2197
2563
|
}
|
|
2198
2564
|
//#endregion
|
|
2199
2565
|
exports.AgySettings = AgySettings;
|
|
2200
2566
|
exports.apply = apply;
|
|
2567
|
+
exports.canActivateAccount = canActivateAccount;
|
|
2201
2568
|
exports.inject = inject;
|
|
2202
2569
|
exports.orderModels = orderModels;
|
|
2570
|
+
exports.resolveSelectedAccountIndex = resolveSelectedAccountIndex;
|
|
2571
|
+
exports.throughputTokenPerSecond = throughputTokenPerSecond;
|
|
2203
2572
|
exports.tokenText = tokenText;
|
|
2573
|
+
exports.truncateIdentity = truncateIdentity;
|
|
2204
2574
|
return module.exports;
|
|
2205
2575
|
}
|
|
2206
2576
|
});
|