opencode-tokenwatch 0.3.2 → 0.4.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/LICENSE +21 -21
- package/README.en.md +79 -107
- package/README.md +79 -107
- package/dist/commands.js +208 -0
- package/dist/generate-usage-html.js +801 -725
- package/dist/queries.js +101 -101
- package/dist/sidebar.jsx +138 -138
- package/package.json +63 -63
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
|
@@ -360,75 +360,75 @@ export function TokenWatchPanel(props) {
|
|
|
360
360
|
ref={(el) => { outerBoxRef = el; }} onSizeChange={() => {
|
|
361
361
|
if (outerBoxRef)
|
|
362
362
|
setPanelWidth(outerBoxRef.width);
|
|
363
|
-
}} flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
364
|
-
|
|
363
|
+
}} flexDirection="column" border={true} borderStyle="rounded" borderColor={borderColor()}>
|
|
364
|
+
|
|
365
365
|
{/* ══════════════════════════════════════
|
|
366
366
|
面板 Header:▾ TokenWatch 89.1% hit
|
|
367
367
|
justifyContent="space-between" 左右分布
|
|
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
|
-
|
|
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
|
+
|
|
390
390
|
{/* ══════════════════════════════════════
|
|
391
391
|
全局统计:Total / Req / Input / Output 均匀分行排布
|
|
392
|
-
══════════════════════════════════════ */}
|
|
393
|
-
<box flexDirection="row" paddingX={1}>
|
|
392
|
+
══════════════════════════════════════ */}
|
|
393
|
+
<box flexDirection="row" paddingX={1}>
|
|
394
394
|
<For each={[
|
|
395
395
|
{ val: formatTokens(sessionTotals().totalTokens), lbl: t("total") },
|
|
396
396
|
{ val: sessionTotals().totalRequests.toString(), lbl: t("requests") },
|
|
397
397
|
{ val: formatTokens(sessionTotals().totalInput), lbl: t("input") },
|
|
398
398
|
{ val: formatTokens(sessionTotals().totalOutput), lbl: t("output") }
|
|
399
|
-
]}>
|
|
399
|
+
]}>
|
|
400
400
|
{(item, idx) => {
|
|
401
401
|
const colW = () => {
|
|
402
402
|
const totalW = panelWidth() - 4;
|
|
403
403
|
const base = Math.floor(totalW / 4);
|
|
404
404
|
return idx() === 3 ? totalW - base * 3 : base;
|
|
405
405
|
};
|
|
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>
|
|
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>
|
|
411
411
|
</box>);
|
|
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
|
-
|
|
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
|
+
|
|
428
428
|
{/* ══════════════════════════════════════
|
|
429
429
|
各模型块:无分隔线,用 marginTop=1 隔开
|
|
430
|
-
══════════════════════════════════════ */}
|
|
431
|
-
<For each={modelStats()}>
|
|
430
|
+
══════════════════════════════════════ */}
|
|
431
|
+
<For each={modelStats()}>
|
|
432
432
|
{([key, stat]) => {
|
|
433
433
|
const isExpanded = () => collapse().models[key] !== true;
|
|
434
434
|
const hitDenom = stat.totalInput + stat.cacheRead;
|
|
@@ -496,109 +496,109 @@ export function TokenWatchPanel(props) {
|
|
|
496
496
|
const modelBarWidth = () => Math.max(8, (panelWidth() - 4) - targetW() - 11);
|
|
497
497
|
return (
|
|
498
498
|
// marginTop=1 提供模型间视觉间距(TUI最小单位为1行)
|
|
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">
|
|
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">
|
|
517
517
|
<For each={[
|
|
518
518
|
{ val: formatTokens(modelTotalTokens), lbl: t("total") },
|
|
519
519
|
{ val: formatTokens(stat.totalInput), lbl: t("input") },
|
|
520
520
|
{ val: formatTokens(stat.totalOutput), lbl: t("output") }
|
|
521
|
-
]}>
|
|
521
|
+
]}>
|
|
522
522
|
{(item, idx) => {
|
|
523
523
|
const colW = () => {
|
|
524
524
|
const totalW = panelWidth() - 6; // 边框占用 2 列
|
|
525
525
|
const base = Math.floor(totalW / 3);
|
|
526
526
|
return idx() === 2 ? totalW - base * 2 : base;
|
|
527
527
|
};
|
|
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>
|
|
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>
|
|
533
533
|
</box>);
|
|
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>
|
|
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>
|
|
545
545
|
{trendStr()
|
|
546
546
|
? <span style={{ fg: trendColor() }}>{trendStr()}</span>
|
|
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>
|
|
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>
|
|
566
566
|
</box>);
|
|
567
|
-
}}
|
|
568
|
-
</For>
|
|
569
|
-
|
|
567
|
+
}}
|
|
568
|
+
</For>
|
|
569
|
+
|
|
570
570
|
{/* ══════════════════════════════════════
|
|
571
571
|
Token 分布区块:左右 space-between 对齐布局 (取消进度条)
|
|
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>
|
|
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>
|
|
603
603
|
</box>);
|
|
604
604
|
}
|