opencode-tokenwatch 0.3.1 → 0.3.2
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/LICENSE +21 -21
- package/README.en.md +107 -104
- package/README.md +107 -104
- package/dist/commands.jsx +3 -0
- package/dist/generate-usage-html.js +708 -700
- package/dist/perf-tracker.js +5 -0
- package/dist/queries.js +101 -101
- package/dist/sidebar.jsx +156 -143
- package/dist/tui.jsx +2 -1
- package/package.json +63 -63
- package/dist/commands.js +0 -208
package/dist/perf-tracker.js
CHANGED
|
@@ -43,6 +43,11 @@ class PerfTracker {
|
|
|
43
43
|
const cacheRead = tokens?.cache?.read ?? 0;
|
|
44
44
|
const cacheWrite = tokens?.cache?.write ?? 0;
|
|
45
45
|
const cost = info.cost ?? 0;
|
|
46
|
+
// 过滤全零 token 的失败请求,不写入日志和统计,防止污染数据
|
|
47
|
+
if (inputTokens + outputTokens + cacheRead + cacheWrite === 0) {
|
|
48
|
+
this.firstPartTimes.delete(messageID);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
46
51
|
const firstPart = this.firstPartTimes.get(messageID) ?? null;
|
|
47
52
|
const latencyMs = completed - created;
|
|
48
53
|
const ttftMs = firstPart !== null ? firstPart - created : null;
|
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.jsx
CHANGED
|
@@ -149,11 +149,13 @@ export function TokenWatchPanel(props) {
|
|
|
149
149
|
// ── 数据聚合 ──
|
|
150
150
|
const modelStats = createMemo(() => {
|
|
151
151
|
const map = new Map();
|
|
152
|
-
|
|
152
|
+
const msgs = props.allTokenMessages();
|
|
153
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
154
|
+
const msg = msgs[i];
|
|
153
155
|
const key = `${msg.providerID}/${msg.modelID}`;
|
|
154
156
|
let e = map.get(key);
|
|
155
157
|
if (!e) {
|
|
156
|
-
e = { providerID: msg.providerID, modelID: msg.modelID, totalInput: 0, totalOutput: 0, totalReasoning: 0, cacheRead: 0, cacheWrite: 0, totalCost: 0, requestCount: 0 };
|
|
158
|
+
e = { providerID: msg.providerID, modelID: msg.modelID, totalInput: 0, totalOutput: 0, totalReasoning: 0, cacheRead: 0, cacheWrite: 0, totalCost: 0, requestCount: 0, lastMessageIndex: -1 };
|
|
157
159
|
map.set(key, e);
|
|
158
160
|
}
|
|
159
161
|
e.totalInput += msg.inputTokens;
|
|
@@ -163,8 +165,13 @@ export function TokenWatchPanel(props) {
|
|
|
163
165
|
e.cacheWrite += msg.cacheWrite;
|
|
164
166
|
e.totalCost += msg.cost;
|
|
165
167
|
e.requestCount++;
|
|
168
|
+
e.lastMessageIndex = i; // 追踪该模型最后一条消息的索引(越大 = 越近调用)
|
|
166
169
|
}
|
|
167
|
-
return Array.from(map.entries())
|
|
170
|
+
return Array.from(map.entries())
|
|
171
|
+
// 过滤掉全零无效模型(如会话失败导致 token 全为 0 的记录)
|
|
172
|
+
.filter(([, s]) => s.totalInput + s.totalOutput + s.totalReasoning + s.cacheRead + s.cacheWrite > 0)
|
|
173
|
+
// 按最近调用时间降序:最后一条消息在数组中索引越大越靠前
|
|
174
|
+
.sort((a, b) => b[1].lastMessageIndex - a[1].lastMessageIndex);
|
|
168
175
|
});
|
|
169
176
|
const sessionTotals = createMemo(() => {
|
|
170
177
|
let i = 0, o = 0, ir = 0, cr = 0, cw = 0, r = 0, c = 0;
|
|
@@ -353,75 +360,75 @@ export function TokenWatchPanel(props) {
|
|
|
353
360
|
ref={(el) => { outerBoxRef = el; }} onSizeChange={() => {
|
|
354
361
|
if (outerBoxRef)
|
|
355
362
|
setPanelWidth(outerBoxRef.width);
|
|
356
|
-
}} flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
357
|
-
|
|
363
|
+
}} flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
364
|
+
|
|
358
365
|
{/* ══════════════════════════════════════
|
|
359
366
|
面板 Header:▾ TokenWatch 89.1% hit
|
|
360
367
|
justifyContent="space-between" 左右分布
|
|
361
|
-
══════════════════════════════════════ */}
|
|
362
|
-
<box flexDirection="row" justifyContent="space-between" onMouseDown={toggle.global} paddingX={1}>
|
|
363
|
-
<text fg={primaryColor()}>
|
|
364
|
-
{collapse().global ? "▶" : "▾"} {t("panelTitle")}
|
|
365
|
-
</text>
|
|
366
|
-
<text fg={mutedColor()}>
|
|
367
|
-
{collapse().global ? (<>
|
|
368
|
-
{formatTokens(sessionTotals().totalTokens)}
|
|
369
|
-
{globalHitRate() >= 0 ? (<span style={{ fg: hitRateColor(globalHitRate()) }}>
|
|
370
|
-
{` (${globalHitRate().toFixed(1)}% hit)`}
|
|
371
|
-
</span>) : ""}
|
|
372
|
-
</>) : (globalHitRate() >= 0 ? (<span style={{ fg: hitRateColor(globalHitRate()) }}>
|
|
373
|
-
{`${globalHitRate().toFixed(1)}% hit`}
|
|
374
|
-
</span>) : "")}
|
|
375
|
-
</text>
|
|
376
|
-
</box>
|
|
377
|
-
|
|
378
|
-
<Show when={!collapse().global}>
|
|
379
|
-
|
|
380
|
-
{/* 标题下分隔线 */}
|
|
381
|
-
<text fg={borderColor()}>{divider()}</text>
|
|
382
|
-
|
|
368
|
+
══════════════════════════════════════ */}
|
|
369
|
+
<box flexDirection="row" justifyContent="space-between" onMouseDown={toggle.global} paddingX={1}>
|
|
370
|
+
<text fg={primaryColor()}>
|
|
371
|
+
{collapse().global ? "▶" : "▾"} {t("panelTitle")}
|
|
372
|
+
</text>
|
|
373
|
+
<text fg={mutedColor()}>
|
|
374
|
+
{collapse().global ? (<>
|
|
375
|
+
{formatTokens(sessionTotals().totalTokens)}
|
|
376
|
+
{globalHitRate() >= 0 ? (<span style={{ fg: hitRateColor(globalHitRate()) }}>
|
|
377
|
+
{` (${globalHitRate().toFixed(1)}% hit)`}
|
|
378
|
+
</span>) : ""}
|
|
379
|
+
</>) : (globalHitRate() >= 0 ? (<span style={{ fg: hitRateColor(globalHitRate()) }}>
|
|
380
|
+
{`${globalHitRate().toFixed(1)}% hit`}
|
|
381
|
+
</span>) : "")}
|
|
382
|
+
</text>
|
|
383
|
+
</box>
|
|
384
|
+
|
|
385
|
+
<Show when={!collapse().global}>
|
|
386
|
+
|
|
387
|
+
{/* 标题下分隔线 */}
|
|
388
|
+
<text fg={borderColor()}>{divider()}</text>
|
|
389
|
+
|
|
383
390
|
{/* ══════════════════════════════════════
|
|
384
391
|
全局统计:Total / Req / Input / Output 均匀分行排布
|
|
385
|
-
══════════════════════════════════════ */}
|
|
386
|
-
<box flexDirection="row" paddingX={1}>
|
|
392
|
+
══════════════════════════════════════ */}
|
|
393
|
+
<box flexDirection="row" paddingX={1}>
|
|
387
394
|
<For each={[
|
|
388
395
|
{ val: formatTokens(sessionTotals().totalTokens), lbl: t("total") },
|
|
389
396
|
{ val: sessionTotals().totalRequests.toString(), lbl: t("requests") },
|
|
390
397
|
{ val: formatTokens(sessionTotals().totalInput), lbl: t("input") },
|
|
391
398
|
{ val: formatTokens(sessionTotals().totalOutput), lbl: t("output") }
|
|
392
|
-
]}>
|
|
399
|
+
]}>
|
|
393
400
|
{(item, idx) => {
|
|
394
401
|
const colW = () => {
|
|
395
402
|
const totalW = panelWidth() - 4;
|
|
396
403
|
const base = Math.floor(totalW / 4);
|
|
397
404
|
return idx() === 3 ? totalW - base * 3 : base;
|
|
398
405
|
};
|
|
399
|
-
return (<box width={colW()} flexDirection="column">
|
|
400
|
-
<text fg={primaryColor()}>{centerAlign(item.val, colW())}</text>
|
|
401
|
-
<text fg={dimColor()}>
|
|
402
|
-
{centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW())}
|
|
403
|
-
</text>
|
|
406
|
+
return (<box width={colW()} flexDirection="column">
|
|
407
|
+
<text fg={primaryColor()}>{centerAlign(item.val, colW())}</text>
|
|
408
|
+
<text fg={dimColor()}>
|
|
409
|
+
{centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW())}
|
|
410
|
+
</text>
|
|
404
411
|
</box>);
|
|
405
|
-
}}
|
|
406
|
-
</For>
|
|
407
|
-
</box>
|
|
408
|
-
|
|
409
|
-
{/* 成本展示 */}
|
|
410
|
-
<Show when={config().sidebar.showPricing && sessionTotals().totalCost > 0}>
|
|
411
|
-
<box flexDirection="row" justifyContent="center" marginTop={1}>
|
|
412
|
-
<text fg={mutedColor()}>
|
|
413
|
-
{t("cost")}:{" "}
|
|
414
|
-
<span style={{ fg: greenColor() }}>
|
|
415
|
-
{formatCost(sessionTotals().totalCost)}
|
|
416
|
-
</span>
|
|
417
|
-
</text>
|
|
418
|
-
</box>
|
|
419
|
-
</Show>
|
|
420
|
-
|
|
412
|
+
}}
|
|
413
|
+
</For>
|
|
414
|
+
</box>
|
|
415
|
+
|
|
416
|
+
{/* 成本展示 */}
|
|
417
|
+
<Show when={config().sidebar.showPricing && sessionTotals().totalCost > 0}>
|
|
418
|
+
<box flexDirection="row" justifyContent="center" marginTop={1}>
|
|
419
|
+
<text fg={mutedColor()}>
|
|
420
|
+
{t("cost")}:{" "}
|
|
421
|
+
<span style={{ fg: greenColor() }}>
|
|
422
|
+
{formatCost(sessionTotals().totalCost)}
|
|
423
|
+
</span>
|
|
424
|
+
</text>
|
|
425
|
+
</box>
|
|
426
|
+
</Show>
|
|
427
|
+
|
|
421
428
|
{/* ══════════════════════════════════════
|
|
422
429
|
各模型块:无分隔线,用 marginTop=1 隔开
|
|
423
|
-
══════════════════════════════════════ */}
|
|
424
|
-
<For each={modelStats()}>
|
|
430
|
+
══════════════════════════════════════ */}
|
|
431
|
+
<For each={modelStats()}>
|
|
425
432
|
{([key, stat]) => {
|
|
426
433
|
const isExpanded = () => collapse().models[key] !== true;
|
|
427
434
|
const hitDenom = stat.totalInput + stat.cacheRead;
|
|
@@ -442,8 +449,14 @@ export function TokenWatchPanel(props) {
|
|
|
442
449
|
? RGBA.fromInts(63, 185, 80, 255)
|
|
443
450
|
: RGBA.fromInts(244, 67, 54, 255);
|
|
444
451
|
};
|
|
445
|
-
//
|
|
446
|
-
|
|
452
|
+
// 供应商名称截断:超过12字符则省略
|
|
453
|
+
const MAX_PROVIDER_LEN = 12;
|
|
454
|
+
let providerDisplay = stat.providerID;
|
|
455
|
+
if (providerDisplay.length > MAX_PROVIDER_LEN) {
|
|
456
|
+
providerDisplay = providerDisplay.slice(0, MAX_PROVIDER_LEN - 1) + "…";
|
|
457
|
+
}
|
|
458
|
+
let fullTitle = `${providerDisplay}/${stat.modelID}`;
|
|
459
|
+
// 去除中间段:格式为 厂商/模型厂商/模型名称 时只留 provider/modelname
|
|
447
460
|
if (fullTitle.length > 22) {
|
|
448
461
|
const parts = fullTitle.split("/");
|
|
449
462
|
if (parts.length >= 3) {
|
|
@@ -483,109 +496,109 @@ export function TokenWatchPanel(props) {
|
|
|
483
496
|
const modelBarWidth = () => Math.max(8, (panelWidth() - 4) - targetW() - 11);
|
|
484
497
|
return (
|
|
485
498
|
// marginTop=1 提供模型间视觉间距(TUI最小单位为1行)
|
|
486
|
-
<box flexDirection="column" marginTop={1}>
|
|
487
|
-
|
|
488
|
-
{/* 模型 Header:左侧 ● 名称,右侧 统计+箭头 */}
|
|
489
|
-
<box flexDirection="row" justifyContent="space-between" onMouseDown={() => toggle.model(key)} paddingX={1}>
|
|
490
|
-
<text fg={mutedColor()}>
|
|
491
|
-
<span style={{ fg: hitRateColor(hitRate) }}>●</span>
|
|
492
|
-
{" "}
|
|
493
|
-
<span style={{ fg: primaryColor() }}>{shortTitle}</span>
|
|
494
|
-
</text>
|
|
495
|
-
<text fg={mutedColor()}>{modelHeaderRight()}</text>
|
|
496
|
-
</box>
|
|
497
|
-
|
|
498
|
-
<Show when={isExpanded()}>
|
|
499
|
-
<box flexDirection="column" paddingX={1}>
|
|
500
|
-
|
|
501
|
-
{/* 模型指标三列网格,外带圆角边框 (取消上下间距) */}
|
|
502
|
-
<box flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
503
|
-
<box flexDirection="row">
|
|
499
|
+
<box flexDirection="column" marginTop={1}>
|
|
500
|
+
|
|
501
|
+
{/* 模型 Header:左侧 ● 名称,右侧 统计+箭头 */}
|
|
502
|
+
<box flexDirection="row" justifyContent="space-between" onMouseDown={() => toggle.model(key)} paddingX={1}>
|
|
503
|
+
<text fg={mutedColor()}>
|
|
504
|
+
<span style={{ fg: hitRateColor(hitRate) }}>●</span>
|
|
505
|
+
{" "}
|
|
506
|
+
<span style={{ fg: primaryColor() }}>{shortTitle}</span>
|
|
507
|
+
</text>
|
|
508
|
+
<text fg={mutedColor()}>{modelHeaderRight()}</text>
|
|
509
|
+
</box>
|
|
510
|
+
|
|
511
|
+
<Show when={isExpanded()}>
|
|
512
|
+
<box flexDirection="column" paddingX={1}>
|
|
513
|
+
|
|
514
|
+
{/* 模型指标三列网格,外带圆角边框 (取消上下间距) */}
|
|
515
|
+
<box flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
516
|
+
<box flexDirection="row">
|
|
504
517
|
<For each={[
|
|
505
518
|
{ val: formatTokens(modelTotalTokens), lbl: t("total") },
|
|
506
519
|
{ val: formatTokens(stat.totalInput), lbl: t("input") },
|
|
507
520
|
{ val: formatTokens(stat.totalOutput), lbl: t("output") }
|
|
508
|
-
]}>
|
|
521
|
+
]}>
|
|
509
522
|
{(item, idx) => {
|
|
510
523
|
const colW = () => {
|
|
511
524
|
const totalW = panelWidth() - 6; // 边框占用 2 列
|
|
512
525
|
const base = Math.floor(totalW / 3);
|
|
513
526
|
return idx() === 2 ? totalW - base * 2 : base;
|
|
514
527
|
};
|
|
515
|
-
return (<box width={colW()} flexDirection="column">
|
|
516
|
-
<text fg={primaryColor()}>{centerAlign(item.val, colW())}</text>
|
|
517
|
-
<text fg={dimColor()}>
|
|
518
|
-
{centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW())}
|
|
519
|
-
</text>
|
|
528
|
+
return (<box width={colW()} flexDirection="column">
|
|
529
|
+
<text fg={primaryColor()}>{centerAlign(item.val, colW())}</text>
|
|
530
|
+
<text fg={dimColor()}>
|
|
531
|
+
{centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW())}
|
|
532
|
+
</text>
|
|
520
533
|
</box>);
|
|
521
|
-
}}
|
|
522
|
-
</For>
|
|
523
|
-
</box>
|
|
524
|
-
</box>
|
|
525
|
-
|
|
526
|
-
{/* 缓存进度条 */}
|
|
527
|
-
<text fg={mutedColor()}>
|
|
528
|
-
{paddedCachePrefix()}
|
|
529
|
-
<span style={{ fg: hitRateColor(hitRate) }}>
|
|
530
|
-
{progressFilled(hitRate, modelBarWidth())}{progressRemaining(hitRate, modelBarWidth())}{" "}{hitRate.toFixed(0)}%
|
|
531
|
-
</span>
|
|
534
|
+
}}
|
|
535
|
+
</For>
|
|
536
|
+
</box>
|
|
537
|
+
</box>
|
|
538
|
+
|
|
539
|
+
{/* 缓存进度条 */}
|
|
540
|
+
<text fg={mutedColor()}>
|
|
541
|
+
{paddedCachePrefix()}
|
|
542
|
+
<span style={{ fg: hitRateColor(hitRate) }}>
|
|
543
|
+
{progressFilled(hitRate, modelBarWidth())}{progressRemaining(hitRate, modelBarWidth())}{" "}{hitRate.toFixed(0)}%
|
|
544
|
+
</span>
|
|
532
545
|
{trendStr()
|
|
533
546
|
? <span style={{ fg: trendColor() }}>{trendStr()}</span>
|
|
534
|
-
: null}
|
|
535
|
-
</text>
|
|
536
|
-
|
|
537
|
-
{/* 性能指标 */}
|
|
538
|
-
<Show when={config().sidebar.showPerformance && !!perfStats().models[key]}>
|
|
539
|
-
<text fg={mutedColor()} marginTop={1}>
|
|
540
|
-
{t("ttft")} <span style={{ fg: primaryColor() }}>{formatDuration(perfStats().models[key]?.avgTTFT ?? null)}</span>
|
|
541
|
-
{" "}{t("tps")} <span style={{ fg: primaryColor() }}>{perfStats().models[key]?.avgTPS?.toFixed(1) ?? "—"}</span>
|
|
542
|
-
{" "}{t("lat")} <span style={{ fg: primaryColor() }}>{formatDuration(perfStats().models[key]?.avgLatency ?? null)}</span>
|
|
543
|
-
</text>
|
|
544
|
-
</Show>
|
|
545
|
-
|
|
546
|
-
{/* 成本 */}
|
|
547
|
-
<Show when={config().sidebar.showPricing && stat.totalCost > 0}>
|
|
548
|
-
<text fg={mutedColor()}>{paddedCostPrefix()}{formatCost(stat.totalCost)}</text>
|
|
549
|
-
</Show>
|
|
550
|
-
|
|
551
|
-
</box>
|
|
552
|
-
</Show>
|
|
547
|
+
: null}
|
|
548
|
+
</text>
|
|
549
|
+
|
|
550
|
+
{/* 性能指标 */}
|
|
551
|
+
<Show when={config().sidebar.showPerformance && !!perfStats().models[key]}>
|
|
552
|
+
<text fg={mutedColor()} marginTop={1}>
|
|
553
|
+
{t("ttft")} <span style={{ fg: primaryColor() }}>{formatDuration(perfStats().models[key]?.avgTTFT ?? null)}</span>
|
|
554
|
+
{" "}{t("tps")} <span style={{ fg: primaryColor() }}>{perfStats().models[key]?.avgTPS?.toFixed(1) ?? "—"}</span>
|
|
555
|
+
{" "}{t("lat")} <span style={{ fg: primaryColor() }}>{formatDuration(perfStats().models[key]?.avgLatency ?? null)}</span>
|
|
556
|
+
</text>
|
|
557
|
+
</Show>
|
|
558
|
+
|
|
559
|
+
{/* 成本 */}
|
|
560
|
+
<Show when={config().sidebar.showPricing && stat.totalCost > 0}>
|
|
561
|
+
<text fg={mutedColor()}>{paddedCostPrefix()}{formatCost(stat.totalCost)}</text>
|
|
562
|
+
</Show>
|
|
563
|
+
|
|
564
|
+
</box>
|
|
565
|
+
</Show>
|
|
553
566
|
</box>);
|
|
554
|
-
}}
|
|
555
|
-
</For>
|
|
556
|
-
|
|
567
|
+
}}
|
|
568
|
+
</For>
|
|
569
|
+
|
|
557
570
|
{/* ══════════════════════════════════════
|
|
558
571
|
Token 分布区块:左右 space-between 对齐布局 (取消进度条)
|
|
559
|
-
══════════════════════════════════════ */}
|
|
560
|
-
<Show when={config().sidebar.showTokenDistribution}>
|
|
561
|
-
<box flexDirection="column" marginTop={1}>
|
|
562
|
-
|
|
563
|
-
{/* 分隔线 */}
|
|
564
|
-
<text fg={borderColor()}>{divider()}</text>
|
|
565
|
-
|
|
566
|
-
{/* Header */}
|
|
567
|
-
<box flexDirection="row" onMouseDown={() => toggle.sub("token-dist")} paddingX={1}>
|
|
568
|
-
<text fg={greenColor()}>
|
|
569
|
-
{!collapse().subBlocks["token-dist"] ? "▾" : "▶"} {t("tokenDistribution")}
|
|
570
|
-
</text>
|
|
571
|
-
</box>
|
|
572
|
-
|
|
573
|
-
<Show when={!collapse().subBlocks["token-dist"]}>
|
|
574
|
-
<box flexDirection="column" paddingX={1} marginTop={1}>
|
|
575
|
-
<For each={Object.entries(tokenDistribution()).filter(([_, val]) => val > 0)}>
|
|
576
|
-
{([role, val]) => (<box flexDirection="row" justifyContent="space-between">
|
|
577
|
-
<box flexDirection="row">
|
|
578
|
-
<text fg={distRoleColor(role)}>█ </text>
|
|
579
|
-
<text fg={mutedColor()}>{t(role)}</text>
|
|
580
|
-
</box>
|
|
581
|
-
<text fg={mutedColor()}>{formatTokens(val)}</text>
|
|
582
|
-
</box>)}
|
|
583
|
-
</For>
|
|
584
|
-
</box>
|
|
585
|
-
</Show>
|
|
586
|
-
</box>
|
|
587
|
-
</Show>
|
|
588
|
-
|
|
589
|
-
</Show>
|
|
572
|
+
══════════════════════════════════════ */}
|
|
573
|
+
<Show when={config().sidebar.showTokenDistribution}>
|
|
574
|
+
<box flexDirection="column" marginTop={1}>
|
|
575
|
+
|
|
576
|
+
{/* 分隔线 */}
|
|
577
|
+
<text fg={borderColor()}>{divider()}</text>
|
|
578
|
+
|
|
579
|
+
{/* Header */}
|
|
580
|
+
<box flexDirection="row" onMouseDown={() => toggle.sub("token-dist")} paddingX={1}>
|
|
581
|
+
<text fg={greenColor()}>
|
|
582
|
+
{!collapse().subBlocks["token-dist"] ? "▾" : "▶"} {t("tokenDistribution")}
|
|
583
|
+
</text>
|
|
584
|
+
</box>
|
|
585
|
+
|
|
586
|
+
<Show when={!collapse().subBlocks["token-dist"]}>
|
|
587
|
+
<box flexDirection="column" paddingX={1} marginTop={1}>
|
|
588
|
+
<For each={Object.entries(tokenDistribution()).filter(([_, val]) => val > 0)}>
|
|
589
|
+
{([role, val]) => (<box flexDirection="row" justifyContent="space-between">
|
|
590
|
+
<box flexDirection="row">
|
|
591
|
+
<text fg={distRoleColor(role)}>█ </text>
|
|
592
|
+
<text fg={mutedColor()}>{t(role)}</text>
|
|
593
|
+
</box>
|
|
594
|
+
<text fg={mutedColor()}>{formatTokens(val)}</text>
|
|
595
|
+
</box>)}
|
|
596
|
+
</For>
|
|
597
|
+
</box>
|
|
598
|
+
</Show>
|
|
599
|
+
</box>
|
|
600
|
+
</Show>
|
|
601
|
+
|
|
602
|
+
</Show>
|
|
590
603
|
</box>);
|
|
591
604
|
}
|
package/dist/tui.jsx
CHANGED
|
@@ -108,7 +108,8 @@ const tui = async (api) => {
|
|
|
108
108
|
if (msg.role !== "assistant")
|
|
109
109
|
continue;
|
|
110
110
|
const tokens = msg.tokens;
|
|
111
|
-
|
|
111
|
+
// 与 message.updated 处理器保持一致:过滤 total=0 的失败请求
|
|
112
|
+
if (!tokens || (tokens.total ?? 0) === 0)
|
|
112
113
|
continue;
|
|
113
114
|
const id = msg.id;
|
|
114
115
|
const idx = next.findIndex(m => m.id === id);
|