opencode-visual-cache 1.2.9 → 1.2.10
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 +2 -0
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +28 -25
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +26 -15
package/README.md
CHANGED
|
@@ -134,6 +134,8 @@ npm install -g opencode-visual-cache@latest
|
|
|
134
134
|
|
|
135
135
|
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
136
136
|
|
|
137
|
+
> **关于 Token 分布数值**:分布面板中"总计"为最后一次 API 调用的精确 token 数,"系统提示"/"用户"等分项为字符级 BPE 估算值。分项之和通常小于总计,差值主要来自 OpenCode 运行时注入的系统提示组成部分,包括环境信息、Skill 目录、工具 Schema 定义等(详见 [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts)、[`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts))。这些内容不在 agent 配置的 `prompt` 字段中,因此插件无法估算,属于预期行为。
|
|
138
|
+
|
|
137
139
|
---
|
|
138
140
|
|
|
139
141
|
## 5. 更新
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.2.10";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.2.
|
|
2
|
+
export const PLUGIN_VERSION = "1.2.10";
|
package/dist/index.js
CHANGED
|
@@ -216,7 +216,7 @@ function fmt(n) {
|
|
|
216
216
|
if (n >= 1_000_000)
|
|
217
217
|
return (n / 1_000_000).toFixed(1) + "M";
|
|
218
218
|
if (n >= 10_000)
|
|
219
|
-
return
|
|
219
|
+
return (n / 1_000).toFixed(1) + "K";
|
|
220
220
|
return n.toLocaleString("en-US");
|
|
221
221
|
}
|
|
222
222
|
function num(v) {
|
|
@@ -256,16 +256,19 @@ function estimateTokens(text) {
|
|
|
256
256
|
else
|
|
257
257
|
ascii++;
|
|
258
258
|
}
|
|
259
|
-
//
|
|
260
|
-
// token
|
|
261
|
-
//
|
|
259
|
+
// Real BPE tokenizers (cl100k_base, o200k_base) average ~3.5-4.0
|
|
260
|
+
// ASCII chars/token for both JSON and source code — close to prose.
|
|
261
|
+
// The old 2.0 / 2.5 ratios matched minified-JS extremes, not typical
|
|
262
|
+
// payloads, and systematically over-estimated token counts.
|
|
262
263
|
const trimmed = text.trimStart();
|
|
263
|
-
|
|
264
|
+
// Strip markdown code-fence prefix so that ```json … is detected as JSON
|
|
265
|
+
const strippedFence = trimmed.replace(/^\x60{3}\w*\s*\n?/, "");
|
|
266
|
+
const jsonLike = (strippedFence.startsWith("{") || strippedFence.startsWith("["))
|
|
264
267
|
&& /"[^"]+"\s*:/.test(text);
|
|
265
268
|
const codeLike = !jsonLike
|
|
266
269
|
&& /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text);
|
|
267
|
-
const asciiPerToken = jsonLike ?
|
|
268
|
-
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.
|
|
270
|
+
const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4;
|
|
271
|
+
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
|
|
269
272
|
}
|
|
270
273
|
const CURRENCIES = {
|
|
271
274
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
@@ -376,6 +379,7 @@ function TokenCachePanel(props) {
|
|
|
376
379
|
const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : "";
|
|
377
380
|
if (sysPrompt)
|
|
378
381
|
dist.system = estimateTokens(sysPrompt);
|
|
382
|
+
let lastAssMsg;
|
|
379
383
|
for (const msg of msgs) {
|
|
380
384
|
if (msg.role === "user") {
|
|
381
385
|
const um = msg;
|
|
@@ -409,14 +413,9 @@ function TokenCachePanel(props) {
|
|
|
409
413
|
const tp = p;
|
|
410
414
|
let rawInput = "";
|
|
411
415
|
try {
|
|
412
|
-
rawInput = tp.state.raw ?? JSON.stringify(tp.state.input);
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
try {
|
|
416
|
-
rawInput = JSON.stringify(tp.state);
|
|
417
|
-
}
|
|
418
|
-
catch { }
|
|
416
|
+
rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "");
|
|
419
417
|
}
|
|
418
|
+
catch { }
|
|
420
419
|
if (rawInput)
|
|
421
420
|
dist.toolCall += estimateTokens(rawInput);
|
|
422
421
|
if (tp.state.status === "completed") {
|
|
@@ -436,19 +435,23 @@ function TokenCachePanel(props) {
|
|
|
436
435
|
const sub = p;
|
|
437
436
|
dist.agent += estimateTokens(sub.prompt || sub.description || "");
|
|
438
437
|
}
|
|
439
|
-
else if (p.type === "step-finish") {
|
|
440
|
-
const sf = p;
|
|
441
|
-
dist.apiInput += sf.tokens?.input ?? 0;
|
|
442
|
-
dist.apiOutput += sf.tokens?.output ?? 0;
|
|
443
|
-
}
|
|
444
438
|
}
|
|
445
439
|
}
|
|
446
440
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
441
|
+
// 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
|
|
442
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
443
|
+
if (msgs[i].role !== "assistant")
|
|
444
|
+
continue;
|
|
445
|
+
const t = msgs[i].tokens;
|
|
446
|
+
if (t && (t.input > 0 || (t.cache?.read ?? 0) > 0)) {
|
|
447
|
+
lastAssMsg = msgs[i];
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 取最后一条有数据消息的总输入(含缓存读)作为当前 context 大小
|
|
452
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read);
|
|
453
|
+
dist.apiOutput = num(lastAssMsg?.tokens?.output);
|
|
454
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
|
|
452
455
|
}
|
|
453
456
|
catch { }
|
|
454
457
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
@@ -626,7 +629,7 @@ function TokenCachePanel(props) {
|
|
|
626
629
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
627
630
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
628
631
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
629
|
-
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: T.title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: pal().muted }, children: [" (v", PLUGIN_VERSION, ")"] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] })] }), _jsx(Show, { when: open(), children: _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: T.noData })] })] }), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [T.hit, " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: data().hasTrendData, children: _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(T.totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secDetail }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + T.secDetail)) })] }), _jsxs(Show, { when: detailOpen(), children: [_jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(data().read), T.tok) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(data().write), T.tok) }) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(data().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(data().output), T.tok) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: T.saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + T.secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + T.cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + T.writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + T.distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distSys, fmt(data().dist.system), T.tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distUser, fmt(data().dist.user), T.tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distAgent, fmt(data().dist.agent), T.tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distTool, fmt(data().dist.toolCall), T.tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distRes, fmt(data().dist.toolResult), T.tok) }) }), _jsx("text", { fg: pal().text, children: justify(T.distTotal, fmt(data().dist.
|
|
632
|
+
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: T.title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: pal().muted }, children: [" (v", PLUGIN_VERSION, ")"] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] })] }), _jsx(Show, { when: open(), children: _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: T.noData })] })] }), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [T.hit, " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: data().hasTrendData, children: _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(T.totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secDetail }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + T.secDetail)) })] }), _jsxs(Show, { when: detailOpen(), children: [_jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(data().read), T.tok) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(data().write), T.tok) }) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(data().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(data().output), T.tok) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: T.saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + T.secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + T.cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + T.writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + T.distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distSys, fmt(data().dist.system), T.tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distUser, fmt(data().dist.user), T.tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distAgent, fmt(data().dist.agent), T.tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distTool, fmt(data().dist.toolCall), T.tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distRes, fmt(data().dist.toolResult), T.tok) }) }), _jsx("text", { fg: pal().text, children: justify(T.distTotal, fmt(data().dist.apiInput), T.tok) })] })] }) })] }) })] }));
|
|
630
633
|
}
|
|
631
634
|
// ---------------------------------------------------------------------------
|
|
632
635
|
// Plugin entry
|
package/package.json
CHANGED
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.2.
|
|
2
|
+
export const PLUGIN_VERSION="1.2.10";
|
package/src/index.tsx
CHANGED
|
@@ -241,7 +241,7 @@ function progressBar(percent: number, width: number): string {
|
|
|
241
241
|
|
|
242
242
|
function fmt(n: number): string {
|
|
243
243
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
|
|
244
|
-
if (n >= 10_000) return
|
|
244
|
+
if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
|
|
245
245
|
return n.toLocaleString("en-US")
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -277,17 +277,20 @@ function estimateTokens(text: string): number {
|
|
|
277
277
|
else ascii++
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
//
|
|
281
|
-
// token
|
|
282
|
-
//
|
|
280
|
+
// Real BPE tokenizers (cl100k_base, o200k_base) average ~3.5-4.0
|
|
281
|
+
// ASCII chars/token for both JSON and source code — close to prose.
|
|
282
|
+
// The old 2.0 / 2.5 ratios matched minified-JS extremes, not typical
|
|
283
|
+
// payloads, and systematically over-estimated token counts.
|
|
283
284
|
const trimmed = text.trimStart()
|
|
284
|
-
|
|
285
|
+
// Strip markdown code-fence prefix so that ```json … is detected as JSON
|
|
286
|
+
const strippedFence = trimmed.replace(/^\x60{3}\w*\s*\n?/, "")
|
|
287
|
+
const jsonLike = (strippedFence.startsWith("{") || strippedFence.startsWith("["))
|
|
285
288
|
&& /"[^"]+"\s*:/.test(text)
|
|
286
289
|
const codeLike = !jsonLike
|
|
287
290
|
&& /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text)
|
|
288
291
|
|
|
289
|
-
const asciiPerToken = jsonLike ?
|
|
290
|
-
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.
|
|
292
|
+
const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4
|
|
293
|
+
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0))
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
interface TokenDist {
|
|
@@ -436,6 +439,7 @@ function TokenCachePanel(props: {
|
|
|
436
439
|
const agentCfg = agents?.[agentName] as Record<string, unknown> | undefined
|
|
437
440
|
const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : ""
|
|
438
441
|
if (sysPrompt) dist.system = estimateTokens(sysPrompt)
|
|
442
|
+
let lastAssMsg: AssistantMessage | undefined
|
|
439
443
|
for (const msg of msgs) {
|
|
440
444
|
if (msg.role === "user") {
|
|
441
445
|
const um = msg as UserMessage; if (um.system) dist.system += estimateTokens(um.system)
|
|
@@ -445,24 +449,31 @@ function TokenCachePanel(props: {
|
|
|
445
449
|
else if (p.type === "file") { const fp = p as any; if (fp.source?.text?.value) dist.user += estimateTokens(fp.source.text.value) }
|
|
446
450
|
}
|
|
447
451
|
} else if (msg.role === "assistant") {
|
|
448
|
-
const am = msg as AssistantMessage
|
|
452
|
+
const am = msg as AssistantMessage
|
|
453
|
+
dist.output += num(am.tokens?.output)
|
|
449
454
|
let parts: readonly Part[] = []; try { parts = props.api.state.part(msg.id) } catch {}
|
|
450
455
|
for (const p of parts) {
|
|
451
456
|
if (p.type === "tool") {
|
|
452
457
|
const tp = p as any; let rawInput = ""
|
|
453
|
-
try { rawInput = tp.state.raw ??
|
|
458
|
+
try { rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "") } catch {}
|
|
454
459
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
455
460
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
456
461
|
else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
|
|
457
462
|
} else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
|
|
458
463
|
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
459
|
-
else if (p.type === "step-finish") { const sf = p as any; dist.apiInput += sf.tokens?.input ?? 0; dist.apiOutput += sf.tokens?.output ?? 0 }
|
|
460
464
|
}
|
|
461
465
|
}
|
|
462
466
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
467
|
+
// 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
|
|
468
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
469
|
+
if (msgs[i].role !== "assistant") continue
|
|
470
|
+
const t = (msgs[i] as AssistantMessage).tokens
|
|
471
|
+
if (t && (t.input > 0 || (t.cache?.read ?? 0) > 0)) { lastAssMsg = msgs[i] as AssistantMessage; break }
|
|
472
|
+
}
|
|
473
|
+
// 取最后一条有数据消息的总输入(含缓存读)作为当前 context 大小
|
|
474
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read)
|
|
475
|
+
dist.apiOutput = num(lastAssMsg?.tokens?.output)
|
|
476
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
|
|
466
477
|
} catch {}
|
|
467
478
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
468
479
|
return { finalDist, finalHasDist }
|
|
@@ -479,7 +490,7 @@ function TokenCachePanel(props: {
|
|
|
479
490
|
|
|
480
491
|
const data = createMemo(() => {
|
|
481
492
|
return dataSignal()
|
|
482
|
-
})
|
|
493
|
+
})
|
|
483
494
|
|
|
484
495
|
// Persist the last valid distribution so that data() can fall back
|
|
485
496
|
// to it while api.state.part() is re-hydrating after a view switch.
|
|
@@ -819,7 +830,7 @@ function TokenCachePanel(props: {
|
|
|
819
830
|
</text>
|
|
820
831
|
</Show>
|
|
821
832
|
<text fg={pal().text}>
|
|
822
|
-
{justify(T.distTotal, fmt(data().dist.
|
|
833
|
+
{justify(T.distTotal, fmt(data().dist.apiInput), T.tok)}
|
|
823
834
|
</text>
|
|
824
835
|
</Show>
|
|
825
836
|
</Show>
|