dsh-token-total 1.1.3 → 1.1.4
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 +4 -0
- package/lib/client.js +39 -7
- package/lib/index.js +166 -180
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/session-usage-locales.d.ts +2 -0
- package/lib/types/session-tree.d.ts +16 -0
- package/lib/types/session-usage.d.ts +5 -33
- package/lib/types/wire.d.ts +16 -0
- package/package.json +1 -1
- package/src/client/SessionUsageView.tsx +6 -1
- package/src/client/StatsLineEnhanced.tsx +14 -5
- package/src/client/locales.ts +8 -0
- package/src/client/session-usage-core.ts +1 -0
- package/src/client/session-usage-locales.ts +3 -0
- package/src/session-detail.ts +7 -36
- package/src/session-tree.ts +146 -0
- package/src/session-usage.ts +32 -181
- package/src/wire.ts +18 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ DSH(DeepSeek Harness)Web 插件:**Token 用量统计看板** + **单会话
|
|
|
15
15
|
### 统计看板
|
|
16
16
|
|
|
17
17
|
入口在左侧边栏的插件区(与原生「插件」页并列的独立页面),点开即为看板,分上中下三部分。
|
|
18
|
+

|
|
18
19
|
|
|
19
20
|
#### 上部
|
|
20
21
|
|
|
@@ -40,6 +41,8 @@ DSH(DeepSeek Harness)Web 插件:**Token 用量统计看板** + **单会话
|
|
|
40
41
|
|
|
41
42
|
会话输入框下方原生的「用时与速度 / Token 用量」两枚胶囊被接管并增强:
|
|
42
43
|
|
|
44
|
+

|
|
45
|
+
|
|
43
46
|
- **Token 胶囊**显示的是**整棵会话树**(主智能体 + 全部子智能体)的合计,点开弹窗分为「主智能体 / 子智能体 / 会话合计」三行,并给出子智能体会话数。
|
|
44
47
|
- 子智能体是独立持久化的会话,通过日志头的 `parentSession` 串成树;插件按需重放这些日志汇总(同一套 `(turn, step)` 去重规则,且跳过 fork 继承来的事件前缀,避免与父会话重复计数)。
|
|
45
48
|
- **会话 Token 明细始终开启**:未命中缓存、缓存读 / 写、输出分别列出;底部胶囊的缓存命中率始终显示两位小数。
|
|
@@ -49,6 +52,7 @@ DSH(DeepSeek Harness)Web 插件:**Token 用量统计看板** + **单会话
|
|
|
49
52
|
### 会话「用量」页
|
|
50
53
|
|
|
51
54
|
在每个会话原生的「对话 / 轨迹」旁新增 **用量** tab;沿用会话切换与视图导航,不另造顶部栏或输入框。
|
|
55
|
+

