opencode-tokenwatch 0.3.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/dist/i18n.js CHANGED
@@ -14,6 +14,7 @@ const zh = {
14
14
  trendUp: "↑",
15
15
  trendDown: "↓",
16
16
  cache: "缓存",
17
+ lat: "延迟",
17
18
  performance: "性能",
18
19
  pricing: "Pricing",
19
20
  tokenDistribution: "Token分布",
@@ -86,6 +87,7 @@ const en = {
86
87
  trendUp: "↑",
87
88
  trendDown: "↓",
88
89
  cache: "Cache",
90
+ lat: "Lat",
89
91
  performance: "Performance",
90
92
  pricing: "Pricing",
91
93
  tokenDistribution: "Token Distribution",
@@ -54,6 +54,7 @@ declare class PerfTracker {
54
54
  getSessionStats(): SessionPerfStats;
55
55
  readLogs(last?: number): LogEntry[];
56
56
  reset(): void;
57
+ loadSession(sessionID: string): void;
57
58
  }
58
59
  export declare function createPerfTracker(): PerfTracker;
59
60
  export type { PartEvent, PerfTracker };
@@ -2,6 +2,7 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { existsSync, statSync } from "node:fs";
5
+ import { updatePersistedStats } from "./stats-store.js";
5
6
  const LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
6
7
  class PerfTracker {
7
8
  firstPartTimes = new Map();
@@ -76,6 +77,7 @@ class PerfTracker {
76
77
  try {
77
78
  // Risk fix: JSONL 日志轮转保护,防止长期使用后文件无限增长
78
79
  // 超过 5MB 时截断,保留最新 2000 行
80
+ // 注意:轮转前先调用 updatePersistedStats,确保被轮转行的数据已持久化
79
81
  const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
80
82
  const KEEP_LINES = 2000;
81
83
  if (existsSync(LOG_PATH) && statSync(LOG_PATH).size > MAX_SIZE) {
@@ -87,6 +89,9 @@ class PerfTracker {
87
89
  catch {
88
90
  // Silently fail — logging is non-critical
89
91
  }
92
+ // 无论 JSONL 写入是否成功,都尝试更新持久化聚合统计
93
+ // 这样即使日志被轮转,历史统计数据也永不丢失
94
+ updatePersistedStats(entry);
90
95
  }
91
96
  handleMessageRemoved(event) {
92
97
  const mid = event.properties?.messageID ?? "";
@@ -247,6 +252,38 @@ class PerfTracker {
247
252
  this.ttftSamples.clear();
248
253
  this.latencySamples.clear();
249
254
  }
255
+ loadSession(sessionID) {
256
+ this.firstPartTimes.clear();
257
+ this.statsMap.clear();
258
+ this.ttftSamples.clear();
259
+ this.latencySamples.clear();
260
+ if (!sessionID)
261
+ return;
262
+ try {
263
+ if (!existsSync(LOG_PATH))
264
+ return;
265
+ const content = readFileSync(LOG_PATH, "utf-8").trim();
266
+ if (!content)
267
+ return;
268
+ const lines = content.split("\n");
269
+ for (const line of lines) {
270
+ if (!line)
271
+ continue;
272
+ try {
273
+ const entry = JSON.parse(line);
274
+ if (entry.sessionID === sessionID) {
275
+ this.updateStats(entry.model, entry);
276
+ }
277
+ }
278
+ catch {
279
+ // Skip malformed lines
280
+ }
281
+ }
282
+ }
283
+ catch {
284
+ // Non-critical loading failure
285
+ }
286
+ }
250
287
  }
251
288
  export function createPerfTracker() {
252
289
  return new PerfTracker();
package/dist/queries.js CHANGED
@@ -90,41 +90,41 @@ export async function getCurrentSessionStats(sessionId) {
90
90
  return getSummary(filters);
91
91
  }
92
92
  export async function getSummary(filters = {}) {
93
- const sql = `
94
- SELECT
95
- group_concat(distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown')) as models_used,
96
- group_concat(distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown')) as providers_used,
97
- count(*) as request_count,
98
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
99
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
100
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
101
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
102
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
103
- sum(coalesce(json_extract(m.data, '$.tokens.cache.write'), 0)) as cache_write,
104
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
105
- FROM message m
106
- WHERE ${messageWhere(filters)}
93
+ const sql = `
94
+ SELECT
95
+ group_concat(distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown')) as models_used,
96
+ group_concat(distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown')) as providers_used,
97
+ count(*) as request_count,
98
+ sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
99
+ sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
100
+ sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
101
+ sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
102
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
103
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.write'), 0)) as cache_write,
104
+ sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
105
+ FROM message m
106
+ WHERE ${messageWhere(filters)}
107
107
  `.trim();
108
108
  const rows = await queryDb(sql);
109
109
  return toSessionTokenData(rows[0]);
110
110
  }
111
111
  export async function getModelBreakdown(filters = {}) {
112
- const sql = `
113
- SELECT
114
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
115
- coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
116
- count(*) as requests,
117
- count(distinct m.session_id) as sessions,
118
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
119
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
120
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
121
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
122
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
123
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
124
- FROM message m
125
- WHERE ${messageWhere(filters)}
126
- GROUP BY provider, model
127
- ORDER BY total_tokens DESC
112
+ const sql = `
113
+ SELECT
114
+ coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
115
+ coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
116
+ count(*) as requests,
117
+ count(distinct m.session_id) as sessions,
118
+ sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
119
+ sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
120
+ sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
121
+ sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
122
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
123
+ sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
124
+ FROM message m
125
+ WHERE ${messageWhere(filters)}
126
+ GROUP BY provider, model
127
+ ORDER BY total_tokens DESC
128
128
  `.trim();
129
129
  const rows = await queryDb(sql);
130
130
  return rows.map((row) => ({
@@ -141,21 +141,21 @@ ORDER BY total_tokens DESC
141
141
  }));
142
142
  }
143
143
  export async function getProviderBreakdown(filters = {}) {
144
- const sql = `
145
- SELECT
146
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
147
- count(*) as requests,
148
- count(distinct m.session_id) as sessions,
149
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
150
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
151
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
152
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
153
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
154
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
155
- FROM message m
156
- WHERE ${messageWhere(filters)}
157
- GROUP BY provider
158
- ORDER BY total_tokens DESC
144
+ const sql = `
145
+ SELECT
146
+ coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
147
+ count(*) as requests,
148
+ count(distinct m.session_id) as sessions,
149
+ sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
150
+ sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
151
+ sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
152
+ sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
153
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
154
+ sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
155
+ FROM message m
156
+ WHERE ${messageWhere(filters)}
157
+ GROUP BY provider
158
+ ORDER BY total_tokens DESC
159
159
  `.trim();
160
160
  const rows = await queryDb(sql);
161
161
  return rows.map((row) => ({
@@ -172,22 +172,22 @@ ORDER BY total_tokens DESC
172
172
  }
173
173
  export async function getDailyBreakdown(filters = {}) {
174
174
  const limit = filters.limit ?? 30;
175
- const sql = `
176
- SELECT
177
- date(m.time_created / 1000, 'unixepoch', 'localtime') as day,
178
- count(*) as requests,
179
- count(distinct m.session_id) as sessions,
180
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
181
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
182
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
183
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
184
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
185
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
186
- FROM message m
187
- WHERE ${messageWhere(filters)}
188
- GROUP BY day
189
- ORDER BY day DESC
190
- LIMIT ${Math.max(1, limit)}
175
+ const sql = `
176
+ SELECT
177
+ date(m.time_created / 1000, 'unixepoch', 'localtime') as day,
178
+ count(*) as requests,
179
+ count(distinct m.session_id) as sessions,
180
+ sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
181
+ sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
182
+ sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
183
+ sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
184
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
185
+ sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
186
+ FROM message m
187
+ WHERE ${messageWhere(filters)}
188
+ GROUP BY day
189
+ ORDER BY day DESC
190
+ LIMIT ${Math.max(1, limit)}
191
191
  `.trim();
192
192
  const rows = await queryDb(sql);
193
193
  return rows.map((row) => ({
@@ -204,26 +204,26 @@ LIMIT ${Math.max(1, limit)}
204
204
  }
205
205
  export async function getSessionBreakdown(filters = {}) {
206
206
  const limit = filters.limit ?? 15;
207
- const sql = `
208
- SELECT
209
- s.id as session_id,
210
- s.title as title,
211
- coalesce(json_extract(m.data, '$.providerID'), json_extract(s.model, '$.providerID'), 'unknown') as provider,
212
- coalesce(json_extract(m.data, '$.modelID'), json_extract(s.model, '$.id'), 'unknown') as model,
213
- count(*) as requests,
214
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
215
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
216
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
217
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
218
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
219
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost,
220
- date(max(m.time_created) / 1000, 'unixepoch', 'localtime') as day
221
- FROM message m
222
- JOIN session s ON s.id = m.session_id
223
- WHERE ${messageWhere(filters)}
224
- GROUP BY s.id, s.title, provider, model
225
- ORDER BY max(m.time_created) DESC
226
- LIMIT ${Math.max(1, limit)}
207
+ const sql = `
208
+ SELECT
209
+ s.id as session_id,
210
+ s.title as title,
211
+ coalesce(json_extract(m.data, '$.providerID'), json_extract(s.model, '$.providerID'), 'unknown') as provider,
212
+ coalesce(json_extract(m.data, '$.modelID'), json_extract(s.model, '$.id'), 'unknown') as model,
213
+ count(*) as requests,
214
+ sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
215
+ sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
216
+ sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
217
+ sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
218
+ sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
219
+ sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost,
220
+ date(max(m.time_created) / 1000, 'unixepoch', 'localtime') as day
221
+ FROM message m
222
+ JOIN session s ON s.id = m.session_id
223
+ WHERE ${messageWhere(filters)}
224
+ GROUP BY s.id, s.title, provider, model
225
+ ORDER BY max(m.time_created) DESC
226
+ LIMIT ${Math.max(1, limit)}
227
227
  `.trim();
228
228
  const rows = await queryDb(sql);
229
229
  return rows.map((row) => ({
@@ -242,21 +242,21 @@ LIMIT ${Math.max(1, limit)}
242
242
  }));
243
243
  }
244
244
  export async function getAvailableModels() {
245
- const sql = `
246
- SELECT distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown') as value
247
- FROM message m
248
- WHERE ${messageWhere({})}
249
- ORDER BY value ASC
245
+ const sql = `
246
+ SELECT distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown') as value
247
+ FROM message m
248
+ WHERE ${messageWhere({})}
249
+ ORDER BY value ASC
250
250
  `.trim();
251
251
  const rows = await queryDb(sql);
252
252
  return rows.map((row) => row.value ?? "unknown");
253
253
  }
254
254
  export async function getAvailableProviders() {
255
- const sql = `
256
- SELECT distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown') as value
257
- FROM message m
258
- WHERE ${messageWhere({})}
259
- ORDER BY value ASC
255
+ const sql = `
256
+ SELECT distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown') as value
257
+ FROM message m
258
+ WHERE ${messageWhere({})}
259
+ ORDER BY value ASC
260
260
  `.trim();
261
261
  const rows = await queryDb(sql);
262
262
  return rows.map((row) => row.value ?? "unknown");
@@ -281,16 +281,16 @@ export async function getErrorStats(filters = {}) {
281
281
  }
282
282
  const baseWhere = baseConds.join(" AND ");
283
283
  // 按模型细化:同时统计成功和失败请求
284
- const sql = `
285
- SELECT
286
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
287
- coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
288
- count(*) as total,
289
- sum(CASE WHEN coalesce(json_extract(m.data, '$.tokens.total'), 0) = 0 THEN 1 ELSE 0 END) as failed
290
- FROM message m
291
- WHERE ${baseWhere}
292
- GROUP BY provider, model
293
- ORDER BY failed DESC
284
+ const sql = `
285
+ SELECT
286
+ coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
287
+ coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
288
+ count(*) as total,
289
+ sum(CASE WHEN coalesce(json_extract(m.data, '$.tokens.total'), 0) = 0 THEN 1 ELSE 0 END) as failed
290
+ FROM message m
291
+ WHERE ${baseWhere}
292
+ GROUP BY provider, model
293
+ ORDER BY failed DESC
294
294
  `.trim();
295
295
  try {
296
296
  const rows = await queryDb(sql);
package/dist/sidebar.d.ts CHANGED
@@ -15,8 +15,8 @@ interface TokenWatchPanelProps {
15
15
  api: TuiPluginApi;
16
16
  theme: TuiTheme;
17
17
  perfTracker: PerfTracker;
18
- messages: readonly any[];
19
- allTokenMessages: TokenMessage[];
18
+ messages: () => readonly any[];
19
+ allTokenMessages: () => TokenMessage[];
20
20
  }
21
21
  export declare function TokenWatchPanel(props: TokenWatchPanelProps): any;
22
22
  export {};