dsh-agy 0.4.0 → 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-BN4h0AU4.mjs → accounts-BjdDqUq1.mjs} +180 -13
- 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 +382 -34
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-B7OSfnyl.mjs} +300 -14
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/web/plugin.mjs +35 -13
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/accounts-BN4h0AU4.mjs.map +0 -1
- package/lib/plugin-common-N7p9Cm-3.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) })),
|
|
@@ -1154,6 +1327,19 @@ window.__ModuleLoader__.load({
|
|
|
1154
1327
|
})())));
|
|
1155
1328
|
}
|
|
1156
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
|
+
/**
|
|
1157
1343
|
* Pick the selected account index: defaults to the active account (badge "current")
|
|
1158
1344
|
* when present, otherwise falls back to index 0. Clamps to valid bounds.
|
|
1159
1345
|
*/
|
|
@@ -1171,6 +1357,29 @@ window.__ModuleLoader__.load({
|
|
|
1171
1357
|
const { accounts, busy, handlers, t } = props;
|
|
1172
1358
|
const [selected, setSelected] = (0, react.useState)(null);
|
|
1173
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
|
+
})();
|
|
1174
1383
|
const index = resolveSelectedAccountIndex(accounts, selected);
|
|
1175
1384
|
const current = accounts[index];
|
|
1176
1385
|
(0, react.useEffect)(() => {
|
|
@@ -1194,32 +1403,35 @@ window.__ModuleLoader__.load({
|
|
|
1194
1403
|
event.preventDefault();
|
|
1195
1404
|
setSelected(at);
|
|
1196
1405
|
}
|
|
1197
|
-
}, 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"), () => {
|
|
1198
1407
|
setSelected(at);
|
|
1199
1408
|
handlers.onActivate(account.index);
|
|
1200
1409
|
}, {
|
|
1201
1410
|
size: "sm",
|
|
1202
1411
|
disabled: busy
|
|
1203
|
-
}), button(t("actionVerify"), () => {
|
|
1412
|
+
}) : null, button(t("actionVerify"), () => {
|
|
1204
1413
|
handlers.onVerify(account.index);
|
|
1205
1414
|
}, {
|
|
1206
1415
|
size: "sm",
|
|
1207
1416
|
disabled: busy
|
|
1208
|
-
}), button(t("actionDelete"), () => {
|
|
1209
|
-
setSelected(null);
|
|
1210
|
-
handlers.onDelete(account.index);
|
|
1211
|
-
}, {
|
|
1212
|
-
size: "sm",
|
|
1213
|
-
variant: "danger",
|
|
1214
|
-
disabled: busy
|
|
1215
1417
|
}))));
|
|
1216
|
-
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, {
|
|
1217
1419
|
key: String(current.index),
|
|
1218
1420
|
account: current,
|
|
1219
1421
|
busy,
|
|
1220
|
-
handlers
|
|
1422
|
+
handlers: {
|
|
1423
|
+
...handlers,
|
|
1424
|
+
onDelete: (index) => {
|
|
1425
|
+
setSelected(null);
|
|
1426
|
+
handlers.onDelete(index);
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
lang: props.lang,
|
|
1221
1430
|
t
|
|
1222
|
-
}))
|
|
1431
|
+
})), h(RecentCard, {
|
|
1432
|
+
rpc: props.rpc,
|
|
1433
|
+
t
|
|
1434
|
+
})));
|
|
1223
1435
|
}
|
|
1224
1436
|
/**
|
|
1225
1437
|
* Models tab: per-model visibility switches, plus the account's model quota in a
|
|
@@ -1577,6 +1789,110 @@ window.__ModuleLoader__.load({
|
|
|
1577
1789
|
}))) : null);
|
|
1578
1790
|
return card(t("thinkingTitle"), block);
|
|
1579
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
|
+
}
|
|
1580
1896
|
const RANGE_IDS = [
|
|
1581
1897
|
"today",
|
|
1582
1898
|
"week",
|
|
@@ -1688,7 +2004,7 @@ window.__ModuleLoader__.load({
|
|
|
1688
2004
|
onClick: () => {
|
|
1689
2005
|
setRange(id);
|
|
1690
2006
|
}
|
|
1691
|
-
}, 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([
|
|
1692
2008
|
metric(t("kpiTotalTokens"), total, t("kpiTotalDetail", {
|
|
1693
2009
|
requests: counters.requests,
|
|
1694
2010
|
failed: counters.failed
|
|
@@ -1701,7 +2017,7 @@ window.__ModuleLoader__.load({
|
|
|
1701
2017
|
[t("fieldRateLimitRotation"), `${counters.rateLimited} / ${counters.rotations}`],
|
|
1702
2018
|
[t("labelLatencyAverage"), formatDuration(average(counters.latencyMs, counters.latencyN))],
|
|
1703
2019
|
[t("labelTtft"), formatDuration(average(counters.ttftMs, counters.ttftN))]
|
|
1704
|
-
])), 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))))))));
|
|
1705
2021
|
}
|
|
1706
2022
|
function CredentialsTab(props) {
|
|
1707
2023
|
const { t } = props;
|
|
@@ -1741,6 +2057,15 @@ window.__ModuleLoader__.load({
|
|
|
1741
2057
|
/** Model ids whose own test call is in flight (see the per-row test button). */
|
|
1742
2058
|
const [modelTesting, setModelTesting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
1743
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)([]);
|
|
1744
2069
|
const [error, setError] = (0, react.useState)(void 0);
|
|
1745
2070
|
/** A non-fatal outcome worth reporting (e.g. a partial credential import). */
|
|
1746
2071
|
const [notice, setNoticeState] = (0, react.useState)(void 0);
|
|
@@ -1842,7 +2167,8 @@ window.__ModuleLoader__.load({
|
|
|
1842
2167
|
return {
|
|
1843
2168
|
...account,
|
|
1844
2169
|
limits: entry.groups,
|
|
1845
|
-
limitsUpdatedAt: entry.updatedAt
|
|
2170
|
+
limitsUpdatedAt: entry.updatedAt,
|
|
2171
|
+
limitBurn: entry.burn ?? account.limitBurn
|
|
1846
2172
|
};
|
|
1847
2173
|
}));
|
|
1848
2174
|
if (report) {
|
|
@@ -1862,6 +2188,19 @@ window.__ModuleLoader__.load({
|
|
|
1862
2188
|
(0, react.useEffect)(() => {
|
|
1863
2189
|
refresh();
|
|
1864
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]);
|
|
1865
2204
|
(0, react.useEffect)(() => {
|
|
1866
2205
|
if (tab === "accounts" && accounts.length > 0) loadLimits();
|
|
1867
2206
|
}, [
|
|
@@ -2075,7 +2414,10 @@ window.__ModuleLoader__.load({
|
|
|
2075
2414
|
const body = tab === "accounts" ? h(AccountsTab, {
|
|
2076
2415
|
accounts,
|
|
2077
2416
|
busy,
|
|
2417
|
+
busyNow: poolBusy,
|
|
2078
2418
|
handlers,
|
|
2419
|
+
lang: props.lang,
|
|
2420
|
+
rpc,
|
|
2079
2421
|
t
|
|
2080
2422
|
}) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
|
|
2081
2423
|
models,
|
|
@@ -2152,6 +2494,7 @@ window.__ModuleLoader__.load({
|
|
|
2152
2494
|
loadModels();
|
|
2153
2495
|
}, { size: "sm" })) : tab === "usage" ? h(UsageTab, {
|
|
2154
2496
|
stats,
|
|
2497
|
+
lang: props.lang,
|
|
2155
2498
|
t
|
|
2156
2499
|
}) : h(CredentialsTab, {
|
|
2157
2500
|
busy,
|
|
@@ -2205,6 +2548,7 @@ window.__ModuleLoader__.load({
|
|
|
2205
2548
|
}
|
|
2206
2549
|
const rpc = createRpc(connection);
|
|
2207
2550
|
const t = ctx.locale.bind(NS);
|
|
2551
|
+
const lang = ctx.locale.getLocale?.().active;
|
|
2208
2552
|
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2209
2553
|
name: "settings.section",
|
|
2210
2554
|
id: "agy",
|
|
@@ -2213,16 +2557,20 @@ window.__ModuleLoader__.load({
|
|
|
2213
2557
|
label: () => t("title")
|
|
2214
2558
|
}, () => h(AgySettings, {
|
|
2215
2559
|
rpc,
|
|
2216
|
-
t
|
|
2560
|
+
t,
|
|
2561
|
+
lang
|
|
2217
2562
|
}))), "dsh-agy: Settings section");
|
|
2218
2563
|
}
|
|
2219
2564
|
//#endregion
|
|
2220
2565
|
exports.AgySettings = AgySettings;
|
|
2221
2566
|
exports.apply = apply;
|
|
2567
|
+
exports.canActivateAccount = canActivateAccount;
|
|
2222
2568
|
exports.inject = inject;
|
|
2223
2569
|
exports.orderModels = orderModels;
|
|
2224
2570
|
exports.resolveSelectedAccountIndex = resolveSelectedAccountIndex;
|
|
2571
|
+
exports.throughputTokenPerSecond = throughputTokenPerSecond;
|
|
2225
2572
|
exports.tokenText = tokenText;
|
|
2573
|
+
exports.truncateIdentity = truncateIdentity;
|
|
2226
2574
|
return module.exports;
|
|
2227
2575
|
}
|
|
2228
2576
|
});
|