opencode-tokenwatch 0.3.2 → 0.5.0

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/dist/i18n.js DELETED
@@ -1,173 +0,0 @@
1
- const zh = {
2
- panelTitle: "TokenWatch",
3
- collapse: "折叠",
4
- expand: "展开",
5
- sessionSummary: "会话累计",
6
- input: "输入",
7
- output: "输出",
8
- cacheRead: "缓存",
9
- cacheWrite: "缓存写",
10
- cacheMiss: "未命中",
11
- hitRate: "命中率",
12
- requests: "请求",
13
- cost: "成本",
14
- trendUp: "↑",
15
- trendDown: "↓",
16
- cache: "缓存",
17
- lat: "延迟",
18
- performance: "性能",
19
- pricing: "Pricing",
20
- tokenDistribution: "Token分布",
21
- modelLabel: "模型",
22
- provider: "提供商",
23
- ttft: "TTFT",
24
- tps: "TPS",
25
- latency: "延迟",
26
- avg: "平均",
27
- max: "最大",
28
- min: "最小",
29
- read: "读",
30
- write: "写",
31
- sessionAccumulated: "Session累计",
32
- saving: "节省",
33
- priceInput: "输入",
34
- priceCacheRead: "缓存读",
35
- priceCacheWrite: "缓存写",
36
- priceOutput: "输出",
37
- total: "总计",
38
- system: "系统提示",
39
- user: "用户",
40
- agent: "Agent指令",
41
- toolCall: "Tool调用",
42
- toolResult: "Tool结果",
43
- outputTokens: "输出",
44
- showPerformance: "显示性能指标",
45
- showPricing: "显示模型定价",
46
- showTokenDistribution: "显示Token分布",
47
- showTrend: "显示趋势指示器",
48
- language: "语言",
49
- auto: "自动",
50
- cmdTitleHtml: "HTML报告",
51
- cmdDescHtml: "生成交互式HTML仪表盘,展示Token用量、缓存和性能图表",
52
- cmdTitleJson: "JSON导出",
53
- cmdDescJson: "导出原始用量数据为JSON文件",
54
- cmdTitleText: "文本报告",
55
- cmdDescText: "生成纯文本报告文件",
56
- cmdTitleSettings: "设置",
57
- cmdDescSettings: "配置侧边栏显示选项",
58
- descShowPerformance: "在侧边栏显示TPS、TTFT、延迟等指标",
59
- descShowPricing: "在侧边栏显示成本估算",
60
- descShowTokenDistribution: "在侧边栏显示输入/输出/推理Token细分",
61
- descShowTrend: "在侧边栏显示Token用量趋势",
62
- settingsLanguage: "语言",
63
- descSettingsLanguage: "切换显示语言",
64
- settingsTitle: "TokenWatch设置",
65
- settingsPlaceholder: "切换设置项...",
66
- langAuto: "自动",
67
- done: "完成",
68
- closeSettings: "关闭设置",
69
- menuToday: "今天",
70
- menu7d: "最近 7 天",
71
- menu30d: "最近 30 天",
72
- menuAll: "全部时间",
73
- };
74
- const en = {
75
- panelTitle: "TokenWatch",
76
- collapse: "Collapse",
77
- expand: "Expand",
78
- sessionSummary: "Session",
79
- input: "Input",
80
- output: "Output",
81
- cacheRead: "Cache",
82
- cacheWrite: "C.Write",
83
- cacheMiss: "Cache Miss",
84
- hitRate: "Hit Rate",
85
- requests: "Req",
86
- cost: "Cost",
87
- trendUp: "↑",
88
- trendDown: "↓",
89
- cache: "Cache",
90
- lat: "Lat",
91
- performance: "Performance",
92
- pricing: "Pricing",
93
- tokenDistribution: "Token Distribution",
94
- modelLabel: "Model",
95
- provider: "Provider",
96
- ttft: "TTFT",
97
- tps: "TPS",
98
- latency: "Latency",
99
- avg: "Avg",
100
- max: "Max",
101
- min: "Min",
102
- read: "Read",
103
- write: "Write",
104
- sessionAccumulated: "Session Accumulated",
105
- saving: "Saving",
106
- priceInput: "Input",
107
- priceCacheRead: "Cache Read",
108
- priceCacheWrite: "Cache Write",
109
- priceOutput: "Output",
110
- total: "Total",
111
- system: "System",
112
- user: "User",
113
- agent: "Agent",
114
- toolCall: "Tool Call",
115
- toolResult: "Tool Result",
116
- outputTokens: "Output",
117
- showPerformance: "Show Performance",
118
- showPricing: "Show Pricing",
119
- showTokenDistribution: "Show Token Distribution",
120
- showTrend: "Show Trend",
121
- language: "Language",
122
- auto: "Auto",
123
- cmdTitleHtml: "HTML Report",
124
- cmdDescHtml: "Generate interactive HTML dashboard with token usage, cache, and performance charts",
125
- cmdTitleJson: "JSON Export",
126
- cmdDescJson: "Export raw usage data as JSON file",
127
- cmdTitleText: "Text Report",
128
- cmdDescText: "Generate plain text report file",
129
- cmdTitleSettings: "Settings",
130
- cmdDescSettings: "Configure sidebar display options",
131
- descShowPerformance: "Display TPS, TTFT, latency metrics in sidebar",
132
- descShowPricing: "Display cost estimates in sidebar",
133
- descShowTokenDistribution: "Display input/output/reasoning token breakdown in sidebar",
134
- descShowTrend: "Display token usage trend in sidebar",
135
- settingsLanguage: "Language",
136
- descSettingsLanguage: "Switch display language",
137
- settingsTitle: "TokenWatch Settings",
138
- settingsPlaceholder: "Toggle settings...",
139
- langAuto: "Auto",
140
- done: "Done",
141
- closeSettings: "Close settings",
142
- menuToday: "Today",
143
- menu7d: "Last 7 Days",
144
- menu30d: "Last 30 Days",
145
- menuAll: "All Time",
146
- };
147
- let currentLang = detectLanguage();
148
- export function detectLanguage() {
149
- try {
150
- const locale = Intl.DateTimeFormat().resolvedOptions().locale;
151
- if (locale.startsWith("zh"))
152
- return "zh";
153
- }
154
- catch {
155
- // Intl may not be available in restricted environments
156
- }
157
- return "en";
158
- }
159
- export function setLanguage(lang) {
160
- if (lang === "auto") {
161
- currentLang = detectLanguage();
162
- }
163
- else {
164
- currentLang = lang;
165
- }
166
- }
167
- export function getCurrentLanguage() {
168
- return currentLang;
169
- }
170
- export function t(key) {
171
- const table = currentLang === "zh" ? zh : en;
172
- return table[key] ?? key;
173
- }
package/dist/index.js DELETED
@@ -1,7 +0,0 @@
1
- const plugin = {
2
- id: "opencode-tokenwatch",
3
- server: async () => {
4
- return {};
5
- },
6
- };
7
- export default plugin;
@@ -1,299 +0,0 @@
1
- import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- import { existsSync, statSync } from "node:fs";
5
- import { updatePersistedStats } from "./stats-store.js";
6
- const LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
7
- class PerfTracker {
8
- firstPartTimes = new Map();
9
- statsMap = new Map();
10
- /** 原始样本串,用于分位数计算,不持久化 */
11
- ttftSamples = new Map();
12
- latencySamples = new Map();
13
- handlePartUpdated(event) {
14
- if (!event.time?.start || !event.message_id)
15
- return;
16
- // Bug fix: 取最早 part 时间而非最后一个,避免 TTFT 被高估
17
- const cur = this.firstPartTimes.get(event.message_id) ?? Number.POSITIVE_INFINITY;
18
- if (event.time.start < cur) {
19
- this.firstPartTimes.set(event.message_id, event.time.start);
20
- }
21
- }
22
- handleMessageUpdated(event) {
23
- const info = event.properties?.info;
24
- if (!info || info.role !== "assistant")
25
- return;
26
- if (!info.time?.completed)
27
- return;
28
- const messageID = info.id ?? "";
29
- const created = info.time.created;
30
- const completed = info.time.completed;
31
- if (!created || !completed) {
32
- this.firstPartTimes.delete(messageID);
33
- return;
34
- }
35
- const sessionID = info.sessionID ?? "";
36
- const providerID = info.providerID ?? "unknown";
37
- const modelID = info.modelID ?? "unknown";
38
- const model = `${providerID}/${modelID}`;
39
- const tokens = info.tokens;
40
- const inputTokens = tokens?.input ?? 0;
41
- const outputTokens = tokens?.output ?? 0;
42
- const reasoningTokens = tokens?.reasoning ?? 0;
43
- const cacheRead = tokens?.cache?.read ?? 0;
44
- const cacheWrite = tokens?.cache?.write ?? 0;
45
- const cost = info.cost ?? 0;
46
- // 过滤全零 token 的失败请求,不写入日志和统计,防止污染数据
47
- if (inputTokens + outputTokens + cacheRead + cacheWrite === 0) {
48
- this.firstPartTimes.delete(messageID);
49
- return;
50
- }
51
- const firstPart = this.firstPartTimes.get(messageID) ?? null;
52
- const latencyMs = completed - created;
53
- const ttftMs = firstPart !== null ? firstPart - created : null;
54
- const genMs = firstPart !== null ? completed - firstPart : null;
55
- const tps = (genMs !== null && genMs > 0 && outputTokens > 0)
56
- ? (outputTokens / genMs) * 1000
57
- : null;
58
- // Bug fix: 移除 TPS fallback。
59
- // 原 fallback 用 latencyMs(completed-created,含排队+TTFT)计算 TPS,
60
- // 会使结果严重低估(约 40%+)。null 表示"无可靠数据"比虚假数字更好。
61
- this.firstPartTimes.delete(messageID);
62
- const entry = {
63
- ts: new Date().toISOString(),
64
- model,
65
- providerID,
66
- modelID,
67
- sessionID,
68
- ttft_ms: ttftMs,
69
- tps: tps, // 只在有可靠 genMs 时才有值
70
- latency_ms: latencyMs,
71
- inputTokens,
72
- outputTokens,
73
- reasoningTokens,
74
- cacheReadTokens: cacheRead,
75
- cacheWriteTokens: cacheWrite,
76
- cost,
77
- };
78
- this.appendLog(entry);
79
- this.updateStats(model, entry);
80
- }
81
- appendLog(entry) {
82
- try {
83
- // Risk fix: JSONL 日志轮转保护,防止长期使用后文件无限增长
84
- // 超过 5MB 时截断,保留最新 2000 行
85
- // 注意:轮转前先调用 updatePersistedStats,确保被轮转行的数据已持久化
86
- const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
87
- const KEEP_LINES = 2000;
88
- if (existsSync(LOG_PATH) && statSync(LOG_PATH).size > MAX_SIZE) {
89
- const lines = readFileSync(LOG_PATH, "utf-8").trim().split("\n");
90
- writeFileSync(LOG_PATH, lines.slice(-KEEP_LINES).join("\n") + "\n");
91
- }
92
- appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
93
- }
94
- catch {
95
- // Silently fail — logging is non-critical
96
- }
97
- // 无论 JSONL 写入是否成功,都尝试更新持久化聚合统计
98
- // 这样即使日志被轮转,历史统计数据也永不丢失
99
- updatePersistedStats(entry);
100
- }
101
- handleMessageRemoved(event) {
102
- const mid = event.properties?.messageID ?? "";
103
- if (mid) {
104
- this.firstPartTimes.delete(mid);
105
- }
106
- }
107
- updateStats(model, entry) {
108
- let stats = this.statsMap.get(model);
109
- if (!stats) {
110
- stats = {
111
- model,
112
- providerID: entry.providerID,
113
- requestCount: 0,
114
- ttftCount: 0, // Bug fix: 独立维护有效样本计数
115
- tpsCount: 0,
116
- latencyCount: 0,
117
- totalInput: 0,
118
- totalOutput: 0,
119
- totalCacheRead: 0,
120
- totalCacheWrite: 0,
121
- totalCost: 0,
122
- avgTTFT: null,
123
- maxTTFT: null,
124
- minTTFT: null,
125
- p50TTFT: null,
126
- p95TTFT: null,
127
- p99TTFT: null,
128
- avgTPS: null,
129
- maxTPS: null,
130
- minTPS: null,
131
- avgLatency: null,
132
- maxLatency: null,
133
- minLatency: null,
134
- p50Latency: null,
135
- p95Latency: null,
136
- p99Latency: null,
137
- cacheHitRate: null,
138
- };
139
- this.statsMap.set(model, stats);
140
- }
141
- stats.requestCount++;
142
- stats.totalInput += entry.inputTokens;
143
- stats.totalOutput += entry.outputTokens;
144
- stats.totalCacheRead += entry.cacheReadTokens;
145
- stats.totalCacheWrite += entry.cacheWriteTokens;
146
- stats.totalCost += entry.cost;
147
- if (entry.ttft_ms !== null) {
148
- // Bug fix: 分母使用 ttftCount(有效样本数),而非 requestCount(总请求数)
149
- stats.ttftCount++;
150
- const c = stats.ttftCount;
151
- const prev = stats.avgTTFT;
152
- stats.avgTTFT = prev !== null ? prev + (entry.ttft_ms - prev) / c : entry.ttft_ms;
153
- stats.maxTTFT = stats.maxTTFT !== null ? Math.max(stats.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
154
- stats.minTTFT = stats.minTTFT !== null ? Math.min(stats.minTTFT, entry.ttft_ms) : entry.ttft_ms;
155
- // 收集原始样本用于分位数计算
156
- const ttftArr = this.ttftSamples.get(model) ?? [];
157
- ttftArr.push(entry.ttft_ms);
158
- this.ttftSamples.set(model, ttftArr);
159
- }
160
- if (entry.tps !== null) {
161
- // Bug fix: 分母使用 tpsCount(有效样本数)
162
- stats.tpsCount++;
163
- const c = stats.tpsCount;
164
- const prev = stats.avgTPS;
165
- stats.avgTPS = prev !== null ? prev + (entry.tps - prev) / c : entry.tps;
166
- stats.maxTPS = stats.maxTPS !== null ? Math.max(stats.maxTPS, entry.tps) : entry.tps;
167
- stats.minTPS = stats.minTPS !== null ? Math.min(stats.minTPS, entry.tps) : entry.tps;
168
- }
169
- if (entry.latency_ms !== null) {
170
- // latency 每条消息都有,但保持一致使用专用计数
171
- stats.latencyCount++;
172
- const c = stats.latencyCount;
173
- const prev = stats.avgLatency;
174
- stats.avgLatency = prev !== null ? prev + (entry.latency_ms - prev) / c : entry.latency_ms;
175
- stats.maxLatency = stats.maxLatency !== null ? Math.max(stats.maxLatency, entry.latency_ms) : entry.latency_ms;
176
- stats.minLatency = stats.minLatency !== null ? Math.min(stats.minLatency, entry.latency_ms) : entry.latency_ms;
177
- // 收集原始样本用于分位数计算
178
- const latArr = this.latencySamples.get(model) ?? [];
179
- latArr.push(entry.latency_ms);
180
- this.latencySamples.set(model, latArr);
181
- }
182
- }
183
- /** 计算有序数组的指定百分位数(线性插值法) */
184
- percentile(sortedArr, p) {
185
- if (sortedArr.length === 0)
186
- return null;
187
- if (sortedArr.length === 1)
188
- return sortedArr[0];
189
- const idx = (p / 100) * (sortedArr.length - 1);
190
- const lo = Math.floor(idx);
191
- const hi = Math.ceil(idx);
192
- if (lo === hi)
193
- return sortedArr[lo];
194
- return sortedArr[lo] + (sortedArr[hi] - sortedArr[lo]) * (idx - lo);
195
- }
196
- getSessionStats() {
197
- let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
198
- let totalRequests = 0, totalCost = 0;
199
- let weightedHitSum = 0, totalReqForHit = 0;
200
- for (const [model, s] of this.statsMap) {
201
- totalInput += s.totalInput;
202
- totalOutput += s.totalOutput;
203
- totalCacheRead += s.totalCacheRead;
204
- totalCacheWrite += s.totalCacheWrite;
205
- totalRequests += s.requestCount;
206
- totalCost += s.totalCost;
207
- // 计算每个模型的分位数(需先排序)
208
- const ttftArr = [...(this.ttftSamples.get(model) ?? [])].sort((a, b) => a - b);
209
- s.p50TTFT = this.percentile(ttftArr, 50);
210
- s.p95TTFT = this.percentile(ttftArr, 95);
211
- s.p99TTFT = this.percentile(ttftArr, 99);
212
- const latArr = [...(this.latencySamples.get(model) ?? [])].sort((a, b) => a - b);
213
- s.p50Latency = this.percentile(latArr, 50);
214
- s.p95Latency = this.percentile(latArr, 95);
215
- s.p99Latency = this.percentile(latArr, 99);
216
- // 模型级缓存命中率
217
- const denom = s.totalInput + s.totalCacheRead;
218
- s.cacheHitRate = denom > 0 ? (s.totalCacheRead / denom) * 100 : null;
219
- // 累加加权命中率(按请求数加权)
220
- if (s.cacheHitRate !== null) {
221
- weightedHitSum += s.cacheHitRate * s.requestCount;
222
- totalReqForHit += s.requestCount;
223
- }
224
- }
225
- const weightedCacheHitRate = totalReqForHit > 0 ? weightedHitSum / totalReqForHit : null;
226
- return {
227
- models: Object.fromEntries(this.statsMap),
228
- totals: { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalRequests, totalCost, weightedCacheHitRate },
229
- };
230
- }
231
- readLogs(last = 50) {
232
- try {
233
- if (!existsSync(LOG_PATH))
234
- return [];
235
- const content = readFileSync(LOG_PATH, "utf-8").trim();
236
- if (!content)
237
- return [];
238
- const lines = content.split("\n");
239
- const entries = [];
240
- for (let i = Math.max(0, lines.length - last); i < lines.length; i++) {
241
- try {
242
- entries.push(JSON.parse(lines[i]));
243
- }
244
- catch {
245
- // Skip malformed lines
246
- }
247
- }
248
- return entries;
249
- }
250
- catch {
251
- return [];
252
- }
253
- }
254
- reset() {
255
- this.firstPartTimes.clear();
256
- this.statsMap.clear();
257
- this.ttftSamples.clear();
258
- this.latencySamples.clear();
259
- }
260
- loadSession(sessionID) {
261
- this.firstPartTimes.clear();
262
- this.statsMap.clear();
263
- this.ttftSamples.clear();
264
- this.latencySamples.clear();
265
- if (!sessionID)
266
- return;
267
- try {
268
- if (!existsSync(LOG_PATH))
269
- return;
270
- const content = readFileSync(LOG_PATH, "utf-8").trim();
271
- if (!content)
272
- return;
273
- const lines = content.split("\n");
274
- for (const line of lines) {
275
- if (!line)
276
- continue;
277
- try {
278
- const entry = JSON.parse(line);
279
- if (entry.sessionID === sessionID) {
280
- this.updateStats(entry.model, entry);
281
- }
282
- }
283
- catch {
284
- // Skip malformed lines
285
- }
286
- }
287
- }
288
- catch {
289
- // Non-critical loading failure
290
- }
291
- }
292
- }
293
- export function createPerfTracker() {
294
- return new PerfTracker();
295
- }
296
- export function readLogs(last = 50) {
297
- const tracker = new PerfTracker();
298
- return tracker.readLogs(last);
299
- }