opencode-tokenwatch 0.2.0 → 0.3.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.en.md +19 -11
- package/README.md +20 -12
- package/dist/commands.jsx +69 -4
- package/dist/formatter.d.ts +30 -0
- package/dist/generate-usage-html.js +210 -71
- package/dist/i18n.js +2 -4
- package/dist/perf-tracker.d.ts +6 -0
- package/dist/perf-tracker.js +122 -13
- package/dist/queries.d.ts +3 -1
- package/dist/queries.js +65 -6
- package/dist/sidebar.d.ts +2 -2
- package/dist/sidebar.jsx +321 -92
- package/dist/stats-store.d.ts +23 -0
- package/dist/stats-store.js +258 -0
- package/dist/tui.jsx +84 -27
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -8,17 +8,17 @@ Real-time token usage, cache analytics & performance dashboard plugin for OpenCo
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Sidebar panel** — Session-level and per-model real-time stats (requests, tokens, cache,
|
|
12
|
-
- **Cache hit rate** — Inline progress bars with
|
|
13
|
-
- **Performance metrics** —
|
|
14
|
-
- **Token distribution** —
|
|
15
|
-
- **
|
|
11
|
+
- **Sidebar panel** — Session-level and per-model real-time stats (requests, tokens, cache, cost)
|
|
12
|
+
- **Cache hit rate** — Inline color progress bars with trend indicators (↑/↓) and global weighted hit rate
|
|
13
|
+
- **Performance metrics** — TTFT / TPS / End-to-end latency + P50/P95/P99 latency percentiles
|
|
14
|
+
- **Token distribution** — 5-bucket role breakdown (system / user / toolCall / toolResult / output + other fallback)
|
|
15
|
+
- **Error rate tracking** — Detects failed requests (empty token response) and computes real-time error rate
|
|
16
|
+
- **Cost display** — Per-model cost (requires provider billing data)
|
|
16
17
|
- **`/usage` command** — HTML Report → JSON Export → Text Report → Settings
|
|
17
|
-
- **HTML report** — Interactive ECharts dashboard, auto-opened in browser
|
|
18
|
+
- **HTML report** — Interactive ECharts dashboard: token distribution, performance percentiles, TPS horizontal ranking, error rate analysis — auto-opened in browser
|
|
19
|
+
- **Persistent stats** — Performance metrics (TPS/TTFT/latency) written to a dedicated JSON file that accumulates forever, unaffected by log rotation
|
|
18
20
|
- **Multi-level collapse** — Panel, models, and sub-blocks collapsible with persisted state
|
|
19
21
|
- **Language switching** — Auto-detect or manually switch between Chinese and English
|
|
20
|
-
- **Per-request tracking** — TTFT/TPS/latency logged to JSONL for report analysis
|
|
21
|
-
- **Adaptive coloring** — Colors auto-derived from theme
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -44,8 +44,8 @@ Add to `opencode.json` or `opencode.jsonc`:
|
|
|
44
44
|
"opencode-tokenwatch": {
|
|
45
45
|
"sidebar": {
|
|
46
46
|
"showPerformance": true,
|
|
47
|
-
"showPricing":
|
|
48
|
-
"showTokenDistribution":
|
|
47
|
+
"showPricing": true,
|
|
48
|
+
"showTokenDistribution": true,
|
|
49
49
|
"showTrend": true
|
|
50
50
|
},
|
|
51
51
|
"language": "auto"
|
|
@@ -57,7 +57,7 @@ Add to `opencode.json` or `opencode.jsonc`:
|
|
|
57
57
|
| Option | Type | Default | Description |
|
|
58
58
|
|--------|------|---------|-------------|
|
|
59
59
|
| `sidebar.showPerformance` | boolean | `true` | Show TTFT/TPS/latency |
|
|
60
|
-
| `sidebar.showPricing` | boolean | `true` | Show
|
|
60
|
+
| `sidebar.showPricing` | boolean | `true` | Show request cost |
|
|
61
61
|
| `sidebar.showTokenDistribution` | boolean | `true` | Show token distribution |
|
|
62
62
|
| `sidebar.showTrend` | boolean | `true` | Show trend indicator |
|
|
63
63
|
| `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | UI language |
|
|
@@ -73,6 +73,14 @@ In OpenCode TUI, run `/usage`:
|
|
|
73
73
|
- **Text Report** — Exports Markdown report to `~/.opencode/reports/`
|
|
74
74
|
- **Settings** — Toggle sidebar blocks, switch language
|
|
75
75
|
|
|
76
|
+
## Data Files
|
|
77
|
+
|
|
78
|
+
| File | Path | Description |
|
|
79
|
+
|------|------|-------------|
|
|
80
|
+
| JSONL log | `~/.opencode/tokenwatch.jsonl` | Raw per-request log, auto-rotated at 5 MB |
|
|
81
|
+
| Aggregated stats | `~/.opencode/tokenwatch-stats.json` | Persistent performance stats, accumulates forever |
|
|
82
|
+
| Report output | `~/.opencode/reports/` | HTML / JSON / Markdown reports |
|
|
83
|
+
|
|
76
84
|
## Requirements
|
|
77
85
|
|
|
78
86
|
- OpenCode CLI (with `opencode db` command)
|
package/README.md
CHANGED
|
@@ -8,17 +8,17 @@ OpenCode CLI 的实时 Token 用量统计、缓存分析与性能指标插件。
|
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
- **侧边栏面板** — 会话级与按模型的实时统计(请求数、Token
|
|
12
|
-
- **缓存命中率** —
|
|
13
|
-
- **性能指标** —
|
|
14
|
-
- **Token 分布** —
|
|
15
|
-
-
|
|
11
|
+
- **侧边栏面板** — 会话级与按模型的实时统计(请求数、Token、缓存、成本)
|
|
12
|
+
- **缓存命中率** — 模型行内彩色进度条,带趋势指示器(↑/↓),全局总计行显示**加权命中率**
|
|
13
|
+
- **性能指标** — TTFT / TPS / 端到端延迟 + P50/P95/P99 延迟分位数
|
|
14
|
+
- **Token 分布** — 5 桶角色分解(system / user / toolCall / toolResult / output + other 兜底)
|
|
15
|
+
- **错误率统计** — 识别并统计失败请求(空 Token 响应),实时计算错误率
|
|
16
|
+
- **成本展示** — 按模型显示 cost(需 provider 返回计费数据)
|
|
16
17
|
- **`/usage` 命令** — HTML 报告 → JSON 导出 → 文本报告 → 设置
|
|
17
|
-
- **HTML 报告** — 交互式 ECharts
|
|
18
|
+
- **HTML 报告** — 交互式 ECharts 仪表盘:Token 分布、性能分位数、TPS 水平排名、错误率分析,自动在浏览器打开
|
|
19
|
+
- **持久化统计** — 性能指标(TPS/TTFT/延迟)写入独立 JSON 文件,永久累积,不受日志轮转影响
|
|
18
20
|
- **多级折叠** — 面板、模型、子区块均可折叠,状态持久化
|
|
19
|
-
- **语言切换** —
|
|
20
|
-
- **性能追踪** — 每次请求的 TTFT/TPS/延迟写入 JSONL,用于报告分析
|
|
21
|
-
- **自适应配色** — 从主题色自动衍生
|
|
21
|
+
- **语言切换** — 中英双语,跟随系统或手动切换
|
|
22
22
|
|
|
23
23
|
## 安装
|
|
24
24
|
|
|
@@ -44,8 +44,8 @@ npm install opencode-tokenwatch
|
|
|
44
44
|
"opencode-tokenwatch": {
|
|
45
45
|
"sidebar": {
|
|
46
46
|
"showPerformance": true,
|
|
47
|
-
"showPricing":
|
|
48
|
-
"showTokenDistribution":
|
|
47
|
+
"showPricing": true,
|
|
48
|
+
"showTokenDistribution": true,
|
|
49
49
|
"showTrend": true
|
|
50
50
|
},
|
|
51
51
|
"language": "auto"
|
|
@@ -57,7 +57,7 @@ npm install opencode-tokenwatch
|
|
|
57
57
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
58
58
|
|--------|------|--------|------|
|
|
59
59
|
| `sidebar.showPerformance` | boolean | `true` | 显示 TTFT/TPS/延迟 |
|
|
60
|
-
| `sidebar.showPricing` | boolean | `true` |
|
|
60
|
+
| `sidebar.showPricing` | boolean | `true` | 显示请求成本 |
|
|
61
61
|
| `sidebar.showTokenDistribution` | boolean | `true` | 显示 Token 分布 |
|
|
62
62
|
| `sidebar.showTrend` | boolean | `true` | 显示趋势指示器 |
|
|
63
63
|
| `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | 界面语言 |
|
|
@@ -73,6 +73,14 @@ npm install opencode-tokenwatch
|
|
|
73
73
|
- **文本报告** — 导出 Markdown 格式至 `~/.opencode/reports/`
|
|
74
74
|
- **设置** — 开关侧边栏显示项、切换语言
|
|
75
75
|
|
|
76
|
+
## 数据文件
|
|
77
|
+
|
|
78
|
+
| 文件 | 路径 | 说明 |
|
|
79
|
+
|------|------|------|
|
|
80
|
+
| JSONL 日志 | `~/.opencode/tokenwatch.jsonl` | 原始请求日志,超 5MB 自动轮转 |
|
|
81
|
+
| 聚合统计 | `~/.opencode/tokenwatch-stats.json` | 持久化性能统计,永久累积 |
|
|
82
|
+
| 报告输出 | `~/.opencode/reports/` | HTML / JSON / Markdown 报告 |
|
|
83
|
+
|
|
76
84
|
## 系统要求
|
|
77
85
|
|
|
78
86
|
- OpenCode CLI(支持 `opencode db` 命令)
|
package/dist/commands.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import { formatUsageReport } from "./formatter.js";
|
|
|
3
3
|
import { generateUsageHtml } from "./generate-usage-html.js";
|
|
4
4
|
import { t, setLanguage } from "./i18n.js";
|
|
5
5
|
import { readLogs } from "./perf-tracker.js";
|
|
6
|
+
import { readPersistedStats } from "./stats-store.js";
|
|
6
7
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { homedir } from "node:os";
|
|
@@ -44,6 +45,19 @@ function openInBrowser(filePath) {
|
|
|
44
45
|
}
|
|
45
46
|
catch { /* silently fail */ }
|
|
46
47
|
}
|
|
48
|
+
/** 线性插值百分位数,输入须为有序数组 */
|
|
49
|
+
function computePercentile(sortedArr, p) {
|
|
50
|
+
if (sortedArr.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
if (sortedArr.length === 1)
|
|
53
|
+
return sortedArr[0];
|
|
54
|
+
const idx = (p / 100) * (sortedArr.length - 1);
|
|
55
|
+
const lo = Math.floor(idx);
|
|
56
|
+
const hi = Math.ceil(idx);
|
|
57
|
+
if (lo === hi)
|
|
58
|
+
return sortedArr[lo];
|
|
59
|
+
return sortedArr[lo] + (sortedArr[hi] - sortedArr[lo]) * (idx - lo);
|
|
60
|
+
}
|
|
47
61
|
function aggregatePerfStats(logs) {
|
|
48
62
|
const map = new Map();
|
|
49
63
|
for (const entry of logs) {
|
|
@@ -54,10 +68,16 @@ function aggregatePerfStats(logs) {
|
|
|
54
68
|
model: key,
|
|
55
69
|
providerID: entry.providerID,
|
|
56
70
|
requestCount: 0,
|
|
71
|
+
ttftCount: 0, // Bug fix: 独立维护有效样本计数
|
|
72
|
+
tpsCount: 0,
|
|
73
|
+
latencyCount: 0,
|
|
57
74
|
totalInput: 0, totalOutput: 0, totalCacheRead: 0, totalCacheWrite: 0, totalCost: 0,
|
|
58
75
|
avgTTFT: null, maxTTFT: null, minTTFT: null,
|
|
76
|
+
p50TTFT: null, p95TTFT: null, p99TTFT: null,
|
|
59
77
|
avgTPS: null, maxTPS: null, minTPS: null,
|
|
60
78
|
avgLatency: null, maxLatency: null, minLatency: null,
|
|
79
|
+
p50Latency: null, p95Latency: null, p99Latency: null,
|
|
80
|
+
cacheHitRate: null,
|
|
61
81
|
};
|
|
62
82
|
map.set(key, s);
|
|
63
83
|
}
|
|
@@ -67,28 +87,69 @@ function aggregatePerfStats(logs) {
|
|
|
67
87
|
s.totalCacheRead += entry.cacheReadTokens;
|
|
68
88
|
s.totalCacheWrite += entry.cacheWriteTokens;
|
|
69
89
|
s.totalCost += entry.cost;
|
|
70
|
-
const c = s.requestCount;
|
|
71
90
|
if (entry.ttft_ms != null) {
|
|
91
|
+
// Bug fix: 分母使用 ttftCount(有效样本数),而非 requestCount(总请求数)
|
|
92
|
+
s.ttftCount++;
|
|
93
|
+
const c = s.ttftCount;
|
|
72
94
|
s.avgTTFT = s.avgTTFT != null ? s.avgTTFT + (entry.ttft_ms - s.avgTTFT) / c : entry.ttft_ms;
|
|
73
95
|
s.maxTTFT = s.maxTTFT != null ? Math.max(s.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
74
96
|
s.minTTFT = s.minTTFT != null ? Math.min(s.minTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
75
97
|
}
|
|
76
98
|
if (entry.tps != null) {
|
|
99
|
+
// Bug fix: 分母使用 tpsCount(有效样本数)
|
|
100
|
+
s.tpsCount++;
|
|
101
|
+
const c = s.tpsCount;
|
|
77
102
|
s.avgTPS = s.avgTPS != null ? s.avgTPS + (entry.tps - s.avgTPS) / c : entry.tps;
|
|
78
103
|
s.maxTPS = s.maxTPS != null ? Math.max(s.maxTPS, entry.tps) : entry.tps;
|
|
79
104
|
s.minTPS = s.minTPS != null ? Math.min(s.minTPS, entry.tps) : entry.tps;
|
|
80
105
|
}
|
|
81
106
|
if (entry.latency_ms != null) {
|
|
107
|
+
s.latencyCount++;
|
|
108
|
+
const c = s.latencyCount;
|
|
82
109
|
s.avgLatency = s.avgLatency != null ? s.avgLatency + (entry.latency_ms - s.avgLatency) / c : entry.latency_ms;
|
|
83
110
|
s.maxLatency = s.maxLatency != null ? Math.max(s.maxLatency, entry.latency_ms) : entry.latency_ms;
|
|
84
111
|
s.minLatency = s.minLatency != null ? Math.min(s.minLatency, entry.latency_ms) : entry.latency_ms;
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
|
-
|
|
114
|
+
// 分位数后处理:需要收集每个模型的所有样本然后计算
|
|
115
|
+
// 注: 此处采用单次遍历日志重新收集分数据,需要两次遍历
|
|
116
|
+
const ttftBuckets = new Map();
|
|
117
|
+
const latBuckets = new Map();
|
|
118
|
+
for (const entry of logs) {
|
|
119
|
+
const key = entry.model;
|
|
120
|
+
if (entry.ttft_ms != null) {
|
|
121
|
+
const arr = ttftBuckets.get(key) ?? [];
|
|
122
|
+
arr.push(entry.ttft_ms);
|
|
123
|
+
ttftBuckets.set(key, arr);
|
|
124
|
+
}
|
|
125
|
+
if (entry.latency_ms != null) {
|
|
126
|
+
const arr = latBuckets.get(key) ?? [];
|
|
127
|
+
arr.push(entry.latency_ms);
|
|
128
|
+
latBuckets.set(key, arr);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const result = Array.from(map.values());
|
|
132
|
+
for (const s of result) {
|
|
133
|
+
const ttftArr = [...(ttftBuckets.get(s.model) ?? [])].sort((a, b) => a - b);
|
|
134
|
+
s.p50TTFT = computePercentile(ttftArr, 50);
|
|
135
|
+
s.p95TTFT = computePercentile(ttftArr, 95);
|
|
136
|
+
s.p99TTFT = computePercentile(ttftArr, 99);
|
|
137
|
+
const latArr = [...(latBuckets.get(s.model) ?? [])].sort((a, b) => a - b);
|
|
138
|
+
s.p50Latency = computePercentile(latArr, 50);
|
|
139
|
+
s.p95Latency = computePercentile(latArr, 95);
|
|
140
|
+
s.p99Latency = computePercentile(latArr, 99);
|
|
141
|
+
const denom = s.totalInput + s.totalCacheRead;
|
|
142
|
+
s.cacheHitRate = denom > 0 ? (s.totalCacheRead / denom) * 100 : null;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
88
145
|
}
|
|
89
146
|
async function buildCombinedData(api, filters = {}) {
|
|
90
147
|
const report = await getUsageReport(filters);
|
|
91
|
-
|
|
148
|
+
// perfLogs: 仅用于 JSON 导出参考,保持适当窗口即可
|
|
149
|
+
const logs = readLogs(200);
|
|
150
|
+
// perfSummary: 使用持久化聚合统计,包含自插件安装以来的全量历史数据
|
|
151
|
+
// 不再受 readLogs 窗口限制,即使 JSONL 被轮转,历史指标也不会丢失
|
|
152
|
+
const perfSummary = readPersistedStats();
|
|
92
153
|
const now = new Date();
|
|
93
154
|
const pad = (n) => String(n).padStart(2, '0');
|
|
94
155
|
const meta = {
|
|
@@ -101,7 +162,7 @@ async function buildCombinedData(api, filters = {}) {
|
|
|
101
162
|
return {
|
|
102
163
|
...report,
|
|
103
164
|
perfLogs: logs,
|
|
104
|
-
perfSummary
|
|
165
|
+
perfSummary,
|
|
105
166
|
meta,
|
|
106
167
|
};
|
|
107
168
|
}
|
|
@@ -152,6 +213,10 @@ function showHtmlReportRangeMenu(api, dialog) {
|
|
|
152
213
|
]} flat={true}/>));
|
|
153
214
|
}
|
|
154
215
|
function showUsageMenu(api, dialog) {
|
|
216
|
+
try {
|
|
217
|
+
setLanguage(loadConfigFromStore(api).language);
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
155
220
|
dialog.replace(() => (<api.ui.DialogSelect title={t("panelTitle")} placeholder="Select an action..." options={[
|
|
156
221
|
{
|
|
157
222
|
title: `${t("cmdTitleHtml")} ▸`,
|
package/dist/formatter.d.ts
CHANGED
|
@@ -67,6 +67,22 @@ export interface SessionBreakdownItem {
|
|
|
67
67
|
totalCost: number;
|
|
68
68
|
day: string;
|
|
69
69
|
}
|
|
70
|
+
/** 失败请求统计 */
|
|
71
|
+
export interface ErrorStats {
|
|
72
|
+
/** 过滤内总请求数(tokens.total > 0 的 assistant) */
|
|
73
|
+
successCount: number;
|
|
74
|
+
/** 失败请求数(tokens.total == 0 的 assistant) */
|
|
75
|
+
failedCount: number;
|
|
76
|
+
/** 失败率:failedCount / (successCount + failedCount) */
|
|
77
|
+
errorRate: number;
|
|
78
|
+
/** 按模型细化的失败数 */
|
|
79
|
+
byModel: Array<{
|
|
80
|
+
provider: string;
|
|
81
|
+
model: string;
|
|
82
|
+
failed: number;
|
|
83
|
+
total: number;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
70
86
|
export interface UsageReport {
|
|
71
87
|
filters: UsageFilters;
|
|
72
88
|
summary: SessionTokenData;
|
|
@@ -74,6 +90,7 @@ export interface UsageReport {
|
|
|
74
90
|
providers: ProviderBreakdownItem[];
|
|
75
91
|
daily: DailyBreakdownItem[];
|
|
76
92
|
sessions: SessionBreakdownItem[];
|
|
93
|
+
errors?: ErrorStats;
|
|
77
94
|
}
|
|
78
95
|
export declare function formatTokens(n: number): string;
|
|
79
96
|
export declare function formatCost(n: number): string;
|
|
@@ -95,12 +112,17 @@ export interface SessionPerfStats {
|
|
|
95
112
|
totalCacheWrite: number;
|
|
96
113
|
totalRequests: number;
|
|
97
114
|
totalCost: number;
|
|
115
|
+
/** 全局加权缓存命中率(按请求数加权平均) */
|
|
116
|
+
weightedCacheHitRate: number | null;
|
|
98
117
|
};
|
|
99
118
|
}
|
|
100
119
|
export interface ModelPerfStats {
|
|
101
120
|
model: string;
|
|
102
121
|
providerID: string;
|
|
103
122
|
requestCount: number;
|
|
123
|
+
ttftCount: number;
|
|
124
|
+
tpsCount: number;
|
|
125
|
+
latencyCount: number;
|
|
104
126
|
totalInput: number;
|
|
105
127
|
totalOutput: number;
|
|
106
128
|
totalCacheRead: number;
|
|
@@ -109,12 +131,20 @@ export interface ModelPerfStats {
|
|
|
109
131
|
avgTTFT: number | null;
|
|
110
132
|
maxTTFT: number | null;
|
|
111
133
|
minTTFT: number | null;
|
|
134
|
+
p50TTFT: number | null;
|
|
135
|
+
p95TTFT: number | null;
|
|
136
|
+
p99TTFT: number | null;
|
|
112
137
|
avgTPS: number | null;
|
|
113
138
|
maxTPS: number | null;
|
|
114
139
|
minTPS: number | null;
|
|
115
140
|
avgLatency: number | null;
|
|
116
141
|
maxLatency: number | null;
|
|
117
142
|
minLatency: number | null;
|
|
143
|
+
p50Latency: number | null;
|
|
144
|
+
p95Latency: number | null;
|
|
145
|
+
p99Latency: number | null;
|
|
146
|
+
/** 该模型加权缓存命中率:cacheRead / (cacheRead + input) */
|
|
147
|
+
cacheHitRate: number | null;
|
|
118
148
|
}
|
|
119
149
|
export interface TokenDistribution {
|
|
120
150
|
system: number;
|