|
|
52
56
|
|
|
53
57
|
- **两张实时卡**(形制参考 [magpie](https://github.com/yetone/magpie))并排位于子代理/模型明细之下、请求记录之上,回答「提示词里现在有什么」与「整场会话到底花了什么」两个不同的问题:
|
|
54
58
|
- 左卡「**当前上下文**」:`已用 / 模型窗口 tokens` 大字附占用百分比,标题行右侧是当前供应商 · 模型;下方一条**整条即模型窗口**的堆叠条——有颜色的部分是已用的七类(按类别占比分色),尾部是还没用上的余量,所以条本身就是占用率;再下面按统一图例列出系统提示词、用户消息、技能注入、工具结果、工具定义、注入内容、助手消息各自的占比与估算 token(输出不占窗口,故不列)。鼠标悬浮可读精确 token 数与占上下文比例;模型窗口未知时只写已用数。
|
package/lib/client.js
CHANGED
|
@@ -31531,7 +31531,8 @@ window.__ModuleLoader__.load({
|
|
|
31531
31531
|
self,
|
|
31532
31532
|
subagents: tree?.subagents ?? { ...ZERO },
|
|
31533
31533
|
total: tree === null ? self : combinedTotals(tree, self),
|
|
31534
|
-
subagentSessions: tree?.subagentSessions ?? 0
|
|
31534
|
+
subagentSessions: tree?.subagentSessions ?? 0,
|
|
31535
|
+
...tree?.coverage ? { coverage: tree.coverage } : {}
|
|
31535
31536
|
};
|
|
31536
31537
|
}
|
|
31537
31538
|
function hasAnyTokens(totals) {
|
|
@@ -31901,9 +31902,11 @@ window.__ModuleLoader__.load({
|
|
|
31901
31902
|
}
|
|
31902
31903
|
function UsagePill({ usage, tree, t, dialog, dock }) {
|
|
31903
31904
|
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
|
|
31904
|
-
const
|
|
31905
|
+
const display = sessionUsageForDisplay(usage, tree?.tree ?? null);
|
|
31906
|
+
const billed = display.total;
|
|
31905
31907
|
const total = billedTotal(billed);
|
|
31906
|
-
const
|
|
31908
|
+
const incomplete = display.coverage?.complete === false;
|
|
31909
|
+
const totalText = (incomplete ? "≥ " : "") + t("message.turnUsage.count", { count: formatTokensCompact(total) });
|
|
31907
31910
|
const cacheHit = cacheHitPercent(billed);
|
|
31908
31911
|
const cacheHitText = cacheHit !== null ? t("stats.cacheHit", { percent: cacheHit }) : null;
|
|
31909
31912
|
const exactCount = (value) => t("message.turnUsage.count", { count: formatTokens(value) });
|
|
@@ -31916,7 +31919,8 @@ window.__ModuleLoader__.load({
|
|
|
31916
31919
|
className: StatsLineEnhanced_module_default.pill,
|
|
31917
31920
|
"aria-haspopup": "dialog",
|
|
31918
31921
|
"aria-expanded": open,
|
|
31919
|
-
"aria-label": cacheHitText === null ? totalText : totalText + " · " + cacheHitText,
|
|
31922
|
+
"aria-label": (incomplete ? t("stats.partial") + " · " : "") + (cacheHitText === null ? totalText : totalText + " · " + cacheHitText),
|
|
31923
|
+
title: incomplete ? t("stats.incomplete", { count: display.coverage.unreadableSessions.length }) : void 0,
|
|
31920
31924
|
onClick: () => {
|
|
31921
31925
|
setOpen(!open);
|
|
31922
31926
|
},
|
|
@@ -31943,13 +31947,18 @@ window.__ModuleLoader__.load({
|
|
|
31943
31947
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DatabaseIcon, {}), t("stats.dialog.usageTitle")]
|
|
31944
31948
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31945
31949
|
className: StatsLineEnhanced_module_default.dialogTitleValue,
|
|
31946
|
-
children: exactCount(total)
|
|
31950
|
+
children: (incomplete ? "≥ " : "") + exactCount(total)
|
|
31947
31951
|
})]
|
|
31948
31952
|
}),
|
|
31949
31953
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31950
31954
|
className: StatsLineEnhanced_module_default.dialogTitleRule,
|
|
31951
31955
|
"aria-hidden": true
|
|
31952
31956
|
}),
|
|
31957
|
+
incomplete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31958
|
+
role: "status",
|
|
31959
|
+
"data-session-usage-partial": true,
|
|
31960
|
+
children: t("stats.incomplete", { count: display.coverage.unreadableSessions.length })
|
|
31961
|
+
}),
|
|
31953
31962
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
31954
31963
|
className: StatsLineEnhanced_module_default.dialogDetails,
|
|
31955
31964
|
"data-session-stats-usage": true,
|
|
@@ -31982,7 +31991,8 @@ window.__ModuleLoader__.load({
|
|
|
31982
31991
|
const stats = (0, react.useMemo)(() => projected ?? deriveStats(settledNodes ?? []), [projected, settledNodes]);
|
|
31983
31992
|
const tokenScale = (0, react.useMemo)(() => measuredTokenScale(settledNodes ?? []) ?? 1, [settledNodes]);
|
|
31984
31993
|
const tree = useSessionUsage(sessionId, usageSignature(usage, stats.steps));
|
|
31985
|
-
const
|
|
31994
|
+
const display = sessionUsageForDisplay(usage, tree.tree);
|
|
31995
|
+
const hasTokens = hasAnyTokens(display.total) || display.coverage?.complete === false;
|
|
31986
31996
|
if (stats.steps === 0 && !hasTokens) return null;
|
|
31987
31997
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31988
31998
|
ref: markDockContainer,
|
|
@@ -32048,7 +32058,8 @@ window.__ModuleLoader__.load({
|
|
|
32048
32058
|
const StatsUsageEntry = (0, react.memo)(function StatsUsageEntry({ useProjection, t, sessionId }) {
|
|
32049
32059
|
const usage = useProjection("tokenUsage");
|
|
32050
32060
|
const tree = useSessionUsage(sessionId, usageSignature(usage, usage === void 0 ? 0 : 1));
|
|
32051
|
-
|
|
32061
|
+
const display = sessionUsageForDisplay(usage, tree.tree);
|
|
32062
|
+
if (!(hasAnyTokens(display.total) || display.coverage?.complete === false)) return null;
|
|
32052
32063
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePill, {
|
|
32053
32064
|
usage,
|
|
32054
32065
|
tree,
|
|
@@ -32420,6 +32431,8 @@ window.__ModuleLoader__.load({
|
|
|
32420
32431
|
"stats.cacheMiss": "Cache miss",
|
|
32421
32432
|
"stats.dialog.title": "Time & speed",
|
|
32422
32433
|
"stats.dialog.usageTitle": "Token usage",
|
|
32434
|
+
"stats.partial": "Partial usage",
|
|
32435
|
+
"stats.incomplete": "{count} subagent session(s) could not be read; totals are incomplete.",
|
|
32423
32436
|
"stats.dialog.llmTime": "LLM time",
|
|
32424
32437
|
"stats.dialog.toolTime": "Tool time",
|
|
32425
32438
|
"stats.dialog.ttft": "Mean TTFT",
|
|
@@ -32508,6 +32521,8 @@ window.__ModuleLoader__.load({
|
|
|
32508
32521
|
"stats.cacheMiss": "未命中缓存",
|
|
32509
32522
|
"stats.dialog.title": "用时与速度",
|
|
32510
32523
|
"stats.dialog.usageTitle": "Token 用量",
|
|
32524
|
+
"stats.partial": "部分用量",
|
|
32525
|
+
"stats.incomplete": "有 {count} 个子会话暂不可读取,总计尚不完整。",
|
|
32511
32526
|
"stats.dialog.llmTime": "LLM 用时",
|
|
32512
32527
|
"stats.dialog.toolTime": "工具用时",
|
|
32513
32528
|
"stats.dialog.ttft": "首 token 平均",
|
|
@@ -32596,6 +32611,8 @@ window.__ModuleLoader__.load({
|
|
|
32596
32611
|
"stats.cacheMiss": "未命中快取",
|
|
32597
32612
|
"stats.dialog.title": "用時與速度",
|
|
32598
32613
|
"stats.dialog.usageTitle": "Token 用量",
|
|
32614
|
+
"stats.partial": "部分用量",
|
|
32615
|
+
"stats.incomplete": "有 {count} 個子會話暫不可讀取,總計尚不完整。",
|
|
32599
32616
|
"stats.dialog.llmTime": "LLM 用時",
|
|
32600
32617
|
"stats.dialog.toolTime": "工具用時",
|
|
32601
32618
|
"stats.dialog.ttft": "首 token 平均",
|
|
@@ -33844,6 +33861,15 @@ window.__ModuleLoader__.load({
|
|
|
33844
33861
|
className: SessionUsageView_module_default.scope,
|
|
33845
33862
|
children: t("scope")
|
|
33846
33863
|
}),
|
|
33864
|
+
detail.coverage?.complete === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33865
|
+
className: SessionUsageView_module_default.error,
|
|
33866
|
+
role: "status",
|
|
33867
|
+
"data-session-usage-partial": true,
|
|
33868
|
+
children: [t("partialCoverage", { count: String(detail.coverage.unreadableSessions.length) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33869
|
+
title: detail.coverage.unreadableSessions.map((issue) => issue.sessionId).join("\n"),
|
|
33870
|
+
children: t("partialTotals")
|
|
33871
|
+
})]
|
|
33872
|
+
}),
|
|
33847
33873
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metrics, {
|
|
33848
33874
|
detail,
|
|
33849
33875
|
t
|
|
@@ -33972,6 +33998,8 @@ window.__ModuleLoader__.load({
|
|
|
33972
33998
|
noModels: "暂无模型用量",
|
|
33973
33999
|
scope: "本会话及所有子代理 · 历史日志累计",
|
|
33974
34000
|
note: "统计包含本会话与全部后代的独立请求、重试和压缩调用,不重复计算继承历史。首 token 与速度仅使用已记录的时间信息,缺失时显示 —。",
|
|
34001
|
+
partialCoverage: "有 {count} 个子会话暂不可读取。",
|
|
34002
|
+
partialTotals: "当前仅显示已读取用量,未将缺失用量视为零。",
|
|
33975
34003
|
failedLoad: "用量读取失败,已保留上次结果。",
|
|
33976
34004
|
retry: "重新读取",
|
|
33977
34005
|
completed: "已完成",
|
|
@@ -34080,6 +34108,8 @@ window.__ModuleLoader__.load({
|
|
|
34080
34108
|
noModels: "No model usage yet",
|
|
34081
34109
|
scope: "This session and all subagents · Historical log totals",
|
|
34082
34110
|
note: "Includes independent requests, retries and compaction calls from this session and all descendants, without counting inherited history twice. Timing uses recorded timestamps only; missing values appear as —.",
|
|
34111
|
+
partialCoverage: "{count} subagent session(s) could not be read.",
|
|
34112
|
+
partialTotals: "Only readable usage is shown; missing usage is not treated as zero.",
|
|
34083
34113
|
failedLoad: "Could not read usage. The last successful result is preserved.",
|
|
34084
34114
|
retry: "Retry",
|
|
34085
34115
|
completed: "Completed",
|
|
@@ -34188,6 +34218,8 @@ window.__ModuleLoader__.load({
|
|
|
34188
34218
|
noModels: "暫無模型用量",
|
|
34189
34219
|
scope: "本會話及所有子代理 · 歷史日誌累計",
|
|
34190
34220
|
note: "統計包含本會話與全部後代的獨立請求、重試和壓縮呼叫,不重複計算繼承歷史。首 token 與速度僅使用已記錄的時間資訊,缺失時顯示 —。",
|
|
34221
|
+
partialCoverage: "有 {count} 個子會話暫不可讀取。",
|
|
34222
|
+
partialTotals: "目前僅顯示已讀取用量,未將缺失用量視為零。",
|
|
34191
34223
|
failedLoad: "用量讀取失敗,已保留上次結果。",
|
|
34192
34224
|
retry: "重新讀取",
|
|
34193
34225
|
completed: "已完成",
|
package/lib/index.js
CHANGED
|
@@ -1526,6 +1526,134 @@ async function resolveModelNames(modelRefs, llm) {
|
|
|
1526
1526
|
return names;
|
|
1527
1527
|
}
|
|
1528
1528
|
//#endregion
|
|
1529
|
+
//#region src/session-tree.ts
|
|
1530
|
+
function headerOf(item) {
|
|
1531
|
+
if (!item || typeof item !== "object") return null;
|
|
1532
|
+
const header = "header" in item ? item.header : item;
|
|
1533
|
+
return header && typeof header.id === "string" && header.id !== "" ? header : null;
|
|
1534
|
+
}
|
|
1535
|
+
function inheritedCount(value) {
|
|
1536
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
1537
|
+
}
|
|
1538
|
+
function throwIfCanceled(error, signal) {
|
|
1539
|
+
signal?.throwIfAborted();
|
|
1540
|
+
if (error && typeof error === "object" && "name" in error && error.name === "AbortError") throw error;
|
|
1541
|
+
}
|
|
1542
|
+
/** Never send raw exceptions to the client: they may contain paths or secrets. */
|
|
1543
|
+
function readIssue(sessionId, error) {
|
|
1544
|
+
const name = error && typeof error === "object" && "name" in error ? error.name : "";
|
|
1545
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : "";
|
|
1546
|
+
return {
|
|
1547
|
+
sessionId,
|
|
1548
|
+
reason: name === "SessionPersistenceCorruptionError" || name === "SessionFormatError" ? "corrupt" : name === "SessionPersistenceNotFoundError" || code === "ENOENT" ? "missing" : "unavailable"
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
async function readSession(persistence, id, header, live, signal) {
|
|
1552
|
+
signal?.throwIfAborted();
|
|
1553
|
+
if (typeof live?.snapshotEvents === "function" && typeof live.inheritedEventCount === "number") return {
|
|
1554
|
+
id,
|
|
1555
|
+
header,
|
|
1556
|
+
live,
|
|
1557
|
+
events: live.snapshotEvents(),
|
|
1558
|
+
inheritedCut: inheritedCount(live.inheritedEventCount)
|
|
1559
|
+
};
|
|
1560
|
+
if (typeof persistence.open === "function") {
|
|
1561
|
+
const options = signal === void 0 ? void 0 : { signal };
|
|
1562
|
+
const handle = await persistence.open(id, "read", options);
|
|
1563
|
+
try {
|
|
1564
|
+
const events = [];
|
|
1565
|
+
for (let offset = 0;;) {
|
|
1566
|
+
signal?.throwIfAborted();
|
|
1567
|
+
const page = await handle.read(offset, 500, options);
|
|
1568
|
+
if (!page.events.length) break;
|
|
1569
|
+
for (const event of page.events) events.push(event);
|
|
1570
|
+
offset += page.events.length;
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
id,
|
|
1574
|
+
header: header ?? handle.header,
|
|
1575
|
+
live,
|
|
1576
|
+
events,
|
|
1577
|
+
inheritedCut: inheritedCount(handle.inheritedEventCount)
|
|
1578
|
+
};
|
|
1579
|
+
} finally {
|
|
1580
|
+
await handle.close().catch(() => {});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof persistence.inspect === "function") {
|
|
1584
|
+
const inspection = await persistence.inspect(id, signal);
|
|
1585
|
+
signal?.throwIfAborted();
|
|
1586
|
+
return {
|
|
1587
|
+
id,
|
|
1588
|
+
header: header ?? inspection.meta,
|
|
1589
|
+
live,
|
|
1590
|
+
events: inspection.events,
|
|
1591
|
+
inheritedCut: inheritedCount(inspection.inheritedEventCount)
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
throw new Error("no readable persistence seam (need open or inspect)");
|
|
1595
|
+
}
|
|
1596
|
+
async function readUsageTree(persistence, sessions, rootId, signal) {
|
|
1597
|
+
signal?.throwIfAborted();
|
|
1598
|
+
const rows = await persistence.list(typeof persistence.open === "function" ? signal === void 0 ? void 0 : { signal } : signal);
|
|
1599
|
+
const headers = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const row of rows) {
|
|
1601
|
+
const header = headerOf(row);
|
|
1602
|
+
if (header) headers.set(header.id, header);
|
|
1603
|
+
}
|
|
1604
|
+
const live = new Map((sessions?.list() ?? []).map((handle) => [handle.id, handle]));
|
|
1605
|
+
const children = /* @__PURE__ */ new Map();
|
|
1606
|
+
const link = (id, parent) => {
|
|
1607
|
+
if (typeof parent !== "string" || parent === "" || parent === id) return;
|
|
1608
|
+
const bucket = children.get(parent) ?? /* @__PURE__ */ new Set();
|
|
1609
|
+
bucket.add(id);
|
|
1610
|
+
children.set(parent, bucket);
|
|
1611
|
+
};
|
|
1612
|
+
for (const header of headers.values()) link(header.id, header.parentSession);
|
|
1613
|
+
for (const handle of live.values()) link(handle.id, handle.header?.parentSession ?? handle.parentSession);
|
|
1614
|
+
const ids = [];
|
|
1615
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
1616
|
+
const queue = [rootId];
|
|
1617
|
+
for (let index = 0; index < queue.length; index++) for (const id of children.get(queue[index]) ?? []) {
|
|
1618
|
+
if (seen.has(id)) continue;
|
|
1619
|
+
seen.add(id);
|
|
1620
|
+
ids.push(id);
|
|
1621
|
+
queue.push(id);
|
|
1622
|
+
}
|
|
1623
|
+
const root = await readSession(persistence, rootId, headers.get(rootId), live.get(rootId), signal);
|
|
1624
|
+
const logs = new Array(ids.length);
|
|
1625
|
+
const issues = new Array(ids.length);
|
|
1626
|
+
let next = 0;
|
|
1627
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(4, ids.length) }, async () => {
|
|
1628
|
+
for (;;) {
|
|
1629
|
+
signal?.throwIfAborted();
|
|
1630
|
+
const index = next++;
|
|
1631
|
+
if (index >= ids.length) return;
|
|
1632
|
+
const id = ids[index];
|
|
1633
|
+
try {
|
|
1634
|
+
logs[index] = await readSession(persistence, id, headers.get(id), live.get(id), signal);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
throwIfCanceled(error, signal);
|
|
1637
|
+
issues[index] = readIssue(id, error);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}));
|
|
1641
|
+
for (const worker of workers) if (worker.status === "rejected") throw worker.reason;
|
|
1642
|
+
signal?.throwIfAborted();
|
|
1643
|
+
const descendants = logs.filter((log) => log !== void 0);
|
|
1644
|
+
const unreadableSessions = issues.filter((issue) => issue !== void 0);
|
|
1645
|
+
return {
|
|
1646
|
+
root,
|
|
1647
|
+
descendants,
|
|
1648
|
+
coverage: {
|
|
1649
|
+
complete: unreadableSessions.length === 0,
|
|
1650
|
+
totalSessions: ids.length + 1,
|
|
1651
|
+
readableSessions: descendants.length + 1,
|
|
1652
|
+
unreadableSessions
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
//#endregion
|
|
1529
1657
|
//#region src/context-window.ts
|
|
1530
1658
|
/** ASCII-ish characters per token (the host estimator's ratio). */
|
|
1531
1659
|
const ASCII_CHARS_PER_TOKEN = 4;
|
|
@@ -1995,7 +2123,7 @@ function mergeUsageCompositions(parts) {
|
|
|
1995
2123
|
}
|
|
1996
2124
|
//#endregion
|
|
1997
2125
|
//#region src/session-detail.ts
|
|
1998
|
-
const zero = () => ({
|
|
2126
|
+
const zero$1 = () => ({
|
|
1999
2127
|
uncachedInputTokens: 0,
|
|
2000
2128
|
outputTokens: 0,
|
|
2001
2129
|
cacheReadTokens: 0,
|
|
@@ -2082,43 +2210,6 @@ function usageOutputTokens(x) {
|
|
|
2082
2210
|
const value = (x?.usage ?? x)?.outputTokens;
|
|
2083
2211
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
2084
2212
|
}
|
|
2085
|
-
function headerOf$1(item) {
|
|
2086
|
-
const x = item;
|
|
2087
|
-
if (x?.header?.id && typeof x.header.id === "string") return x.header;
|
|
2088
|
-
return typeof x?.id === "string" && x.id ? x : null;
|
|
2089
|
-
}
|
|
2090
|
-
async function read(p, id, live) {
|
|
2091
|
-
if (live?.snapshotEvents && typeof live.inheritedEventCount === "number") return {
|
|
2092
|
-
events: live.snapshotEvents(),
|
|
2093
|
-
cut: Math.max(0, live.inheritedEventCount)
|
|
2094
|
-
};
|
|
2095
|
-
if (p.open) {
|
|
2096
|
-
const h = await p.open(id, "read");
|
|
2097
|
-
const out = [];
|
|
2098
|
-
try {
|
|
2099
|
-
for (let off = 0;;) {
|
|
2100
|
-
const page = await h.read(off, 500);
|
|
2101
|
-
if (!page.events.length) break;
|
|
2102
|
-
out.push(...page.events);
|
|
2103
|
-
off += page.events.length;
|
|
2104
|
-
}
|
|
2105
|
-
} finally {
|
|
2106
|
-
await h.close().catch(() => {});
|
|
2107
|
-
}
|
|
2108
|
-
return {
|
|
2109
|
-
events: out,
|
|
2110
|
-
cut: Math.max(0, Number(h.inheritedEventCount) || 0)
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
if (p.inspect) {
|
|
2114
|
-
const x = await p.inspect(id);
|
|
2115
|
-
return {
|
|
2116
|
-
events: x.events,
|
|
2117
|
-
cut: Math.max(0, Number(x.inheritedEventCount) || 0)
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
throw new Error("no readable persistence seam (need open or inspect)");
|
|
2121
|
-
}
|
|
2122
2213
|
function nameOf(h, id) {
|
|
2123
2214
|
const x = h;
|
|
2124
2215
|
if (typeof x?.name === "string" && x.name) return x.name;
|
|
@@ -2126,10 +2217,10 @@ function nameOf(h, id) {
|
|
|
2126
2217
|
if (typeof x?.cwd === "string" && x.cwd) return x.cwd.split(/[\\/]/).filter(Boolean).pop() || id;
|
|
2127
2218
|
return id;
|
|
2128
2219
|
}
|
|
2129
|
-
|
|
2130
|
-
const { events, cut } =
|
|
2220
|
+
function one(log) {
|
|
2221
|
+
const { id, header, live, events, inheritedCut: cut } = log;
|
|
2131
2222
|
const fold = new UsageFold();
|
|
2132
|
-
const totals = zero();
|
|
2223
|
+
const totals = zero$1();
|
|
2133
2224
|
const records = [];
|
|
2134
2225
|
const byKey = /* @__PURE__ */ new Map();
|
|
2135
2226
|
let route = live?.requestContext?.() ?? {};
|
|
@@ -2205,14 +2296,14 @@ async function one(p, id, header, live) {
|
|
|
2205
2296
|
provider: r.provider || route.provider || "",
|
|
2206
2297
|
model: r.model || route.model || "",
|
|
2207
2298
|
status: "running",
|
|
2208
|
-
totals: zero(),
|
|
2299
|
+
totals: zero$1(),
|
|
2209
2300
|
firstTokenMs: null,
|
|
2210
2301
|
decodeMs: null,
|
|
2211
2302
|
tokensPerSecond: null,
|
|
2212
2303
|
turnName: "",
|
|
2213
2304
|
effort,
|
|
2214
2305
|
_attempt: ev.type === "llm/retry-started" ? 1 : 0,
|
|
2215
|
-
_lastUsage: zero()
|
|
2306
|
+
_lastUsage: zero$1()
|
|
2216
2307
|
};
|
|
2217
2308
|
records.push(rec);
|
|
2218
2309
|
byKey.set(key, rec);
|
|
@@ -2232,14 +2323,14 @@ async function one(p, id, header, live) {
|
|
|
2232
2323
|
provider: r.provider || route.provider || "",
|
|
2233
2324
|
model: r.model || route.model || "",
|
|
2234
2325
|
status: "running",
|
|
2235
|
-
totals: zero(),
|
|
2326
|
+
totals: zero$1(),
|
|
2236
2327
|
firstTokenMs: null,
|
|
2237
2328
|
decodeMs: null,
|
|
2238
2329
|
tokensPerSecond: null,
|
|
2239
2330
|
turnName: "",
|
|
2240
2331
|
effort,
|
|
2241
2332
|
_attempt: 0,
|
|
2242
|
-
_lastUsage: zero()
|
|
2333
|
+
_lastUsage: zero$1()
|
|
2243
2334
|
};
|
|
2244
2335
|
records.push(rec);
|
|
2245
2336
|
byKey.set(key, rec);
|
|
@@ -2326,32 +2417,16 @@ async function one(p, id, header, live) {
|
|
|
2326
2417
|
};
|
|
2327
2418
|
}
|
|
2328
2419
|
async function buildSessionUsageDetail(persistence, sessions, rootId, ctx) {
|
|
2329
|
-
const
|
|
2330
|
-
const
|
|
2331
|
-
const
|
|
2332
|
-
for (const h of headers) if (h.parentSession) children.set(h.parentSession, [...children.get(h.parentSession) ?? [], h.id]);
|
|
2333
|
-
for (const h of live.values()) {
|
|
2334
|
-
const p = h.header?.parentSession ?? h.parentSession;
|
|
2335
|
-
if (p) children.set(p, [...(children.get(p) ?? []).filter((x) => x !== h.id), h.id]);
|
|
2336
|
-
}
|
|
2337
|
-
const ids = [];
|
|
2338
|
-
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
2339
|
-
const q = [rootId];
|
|
2340
|
-
while (q.length) for (const c of children.get(q.shift()) ?? []) if (!seen.has(c)) {
|
|
2341
|
-
seen.add(c);
|
|
2342
|
-
ids.push(c);
|
|
2343
|
-
q.push(c);
|
|
2344
|
-
}
|
|
2345
|
-
const all = [rootId, ...ids];
|
|
2346
|
-
const parts = await Promise.all(all.map((id) => one(persistence, id, headers.find((h) => h.id === id), live.get(id))));
|
|
2347
|
-
const totals = zero();
|
|
2420
|
+
const { root, descendants, coverage } = await readUsageTree(persistence, sessions, rootId);
|
|
2421
|
+
const parts = [root, ...descendants].map(one);
|
|
2422
|
+
const totals = zero$1();
|
|
2348
2423
|
const models = /* @__PURE__ */ new Map();
|
|
2349
2424
|
const records = parts.flatMap((x) => x.records);
|
|
2350
2425
|
for (const p of parts) add$1(totals, p.totals);
|
|
2351
2426
|
for (const r of records) {
|
|
2352
2427
|
const key = r.model || "(unknown)";
|
|
2353
2428
|
const m = models.get(key) ?? {
|
|
2354
|
-
totals: zero(),
|
|
2429
|
+
totals: zero$1(),
|
|
2355
2430
|
requests: 0,
|
|
2356
2431
|
providers: /* @__PURE__ */ new Set()
|
|
2357
2432
|
};
|
|
@@ -2405,6 +2480,7 @@ async function buildSessionUsageDetail(persistence, sessions, rootId, ctx) {
|
|
|
2405
2480
|
const firsts = records.filter((x) => x.status === "completed").map((x) => x.firstTokenMs).filter((x) => x !== null);
|
|
2406
2481
|
return {
|
|
2407
2482
|
sessionId: rootId,
|
|
2483
|
+
coverage,
|
|
2408
2484
|
totals,
|
|
2409
2485
|
requests: parts.reduce((n, p) => n + p.requests, 0),
|
|
2410
2486
|
averageFirstTokenMs: firsts.length ? firsts.reduce((a, b) => a + b, 0) / firsts.length : null,
|
|
@@ -2811,82 +2887,25 @@ function buildUsageRoute(ctx, deps) {
|
|
|
2811
2887
|
}
|
|
2812
2888
|
//#endregion
|
|
2813
2889
|
//#region src/session-usage.ts
|
|
2814
|
-
const
|
|
2890
|
+
const zero = () => ({
|
|
2815
2891
|
uncachedInputTokens: 0,
|
|
2816
2892
|
outputTokens: 0,
|
|
2817
2893
|
cacheReadTokens: 0,
|
|
2818
2894
|
cacheWriteTokens: 0
|
|
2819
|
-
};
|
|
2895
|
+
});
|
|
2820
2896
|
function add(into, from) {
|
|
2821
2897
|
into.uncachedInputTokens += from.uncachedInputTokens;
|
|
2822
2898
|
into.outputTokens += from.outputTokens;
|
|
2823
2899
|
into.cacheReadTokens += from.cacheReadTokens;
|
|
2824
2900
|
into.cacheWriteTokens += from.cacheWriteTokens;
|
|
2825
2901
|
}
|
|
2826
|
-
|
|
2827
|
-
* and a live session's numbers move at step granularity, not per frame. */
|
|
2828
|
-
const CACHE_TTL_MS = 4e3;
|
|
2829
|
-
/** Absolute ceiling on cached trees, so a long-lived host with many sessions
|
|
2830
|
-
* cannot grow this map without bound. */
|
|
2831
|
-
const CACHE_MAX = 64;
|
|
2832
|
-
/** Normalize a `list()` row (alpha snapshots or rc.1 header rows). */
|
|
2833
|
-
function headerOf(item) {
|
|
2834
|
-
const snap = item;
|
|
2835
|
-
if (snap !== null && typeof snap === "object" && "header" in snap && snap.header !== null && typeof snap.header === "object" && typeof snap.header.id === "string") return snap.header;
|
|
2836
|
-
const row = item;
|
|
2837
|
-
return typeof row?.id === "string" && row.id !== "" ? row : null;
|
|
2838
|
-
}
|
|
2839
|
-
function inheritedCount(cut) {
|
|
2840
|
-
return typeof cut === "number" && Number.isSafeInteger(cut) && cut >= 0 ? cut : 0;
|
|
2841
|
-
}
|
|
2842
|
-
/** Read one stored session's events through whichever persistence seam the
|
|
2843
|
-
* host line exposes (alpha open/read/close wins; rc.1 inspect is the tail). */
|
|
2844
|
-
async function readEvents(persistence, id, signal) {
|
|
2845
|
-
if (typeof persistence.open === "function") {
|
|
2846
|
-
const handle = await persistence.open(id, "read", signal === void 0 ? void 0 : { signal });
|
|
2847
|
-
try {
|
|
2848
|
-
const inheritedCut = inheritedCount(handle.inheritedEventCount);
|
|
2849
|
-
const collected = [];
|
|
2850
|
-
let offset = 0;
|
|
2851
|
-
const PAGE = 500;
|
|
2852
|
-
for (;;) {
|
|
2853
|
-
signal?.throwIfAborted();
|
|
2854
|
-
const slice = await handle.read(offset, PAGE, signal === void 0 ? void 0 : { signal });
|
|
2855
|
-
const page = slice && slice.events ? slice.events : [];
|
|
2856
|
-
if (page.length === 0) break;
|
|
2857
|
-
for (const ev of page) collected.push(ev);
|
|
2858
|
-
offset += page.length;
|
|
2859
|
-
}
|
|
2860
|
-
return {
|
|
2861
|
-
events: collected,
|
|
2862
|
-
inheritedCut
|
|
2863
|
-
};
|
|
2864
|
-
} finally {
|
|
2865
|
-
await handle.close().catch(() => {});
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
if (typeof persistence.inspect === "function") {
|
|
2869
|
-
const inspection = await persistence.inspect(id, signal);
|
|
2870
|
-
return {
|
|
2871
|
-
events: inspection.events,
|
|
2872
|
-
inheritedCut: inheritedCount(inspection.inheritedEventCount)
|
|
2873
|
-
};
|
|
2874
|
-
}
|
|
2875
|
-
throw new Error("no readable persistence seam (need open or inspect)");
|
|
2876
|
-
}
|
|
2877
|
-
/** Sum one session's own usage (its main agent) from its log. */
|
|
2878
|
-
async function sumOneSession(persistence, id, signal, live) {
|
|
2879
|
-
signal?.throwIfAborted();
|
|
2880
|
-
const { events, inheritedCut } = typeof live?.snapshotEvents === "function" && typeof live.inheritedEventCount === "number" ? {
|
|
2881
|
-
events: live.snapshotEvents(),
|
|
2882
|
-
inheritedCut: inheritedCount(live.inheritedEventCount)
|
|
2883
|
-
} : await readEvents(persistence, id, signal);
|
|
2902
|
+
function sumOneSession(log) {
|
|
2884
2903
|
const fold = new UsageFold();
|
|
2885
|
-
const totals =
|
|
2886
|
-
const compaction =
|
|
2887
|
-
for (const
|
|
2888
|
-
if (
|
|
2889
|
-
const sample = fold.fold(
|
|
2904
|
+
const totals = zero();
|
|
2905
|
+
const compaction = zero();
|
|
2906
|
+
for (const event of log.events) {
|
|
2907
|
+
if (event.seq < log.inheritedCut) continue;
|
|
2908
|
+
const sample = fold.fold(event);
|
|
2890
2909
|
if (sample === null || sample.turn) continue;
|
|
2891
2910
|
const buckets = {
|
|
2892
2911
|
uncachedInputTokens: sample.inputTokens,
|
|
@@ -2895,77 +2914,47 @@ async function sumOneSession(persistence, id, signal, live) {
|
|
|
2895
2914
|
cacheWriteTokens: sample.cacheWriteTokens
|
|
2896
2915
|
};
|
|
2897
2916
|
add(totals, buckets);
|
|
2898
|
-
if (
|
|
2917
|
+
if (event.type === "compaction/summary") add(compaction, buckets);
|
|
2899
2918
|
}
|
|
2900
2919
|
return {
|
|
2901
2920
|
totals,
|
|
2902
2921
|
compaction
|
|
2903
2922
|
};
|
|
2904
2923
|
}
|
|
2924
|
+
const CACHE_TTL_MS = 4e3;
|
|
2925
|
+
const CACHE_MAX = 64;
|
|
2905
2926
|
var SessionUsageService = class {
|
|
2906
|
-
/** Short-lived tree reads; any live event can invalidate tree membership. */
|
|
2907
2927
|
cache = /* @__PURE__ */ new Map();
|
|
2908
|
-
/** Any event can introduce a new descendant while an async read is running. */
|
|
2909
2928
|
generation = 0;
|
|
2910
|
-
/**
|
|
2911
|
-
* of ANY cached root, so membership-only invalidation is not safe. */
|
|
2929
|
+
/** New events can introduce descendants of any cached root. */
|
|
2912
2930
|
invalidate(_sessionId) {
|
|
2913
2931
|
this.generation++;
|
|
2914
2932
|
this.cache.clear();
|
|
2915
2933
|
}
|
|
2916
|
-
/**
|
|
2917
|
-
*
|
|
2918
|
-
*
|
|
2919
|
-
* callers can retain their last good display while surfacing the error. */
|
|
2934
|
+
/** A bad descendant leaves the readable totals available with coverage.
|
|
2935
|
+
* Never cache incomplete results: a repaired log must recover on the next read.
|
|
2936
|
+
* Root, catalog and cancellation failures still reject without publishing zero. */
|
|
2920
2937
|
async tree(persistence, sessions, rootId, signal) {
|
|
2921
2938
|
signal?.throwIfAborted();
|
|
2922
2939
|
const hit = this.cache.get(rootId);
|
|
2923
2940
|
if (hit !== void 0 && Date.now() - hit.at < CACHE_TTL_MS) return hit.value;
|
|
2924
2941
|
const generation = this.generation;
|
|
2925
|
-
const
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
const bucket = children.get(parent);
|
|
2931
|
-
if (bucket === void 0) children.set(parent, [header.id]);
|
|
2932
|
-
else bucket.push(header.id);
|
|
2933
|
-
}
|
|
2934
|
-
const live = new Map((sessions?.list() ?? []).map((handle) => [handle.id, handle]));
|
|
2935
|
-
for (const handle of live.values()) {
|
|
2936
|
-
const parent = handle.header?.parentSession ?? handle.parentSession;
|
|
2937
|
-
if (typeof parent !== "string" || parent === "" || parent === handle.id) continue;
|
|
2938
|
-
const bucket = children.get(parent);
|
|
2939
|
-
if (bucket === void 0) children.set(parent, [handle.id]);
|
|
2940
|
-
else if (!bucket.includes(handle.id)) bucket.push(handle.id);
|
|
2941
|
-
}
|
|
2942
|
-
const descendants = [];
|
|
2943
|
-
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
2944
|
-
const queue = [rootId];
|
|
2945
|
-
while (queue.length > 0) {
|
|
2946
|
-
const current = queue.shift();
|
|
2947
|
-
for (const child of children.get(current) ?? []) {
|
|
2948
|
-
if (seen.has(child)) continue;
|
|
2949
|
-
seen.add(child);
|
|
2950
|
-
descendants.push(child);
|
|
2951
|
-
queue.push(child);
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
const root = await sumOneSession(persistence, rootId, signal, live.get(rootId));
|
|
2955
|
-
const self = root.totals;
|
|
2956
|
-
const subagents = { ...ZERO };
|
|
2957
|
-
for (const id of descendants) add(subagents, (await sumOneSession(persistence, id, signal, live.get(id))).totals);
|
|
2958
|
-
const total = { ...self };
|
|
2942
|
+
const { root, descendants, coverage } = await readUsageTree(persistence, sessions, rootId, signal);
|
|
2943
|
+
const own = sumOneSession(root);
|
|
2944
|
+
const subagents = zero();
|
|
2945
|
+
for (const log of descendants) add(subagents, sumOneSession(log).totals);
|
|
2946
|
+
const total = { ...own.totals };
|
|
2959
2947
|
add(total, subagents);
|
|
2960
2948
|
const value = {
|
|
2961
|
-
self,
|
|
2962
|
-
selfCompaction:
|
|
2949
|
+
self: own.totals,
|
|
2950
|
+
selfCompaction: own.compaction,
|
|
2963
2951
|
subagents,
|
|
2964
2952
|
total,
|
|
2965
|
-
subagentSessions: descendants.length
|
|
2953
|
+
subagentSessions: descendants.length,
|
|
2954
|
+
coverage
|
|
2966
2955
|
};
|
|
2967
2956
|
signal?.throwIfAborted();
|
|
2968
|
-
if (generation === this.generation) {
|
|
2957
|
+
if (coverage.complete && generation === this.generation) {
|
|
2969
2958
|
if (this.cache.size >= CACHE_MAX) this.cache.clear();
|
|
2970
2959
|
this.cache.set(rootId, {
|
|
2971
2960
|
at: Date.now(),
|
|
@@ -2974,9 +2963,6 @@ var SessionUsageService = class {
|
|
|
2974
2963
|
}
|
|
2975
2964
|
return value;
|
|
2976
2965
|
}
|
|
2977
|
-
async listHeaders(persistence, signal) {
|
|
2978
|
-
return (typeof persistence.open === "function" ? await persistence.list(signal === void 0 ? void 0 : { signal }) : await persistence.list(signal)).map(headerOf).filter((h) => h !== null);
|
|
2979
|
-
}
|
|
2980
2966
|
};
|
|
2981
2967
|
//#endregion
|
|
2982
2968
|
//#region src/index.ts
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** The locale namespace this plugin owns. */
|
|
8
8
|
export declare const LOCALE_NS = "tokenTotal";
|
|
9
9
|
/** The typed translator key union for the panel's locale seat. */
|
|
10
|
-
export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'requestsFailed' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dow.mon' | 'dow.tue' | 'dow.wed' | 'dow.thu' | 'dow.fri' | 'dow.sat' | 'dow.sun' | 'month.jan' | 'month.feb' | 'month.mar' | 'month.apr' | 'month.may' | 'month.jun' | 'month.jul' | 'month.aug' | 'month.sep' | 'month.oct' | 'month.nov' | 'month.dec' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
|
|
10
|
+
export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'requestsFailed' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dow.mon' | 'dow.tue' | 'dow.wed' | 'dow.thu' | 'dow.fri' | 'dow.sat' | 'dow.sun' | 'month.jan' | 'month.feb' | 'month.mar' | 'month.apr' | 'month.may' | 'month.jun' | 'month.jul' | 'month.aug' | 'month.sep' | 'month.oct' | 'month.nov' | 'month.dec' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.partial' | 'stats.incomplete' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
|
|
11
11
|
export declare const en: Record<TokenTotalKey, string>;
|
|
12
12
|
export declare const zh: Record<TokenTotalKey, string>;
|
|
13
13
|
export declare const zhTW: Record<TokenTotalKey, string>;
|