opencode-tokenwatch 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +19 -11
- package/README.md +20 -12
- package/dist/commands.jsx +69 -4
- package/dist/formatter.d.ts +30 -0
- package/dist/generate-usage-html.js +210 -71
- package/dist/i18n.js +2 -4
- package/dist/perf-tracker.d.ts +6 -0
- package/dist/perf-tracker.js +122 -13
- package/dist/queries.d.ts +3 -1
- package/dist/queries.js +65 -6
- package/dist/sidebar.d.ts +2 -2
- package/dist/sidebar.jsx +321 -92
- package/dist/stats-store.d.ts +23 -0
- package/dist/stats-store.js +258 -0
- package/dist/tui.jsx +84 -27
- package/package.json +1 -1
|
@@ -33,12 +33,20 @@ function renderKpiCards(data) {
|
|
|
33
33
|
const s = data.summary;
|
|
34
34
|
const hitRate = cacheHitRate(s.inputTokens, s.cacheRead);
|
|
35
35
|
const hitRatePct = fmtPercent(hitRate);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
37
|
+
for (const p of data.perfSummary) {
|
|
38
|
+
if (p.avgTPS != null && p.avgTPS > 0) {
|
|
39
|
+
tpsSum += p.avgTPS * p.requestCount;
|
|
40
|
+
tpsReqs += p.requestCount;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const avgTpsRaw = tpsReqs > 0 ? tpsSum / tpsReqs : 0;
|
|
44
|
+
const avgTps = tpsReqs > 0 ? avgTpsRaw.toFixed(1) : '—';
|
|
41
45
|
const isHighCache = hitRate >= 0.5;
|
|
46
|
+
const errors = data.errors;
|
|
47
|
+
const errorRatePct = errors ? (errors.errorRate * 100).toFixed(1) + '%' : '—';
|
|
48
|
+
const errorColor = errors && errors.errorRate >= 0.05 ? 'var(--output)'
|
|
49
|
+
: errors && errors.errorRate > 0 ? 'var(--tps)' : 'var(--cache)';
|
|
42
50
|
return `
|
|
43
51
|
<div class="kpi-row">
|
|
44
52
|
<div class="kpi-card">
|
|
@@ -61,6 +69,10 @@ function renderKpiCards(data) {
|
|
|
61
69
|
<div class="kpi-label">Total Cost</div>
|
|
62
70
|
<div class="kpi-value" style="color:var(--tps)">${fmtCost(s.totalCost)}</div>
|
|
63
71
|
</div>
|
|
72
|
+
<div class="kpi-card">
|
|
73
|
+
<div class="kpi-label">Error Rate</div>
|
|
74
|
+
<div class="kpi-value" style="color:${errorColor}">${errorRatePct}</div>
|
|
75
|
+
</div>
|
|
64
76
|
</div>`;
|
|
65
77
|
}
|
|
66
78
|
function renderModelChartInit(data) {
|
|
@@ -189,18 +201,16 @@ function renderModelChart(chart) {
|
|
|
189
201
|
},
|
|
190
202
|
{
|
|
191
203
|
name: 'TPS',
|
|
192
|
-
type: '
|
|
204
|
+
type: 'scatter',
|
|
193
205
|
yAxisIndex: 1,
|
|
194
206
|
data: modelTps,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
symbolSize: 6,
|
|
198
|
-
lineStyle: { color: '#FFB800', width: 2 },
|
|
207
|
+
symbol: 'diamond',
|
|
208
|
+
symbolSize: function(val) { return val != null && val > 0 ? 13 : 0; },
|
|
199
209
|
itemStyle: { color: '#FFB800' },
|
|
200
210
|
label: {
|
|
201
211
|
show: true,
|
|
202
212
|
position: 'right',
|
|
203
|
-
formatter: function(p) { return p.value != null ? p.value.toFixed(1) : ''; },
|
|
213
|
+
formatter: function(p) { return p.value != null && p.value > 0 ? p.value.toFixed(1) : ''; },
|
|
204
214
|
color: '#FFB800', fontSize: 10
|
|
205
215
|
}
|
|
206
216
|
}
|
|
@@ -213,84 +223,112 @@ function renderModelChart(chart) {
|
|
|
213
223
|
function renderScatterChartInit(data) {
|
|
214
224
|
if (data.perfSummary.length === 0)
|
|
215
225
|
return "";
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
provider: p.providerID,
|
|
226
|
-
tps: p.avgTPS,
|
|
227
|
-
value: [p.avgTTFT != null && p.avgTTFT > 0 ? p.avgTTFT : 0.01, costPer1K, p.requestCount]
|
|
228
|
-
};
|
|
226
|
+
// 按 TPS 降序排列(null TPS 放末尾),让最快的模型显示在最上方
|
|
227
|
+
const sorted = [...data.perfSummary].sort((a, b) => {
|
|
228
|
+
if (a.avgTPS == null && b.avgTPS == null)
|
|
229
|
+
return 0;
|
|
230
|
+
if (a.avgTPS == null)
|
|
231
|
+
return 1;
|
|
232
|
+
if (b.avgTPS == null)
|
|
233
|
+
return -1;
|
|
234
|
+
return b.avgTPS - a.avgTPS;
|
|
229
235
|
});
|
|
236
|
+
const names = sorted.map(p => p.model);
|
|
237
|
+
const tpsValues = sorted.map(p => p.avgTPS ?? 0);
|
|
238
|
+
const ttftValues = sorted.map(p => p.avgTTFT ?? 0);
|
|
239
|
+
const costValues = sorted.map(p => {
|
|
240
|
+
const billable = p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite;
|
|
241
|
+
return billable > 0 ? (p.totalCost / billable) * 1000 : 0;
|
|
242
|
+
});
|
|
243
|
+
const hitRates = sorted.map(p => p.cacheHitRate ?? 0);
|
|
244
|
+
const reqCounts = sorted.map(p => p.requestCount);
|
|
230
245
|
return `
|
|
231
|
-
var
|
|
232
|
-
var
|
|
246
|
+
var effNames = ${JSON.stringify(names)};
|
|
247
|
+
var effTps = ${JSON.stringify(tpsValues)};
|
|
248
|
+
var effTtft = ${JSON.stringify(ttftValues)};
|
|
249
|
+
var effCost = ${JSON.stringify(costValues)};
|
|
250
|
+
var effHit = ${JSON.stringify(hitRates)};
|
|
251
|
+
var effReq = ${JSON.stringify(reqCounts)};
|
|
233
252
|
|
|
234
253
|
function initScatterChart() {
|
|
235
254
|
var el = document.getElementById('scatter-chart');
|
|
236
255
|
if (!el) return;
|
|
237
256
|
var chart = echarts.init(el);
|
|
238
257
|
window.scatterChart = chart;
|
|
258
|
+
|
|
259
|
+
// TPS 越高越绿,越低越紫,无数据为灰
|
|
260
|
+
var maxTps = Math.max.apply(null, effTps.filter(function(v){ return v > 0; })) || 1;
|
|
261
|
+
var barColors = effTps.map(function(v) {
|
|
262
|
+
if (v <= 0) return '#444455';
|
|
263
|
+
var r = v / maxTps;
|
|
264
|
+
if (r >= 0.8) return '#00F593';
|
|
265
|
+
if (r >= 0.5) return '#FFB800';
|
|
266
|
+
return '#B545FF';
|
|
267
|
+
});
|
|
268
|
+
|
|
239
269
|
var option = {
|
|
240
270
|
tooltip: {
|
|
271
|
+
trigger: 'axis',
|
|
272
|
+
axisPointer: { type: 'none' },
|
|
241
273
|
formatter: function(params) {
|
|
242
|
-
var
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'
|
|
274
|
+
var i = params[0].dataIndex;
|
|
275
|
+
var tps = effTps[i] > 0 ? effTps[i].toFixed(1) + ' tok/s' : '\u2014';
|
|
276
|
+
var ttft = effTtft[i] > 0 ? effTtft[i].toFixed(0) + ' ms' : '\u2014';
|
|
277
|
+
var cost = effCost[i] > 0 ? '$' + effCost[i].toFixed(4) + '/1K' : '\u2014';
|
|
278
|
+
var hit = effHit[i] > 0 ? effHit[i].toFixed(1) + '%' : '\u2014';
|
|
279
|
+
return '<b>' + effNames[i] + '</b><br/>' +
|
|
280
|
+
'\u25B6 TPS: ' + tps + '<br/>' +
|
|
281
|
+
'\u23F1 TTFT: ' + ttft + '<br/>' +
|
|
282
|
+
'\uD83D\uDCB0 Cost/1K: ' + cost + '<br/>' +
|
|
283
|
+
'\uD83D\uDCBE Cache Hit: ' + hit + '<br/>' +
|
|
284
|
+
'Requests: ' + effReq[i];
|
|
249
285
|
}
|
|
250
286
|
},
|
|
251
|
-
grid: { left:
|
|
287
|
+
grid: { left: 20, right: 280, bottom: 30, top: 20, containLabel: true },
|
|
252
288
|
xAxis: {
|
|
253
|
-
type: '
|
|
254
|
-
name: '
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
axisLabel: { color: '#B0B0C0' },
|
|
289
|
+
type: 'value',
|
|
290
|
+
name: 'Avg TPS (tokens / sec)',
|
|
291
|
+
nameTextStyle: { color: '#B0B0C0', fontSize: 11 },
|
|
292
|
+
axisLabel: { color: '#B0B0C0', formatter: function(v) { return v > 0 ? v.toFixed(0) : '0'; } },
|
|
258
293
|
splitLine: { lineStyle: { color: '#2A2A35', type: 'dashed' } }
|
|
259
294
|
},
|
|
260
295
|
yAxis: {
|
|
261
|
-
type: '
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
axisLabel: {
|
|
265
|
-
|
|
296
|
+
type: 'category',
|
|
297
|
+
data: effNames,
|
|
298
|
+
inverse: true,
|
|
299
|
+
axisLabel: {
|
|
300
|
+
color: '#E0E0F0',
|
|
301
|
+
fontSize: 11,
|
|
302
|
+
formatter: function(v) { return v.length > 50 ? v.slice(0, 48) + '\u2026' : v; }
|
|
303
|
+
},
|
|
304
|
+
axisLine: { show: false },
|
|
305
|
+
axisTick: { show: false }
|
|
266
306
|
},
|
|
267
307
|
series: [{
|
|
268
|
-
type: '
|
|
269
|
-
data:
|
|
270
|
-
return {
|
|
271
|
-
value: d.value,
|
|
272
|
-
name: d.name,
|
|
273
|
-
provider: d.provider,
|
|
274
|
-
tps: d.tps,
|
|
275
|
-
itemStyle: { color: providerColors[d.provider] || '#B545FF' }
|
|
276
|
-
};
|
|
308
|
+
type: 'bar',
|
|
309
|
+
data: effTps.map(function(v, i) {
|
|
310
|
+
return { value: v > 0 ? v : 0.001, itemStyle: { color: barColors[i], borderRadius: [0, 4, 4, 0] } };
|
|
277
311
|
}),
|
|
278
|
-
|
|
279
|
-
return Math.max(8, Math.min(40, Math.sqrt(val[2]) * 3));
|
|
280
|
-
},
|
|
281
|
-
itemStyle: { opacity: 0.8 },
|
|
312
|
+
barMaxWidth: 22,
|
|
282
313
|
label: {
|
|
283
314
|
show: true,
|
|
284
|
-
formatter: function(p) { return p.name; },
|
|
285
315
|
position: 'right',
|
|
286
|
-
color: '#
|
|
287
|
-
fontSize: 10
|
|
316
|
+
color: '#E0E0F0',
|
|
317
|
+
fontSize: 10,
|
|
318
|
+
formatter: function(p) {
|
|
319
|
+
var i = p.dataIndex;
|
|
320
|
+
var parts = [effTps[i] > 0 ? effTps[i].toFixed(1) + ' t/s' : '\u2014'];
|
|
321
|
+
if (effTtft[i] > 0) parts.push('TTFT ' + effTtft[i].toFixed(0) + 'ms');
|
|
322
|
+
if (effCost[i] > 0) parts.push('\u0024' + effCost[i].toFixed(4) + '/1K');
|
|
323
|
+
return parts.join(' ');
|
|
324
|
+
}
|
|
288
325
|
}
|
|
289
326
|
}]
|
|
290
327
|
};
|
|
291
328
|
chart.setOption(option);
|
|
292
329
|
chart.resize();
|
|
293
|
-
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
294
332
|
}
|
|
295
333
|
function providerBorderColor(provider) {
|
|
296
334
|
const colors = { opencode: "#00F593", deepseek: "#00D1FF", nvidia: "#B545FF", modelscope: "#FFB800", };
|
|
@@ -300,14 +338,22 @@ function renderProviderCards(data) {
|
|
|
300
338
|
return data.providers.map(p => {
|
|
301
339
|
const modelCount = data.models.filter(m => m.provider === p.provider).length;
|
|
302
340
|
const perfItems = data.perfSummary.filter(ps => ps.providerID === p.provider);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
341
|
+
let ttftSum = 0, ttftReqs = 0;
|
|
342
|
+
for (const x of perfItems) {
|
|
343
|
+
if (x.avgTTFT != null && x.avgTTFT > 0) {
|
|
344
|
+
ttftSum += x.avgTTFT * x.requestCount;
|
|
345
|
+
ttftReqs += x.requestCount;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const avgTtft = ttftReqs > 0 ? ttftSum / ttftReqs : null;
|
|
349
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
350
|
+
for (const x of perfItems) {
|
|
351
|
+
if (x.avgTPS != null && x.avgTPS > 0) {
|
|
352
|
+
tpsSum += x.avgTPS * x.requestCount;
|
|
353
|
+
tpsReqs += x.requestCount;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const avgTps = tpsReqs > 0 ? tpsSum / tpsReqs : null;
|
|
311
357
|
return `
|
|
312
358
|
<div class="provider-card" style="border-color:${providerBorderColor(p.provider)}">
|
|
313
359
|
<div class="provider-name">${p.provider}</div>
|
|
@@ -326,6 +372,7 @@ function renderDataTable(data) {
|
|
|
326
372
|
const hitColor = hitRate >= 0.85 ? 'var(--cache)' : hitRate >= 0.70 ? 'var(--tps)' : 'var(--output)';
|
|
327
373
|
const perf = data.perfSummary.find(p => p.model === `${m.provider}/${m.model}`);
|
|
328
374
|
const ttft = perf?.avgTTFT != null ? perf.avgTTFT.toFixed(0) + 'ms' : '—';
|
|
375
|
+
const p95ttft = perf?.p95TTFT != null ? perf.p95TTFT.toFixed(0) + 'ms' : '—';
|
|
329
376
|
const tps = perf?.avgTPS != null ? perf.avgTPS.toFixed(1) : '—';
|
|
330
377
|
return `<tr>
|
|
331
378
|
<td>${m.model}</td>
|
|
@@ -337,6 +384,7 @@ function renderDataTable(data) {
|
|
|
337
384
|
<td>${fmtTokens(m.cacheRead)}</td>
|
|
338
385
|
<td style="color:${hitColor};font-weight:600">${hitRatePct}</td>
|
|
339
386
|
<td>${ttft}</td>
|
|
387
|
+
<td style="color:var(--tps);font-size:0.85em">${p95ttft}</td>
|
|
340
388
|
<td>${tps}</td>
|
|
341
389
|
<td>${fmtCost(m.totalCost)}</td>
|
|
342
390
|
</tr>`;
|
|
@@ -352,7 +400,8 @@ function renderDataTable(data) {
|
|
|
352
400
|
<th>Output</th>
|
|
353
401
|
<th>Cache</th>
|
|
354
402
|
<th>Hit Rate</th>
|
|
355
|
-
<th>TTFT</th>
|
|
403
|
+
<th>Avg TTFT</th>
|
|
404
|
+
<th>P95 TTFT</th>
|
|
356
405
|
<th>TPS</th>
|
|
357
406
|
<th>Cost</th>
|
|
358
407
|
</tr>
|
|
@@ -360,6 +409,90 @@ function renderDataTable(data) {
|
|
|
360
409
|
<tbody>${rows}</tbody>
|
|
361
410
|
</table>`;
|
|
362
411
|
}
|
|
412
|
+
/** 渲染 P50/P95/P99 延迟分位数表格 */
|
|
413
|
+
function renderPerfPercentileTable(data) {
|
|
414
|
+
if (data.perfSummary.length === 0)
|
|
415
|
+
return '';
|
|
416
|
+
const fmtMs = (v) => v != null ? v.toFixed(0) + 'ms' : '—';
|
|
417
|
+
const rows = data.perfSummary.map(p => {
|
|
418
|
+
const hitColor = p.cacheHitRate != null && p.cacheHitRate >= 85 ? 'var(--cache)'
|
|
419
|
+
: p.cacheHitRate != null && p.cacheHitRate >= 70 ? 'var(--tps)' : 'var(--output)';
|
|
420
|
+
return `<tr>
|
|
421
|
+
<td>${p.model}</td>
|
|
422
|
+
<td>${p.requestCount}</td>
|
|
423
|
+
<td>${fmtMs(p.avgTTFT)}</td>
|
|
424
|
+
<td>${fmtMs(p.p50TTFT)}</td>
|
|
425
|
+
<td>${fmtMs(p.p95TTFT)}</td>
|
|
426
|
+
<td>${fmtMs(p.p99TTFT)}</td>
|
|
427
|
+
<td>${fmtMs(p.avgLatency)}</td>
|
|
428
|
+
<td>${fmtMs(p.p50Latency)}</td>
|
|
429
|
+
<td>${fmtMs(p.p95Latency)}</td>
|
|
430
|
+
<td>${fmtMs(p.p99Latency)}</td>
|
|
431
|
+
<td style="color:${hitColor};font-weight:600">${p.cacheHitRate != null ? p.cacheHitRate.toFixed(1) + '%' : '—'}</td>
|
|
432
|
+
</tr>`;
|
|
433
|
+
}).join("\n");
|
|
434
|
+
return `
|
|
435
|
+
<section class="section">
|
|
436
|
+
<h2 class="section-title">Performance Latency Percentiles</h2>
|
|
437
|
+
<table class="data-table">
|
|
438
|
+
<thead>
|
|
439
|
+
<tr>
|
|
440
|
+
<th>Model</th>
|
|
441
|
+
<th>Req</th>
|
|
442
|
+
<th>Avg TTFT</th>
|
|
443
|
+
<th>P50 TTFT</th>
|
|
444
|
+
<th>P95 TTFT</th>
|
|
445
|
+
<th>P99 TTFT</th>
|
|
446
|
+
<th>Avg E2E</th>
|
|
447
|
+
<th>P50 E2E</th>
|
|
448
|
+
<th>P95 E2E</th>
|
|
449
|
+
<th>P99 E2E</th>
|
|
450
|
+
<th>Cache Hit</th>
|
|
451
|
+
</tr>
|
|
452
|
+
</thead>
|
|
453
|
+
<tbody>${rows}</tbody>
|
|
454
|
+
</table>
|
|
455
|
+
</section>`;
|
|
456
|
+
}
|
|
457
|
+
/** 渲染失败请求统计区域 */
|
|
458
|
+
function renderErrorStatsSection(data) {
|
|
459
|
+
const errors = data.errors;
|
|
460
|
+
if (!errors || errors.failedCount === 0)
|
|
461
|
+
return '';
|
|
462
|
+
const errorRatePct = (errors.errorRate * 100).toFixed(2) + '%';
|
|
463
|
+
const rateColor = errors.errorRate >= 0.05 ? 'var(--output)'
|
|
464
|
+
: errors.errorRate > 0 ? 'var(--tps)' : 'var(--cache)';
|
|
465
|
+
const rows = errors.byModel
|
|
466
|
+
.filter(m => m.failed > 0)
|
|
467
|
+
.map(m => {
|
|
468
|
+
const modelRate = m.total > 0 ? (m.failed / m.total * 100).toFixed(1) + '%' : '—';
|
|
469
|
+
return `<tr>
|
|
470
|
+
<td>${m.provider}</td>
|
|
471
|
+
<td>${m.model}</td>
|
|
472
|
+
<td>${m.total}</td>
|
|
473
|
+
<td style="color:var(--output)">${m.failed}</td>
|
|
474
|
+
<td style="color:var(--tps)">${m.total - m.failed}</td>
|
|
475
|
+
<td style="color:${errors.errorRate >= 0.05 ? 'var(--output)' : 'var(--tps)'}">${modelRate}</td>
|
|
476
|
+
</tr>`;
|
|
477
|
+
}).join('\n');
|
|
478
|
+
return `
|
|
479
|
+
<section class="section">
|
|
480
|
+
<h2 class="section-title">Failed Requests
|
|
481
|
+
<span style="margin-left:12px;font-size:0.85em;color:${rateColor}">
|
|
482
|
+
overall error rate: ${errorRatePct} (${errors.failedCount} failed / ${errors.successCount + errors.failedCount} total)
|
|
483
|
+
</span>
|
|
484
|
+
</h2>
|
|
485
|
+
<table class="data-table">
|
|
486
|
+
<thead>
|
|
487
|
+
<tr>
|
|
488
|
+
<th>Provider</th><th>Model</th><th>Total</th>
|
|
489
|
+
<th>Failed</th><th>Success</th><th>Error Rate</th>
|
|
490
|
+
</tr>
|
|
491
|
+
</thead>
|
|
492
|
+
<tbody>${rows}</tbody>
|
|
493
|
+
</table>
|
|
494
|
+
</section>`;
|
|
495
|
+
}
|
|
363
496
|
function renderDailyTrendInit(data) {
|
|
364
497
|
const days = data.daily.slice().reverse().map(d => d.day);
|
|
365
498
|
const tokens = data.daily.slice().reverse().map(d => d.totalTokens);
|
|
@@ -511,6 +644,8 @@ export function generateUsageHtml(data) {
|
|
|
511
644
|
const scatterChartJs = renderScatterChartInit(data);
|
|
512
645
|
const providerStr = renderProviderCards(data);
|
|
513
646
|
const tableStr = renderDataTable(data);
|
|
647
|
+
const perfPercentileStr = renderPerfPercentileTable(data);
|
|
648
|
+
const errorStatsStr = renderErrorStatsSection(data);
|
|
514
649
|
const dailyChartJs = renderDailyTrendInit(data);
|
|
515
650
|
const heatmapJs = renderHeatmapInit(data);
|
|
516
651
|
const hasPerf = data.perfSummary.length > 0;
|
|
@@ -555,7 +690,7 @@ export function generateUsageHtml(data) {
|
|
|
555
690
|
.header h1 span { color: var(--input); }
|
|
556
691
|
.header .meta { font-size: 12px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
|
|
557
692
|
|
|
558
|
-
.kpi-row { display: grid; grid-template-columns: repeat(
|
|
693
|
+
.kpi-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-bottom: 24px; }
|
|
559
694
|
.kpi-card {
|
|
560
695
|
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
561
696
|
padding: 16px; text-align: center;
|
|
@@ -656,6 +791,10 @@ export function generateUsageHtml(data) {
|
|
|
656
791
|
${tableStr}
|
|
657
792
|
</div>
|
|
658
793
|
|
|
794
|
+
${perfPercentileStr}
|
|
795
|
+
|
|
796
|
+
${errorStatsStr}
|
|
797
|
+
|
|
659
798
|
<div class="section">
|
|
660
799
|
<div class="section-title">Efficiency vs Cost</div>
|
|
661
800
|
${hasPerf ? '<div class="chart-box" id="scatter-chart"></div>' : '<div class="empty-state">No performance data available for this period.</div>'}
|
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分布",
|
|
@@ -40,8 +41,6 @@ const zh = {
|
|
|
40
41
|
toolCall: "Tool调用",
|
|
41
42
|
toolResult: "Tool结果",
|
|
42
43
|
outputTokens: "输出",
|
|
43
|
-
settings: "Settings",
|
|
44
|
-
showCache: "显示缓存统计",
|
|
45
44
|
showPerformance: "显示性能指标",
|
|
46
45
|
showPricing: "显示模型定价",
|
|
47
46
|
showTokenDistribution: "显示Token分布",
|
|
@@ -88,6 +87,7 @@ const en = {
|
|
|
88
87
|
trendUp: "↑",
|
|
89
88
|
trendDown: "↓",
|
|
90
89
|
cache: "Cache",
|
|
90
|
+
lat: "Lat",
|
|
91
91
|
performance: "Performance",
|
|
92
92
|
pricing: "Pricing",
|
|
93
93
|
tokenDistribution: "Token Distribution",
|
|
@@ -114,8 +114,6 @@ const en = {
|
|
|
114
114
|
toolCall: "Tool Call",
|
|
115
115
|
toolResult: "Tool Result",
|
|
116
116
|
outputTokens: "Output",
|
|
117
|
-
settings: "Settings",
|
|
118
|
-
showCache: "Show Cache",
|
|
119
117
|
showPerformance: "Show Performance",
|
|
120
118
|
showPricing: "Show Pricing",
|
|
121
119
|
showTokenDistribution: "Show Token Distribution",
|
package/dist/perf-tracker.d.ts
CHANGED
|
@@ -41,14 +41,20 @@ interface MessageRemoveEvent {
|
|
|
41
41
|
declare class PerfTracker {
|
|
42
42
|
private firstPartTimes;
|
|
43
43
|
private statsMap;
|
|
44
|
+
/** 原始样本串,用于分位数计算,不持久化 */
|
|
45
|
+
private ttftSamples;
|
|
46
|
+
private latencySamples;
|
|
44
47
|
handlePartUpdated(event: PartEvent): void;
|
|
45
48
|
handleMessageUpdated(event: MessageUpdateEvent): void;
|
|
46
49
|
private appendLog;
|
|
47
50
|
handleMessageRemoved(event: MessageRemoveEvent): void;
|
|
48
51
|
private updateStats;
|
|
52
|
+
/** 计算有序数组的指定百分位数(线性插值法) */
|
|
53
|
+
private percentile;
|
|
49
54
|
getSessionStats(): SessionPerfStats;
|
|
50
55
|
readLogs(last?: number): LogEntry[];
|
|
51
56
|
reset(): void;
|
|
57
|
+
loadSession(sessionID: string): void;
|
|
52
58
|
}
|
|
53
59
|
export declare function createPerfTracker(): PerfTracker;
|
|
54
60
|
export type { PartEvent, PerfTracker };
|