token-stats-timer 1.0.14 → 1.0.15
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/package.json +1 -1
- package/token-stats.ts +113 -69
package/package.json
CHANGED
package/token-stats.ts
CHANGED
|
@@ -13,8 +13,9 @@ import type {
|
|
|
13
13
|
ExtensionContext,
|
|
14
14
|
Theme,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
17
|
-
import {
|
|
18
|
+
import { Markdown } from "@earendil-works/pi-tui";
|
|
18
19
|
import {
|
|
19
20
|
appendFile,
|
|
20
21
|
mkdir,
|
|
@@ -212,6 +213,41 @@ function formatTokenSpeed(tokensPerSecond: number): string {
|
|
|
212
213
|
return `${Math.round(tokensPerSecond / 1000000)}M`;
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
/**
|
|
217
|
+
* 生成 GitHub 风格 markdown 表格源码(由 Markdown 组件负责渲染对齐/换行)。
|
|
218
|
+
*
|
|
219
|
+
* @param headers 表头
|
|
220
|
+
* @param rows 数据行
|
|
221
|
+
* @param opts.aligns 每列对齐方式,缺省左对齐
|
|
222
|
+
* @param opts.totalRow 底部合计行
|
|
223
|
+
*/
|
|
224
|
+
function renderTable(
|
|
225
|
+
headers: string[],
|
|
226
|
+
rows: string[][],
|
|
227
|
+
opts?: {
|
|
228
|
+
aligns?: Array<"left" | "right" | "center">;
|
|
229
|
+
totalRow?: string[];
|
|
230
|
+
},
|
|
231
|
+
): string[] {
|
|
232
|
+
const aligns = opts?.aligns ?? [];
|
|
233
|
+
// 转义单元格内的 | 与换行,防止破坏表格结构
|
|
234
|
+
const esc = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
235
|
+
const alignMark = (i: number) => {
|
|
236
|
+
const a = aligns[i] ?? "left";
|
|
237
|
+
return a === "right" ? "---:" : a === "center" ? ":---:" : "---";
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const lines = [
|
|
241
|
+
`| ${headers.map(esc).join(" | ")} |`,
|
|
242
|
+
`| ${headers.map((_, i) => alignMark(i)).join(" | ")} |`,
|
|
243
|
+
...rows.map((r) => `| ${r.map(esc).join(" | ")} |`),
|
|
244
|
+
];
|
|
245
|
+
if (opts?.totalRow) {
|
|
246
|
+
lines.push(`| ${opts.totalRow.map(esc).join(" | ")} |`);
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
|
|
215
251
|
function isReasonableTokenSpeed(tokensPerSecond: number): boolean {
|
|
216
252
|
return Number.isFinite(tokensPerSecond) && tokensPerSecond > 0 && tokensPerSecond <= MAX_REASONABLE_TOKEN_SPEED;
|
|
217
253
|
}
|
|
@@ -1317,23 +1353,25 @@ export function createTokenStats(
|
|
|
1317
1353
|
return total > 0 ? (d.sumCacheRead / total) * 100 : 0;
|
|
1318
1354
|
}
|
|
1319
1355
|
|
|
1320
|
-
function renderDaySummary(daily: DailyRecord): string {
|
|
1356
|
+
function renderDaySummary(daily: DailyRecord): string[] {
|
|
1321
1357
|
const d = daily;
|
|
1322
1358
|
const avgInput = d.count > 0 ? d.sumInput / d.count : 0;
|
|
1323
1359
|
const avgOutput = d.count > 0 ? d.sumOutput / d.count : 0;
|
|
1324
1360
|
const totalPrompt = d.sumInput + d.sumCacheRead + d.sumCacheWrite;
|
|
1325
1361
|
const cacheHitRate = weightedCacheHitRate(d);
|
|
1326
1362
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1363
|
+
return renderTable(
|
|
1364
|
+
["指标", "数值"],
|
|
1365
|
+
[
|
|
1366
|
+
["对话次数", String(d.count)],
|
|
1367
|
+
["新增输入", `${formatTokens(d.sumInput)}(平均 ${formatTokens(avgInput)}/次,未命中缓存)`],
|
|
1368
|
+
["缓存输入", formatTokens(d.sumCacheRead)],
|
|
1369
|
+
["总输出", `${formatTokens(d.sumOutput)}(平均 ${formatTokens(avgOutput)}/次)`],
|
|
1370
|
+
["总token", `${formatTokens(totalPrompt)}(新增 + 缓存)`],
|
|
1371
|
+
["缓存命中率", `${cacheHitRate.toFixed(1)}%`],
|
|
1372
|
+
["平均速率", `${(d.sumTokensPerSec / d.count).toFixed(1)} t/s`],
|
|
1373
|
+
],
|
|
1374
|
+
);
|
|
1337
1375
|
}
|
|
1338
1376
|
|
|
1339
1377
|
async function showStats(
|
|
@@ -1341,9 +1379,7 @@ export function createTokenStats(
|
|
|
1341
1379
|
title: string,
|
|
1342
1380
|
ctx: ExtensionContext,
|
|
1343
1381
|
) {
|
|
1344
|
-
const
|
|
1345
|
-
const text = `${theme.fg("accent", theme.bold(title))}\n${theme.fg("dim", "─".repeat(42))}\n` +
|
|
1346
|
-
lines.map((l) => theme.fg("dim", l)).join("\n");
|
|
1382
|
+
const text = `## ${title}\n\n${lines.join("\n")}`;
|
|
1347
1383
|
pi.sendMessage({
|
|
1348
1384
|
customType: "token-stats",
|
|
1349
1385
|
content: text,
|
|
@@ -1369,7 +1405,7 @@ export function createTokenStats(
|
|
|
1369
1405
|
}
|
|
1370
1406
|
|
|
1371
1407
|
await showStats(
|
|
1372
|
-
renderDaySummary(daily)
|
|
1408
|
+
renderDaySummary(daily),
|
|
1373
1409
|
`Token 统计 | ${date}`,
|
|
1374
1410
|
ctx,
|
|
1375
1411
|
);
|
|
@@ -1393,18 +1429,18 @@ export function createTokenStats(
|
|
|
1393
1429
|
|
|
1394
1430
|
records.sort((a, b) => a.hour - b.hour);
|
|
1395
1431
|
|
|
1396
|
-
const lines =
|
|
1397
|
-
"
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
`${
|
|
1404
|
-
`${
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1432
|
+
const lines = renderTable(
|
|
1433
|
+
["时间", "次数", "输入", "输出", "命中率", "速率"],
|
|
1434
|
+
records.map((r) => [
|
|
1435
|
+
String(r.hour).padStart(2, "0"),
|
|
1436
|
+
String(r.count),
|
|
1437
|
+
formatTokens(r.sumInput),
|
|
1438
|
+
formatTokens(r.sumOutput),
|
|
1439
|
+
`${weightedCacheHitRate(r).toFixed(1)}%`,
|
|
1440
|
+
`${(r.sumTokensPerSec / r.count).toFixed(1)}`,
|
|
1441
|
+
]),
|
|
1442
|
+
{ aligns: ["left", "right", "right", "right", "right", "right"] },
|
|
1443
|
+
);
|
|
1408
1444
|
|
|
1409
1445
|
await showStats(lines, `按小时分布 | ${date}`, ctx);
|
|
1410
1446
|
}
|
|
@@ -1433,23 +1469,25 @@ export function createTokenStats(
|
|
|
1433
1469
|
return;
|
|
1434
1470
|
}
|
|
1435
1471
|
|
|
1436
|
-
const lines =
|
|
1437
|
-
"日期
|
|
1438
|
-
|
|
1439
|
-
...weekRecords.map((r) => {
|
|
1472
|
+
const lines = renderTable(
|
|
1473
|
+
["日期", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
|
|
1474
|
+
weekRecords.map((r) => {
|
|
1440
1475
|
const totalPrompt = r.sumInput + r.sumCacheRead + r.sumCacheWrite;
|
|
1441
|
-
return
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
`${weightedCacheHitRate(r).toFixed(1)
|
|
1449
|
-
`${(r.sumTokensPerSec / r.count).toFixed(1)
|
|
1450
|
-
|
|
1476
|
+
return [
|
|
1477
|
+
r.date,
|
|
1478
|
+
String(r.count),
|
|
1479
|
+
formatTokens(r.sumInput),
|
|
1480
|
+
formatTokens(r.sumCacheRead),
|
|
1481
|
+
formatTokens(r.sumOutput),
|
|
1482
|
+
formatTokens(totalPrompt),
|
|
1483
|
+
`${weightedCacheHitRate(r).toFixed(1)}%`,
|
|
1484
|
+
`${(r.sumTokensPerSec / r.count).toFixed(1)}`,
|
|
1485
|
+
];
|
|
1451
1486
|
}),
|
|
1452
|
-
|
|
1487
|
+
{
|
|
1488
|
+
aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
|
|
1489
|
+
},
|
|
1490
|
+
);
|
|
1453
1491
|
|
|
1454
1492
|
await showStats(lines, "本周每天汇总", ctx);
|
|
1455
1493
|
}
|
|
@@ -1495,40 +1533,46 @@ export function createTokenStats(
|
|
|
1495
1533
|
const totalPrompt = total.sumInput + total.sumCacheRead + total.sumCacheWrite;
|
|
1496
1534
|
const cacheHitRate = weightedCacheHitRate(total);
|
|
1497
1535
|
|
|
1498
|
-
const lines =
|
|
1499
|
-
"日期
|
|
1500
|
-
|
|
1501
|
-
...monthRecords.map((r) => {
|
|
1536
|
+
const lines = renderTable(
|
|
1537
|
+
["日期", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
|
|
1538
|
+
monthRecords.map((r) => {
|
|
1502
1539
|
const tp = r.sumInput + r.sumCacheRead + r.sumCacheWrite;
|
|
1503
|
-
return
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
`${weightedCacheHitRate(r).toFixed(1)
|
|
1511
|
-
`${(r.sumTokensPerSec / r.count).toFixed(1)
|
|
1512
|
-
|
|
1540
|
+
return [
|
|
1541
|
+
r.date,
|
|
1542
|
+
String(r.count),
|
|
1543
|
+
formatTokens(r.sumInput),
|
|
1544
|
+
formatTokens(r.sumCacheRead),
|
|
1545
|
+
formatTokens(r.sumOutput),
|
|
1546
|
+
formatTokens(tp),
|
|
1547
|
+
`${weightedCacheHitRate(r).toFixed(1)}%`,
|
|
1548
|
+
`${(r.sumTokensPerSec / r.count).toFixed(1)}`,
|
|
1549
|
+
];
|
|
1513
1550
|
}),
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1551
|
+
{
|
|
1552
|
+
aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
|
|
1553
|
+
totalRow: [
|
|
1554
|
+
"合计",
|
|
1555
|
+
String(total.count),
|
|
1556
|
+
formatTokens(total.sumInput),
|
|
1557
|
+
formatTokens(total.sumCacheRead),
|
|
1558
|
+
formatTokens(total.sumOutput),
|
|
1559
|
+
formatTokens(totalPrompt),
|
|
1560
|
+
`${cacheHitRate.toFixed(1)}%`,
|
|
1561
|
+
`${(total.sumTokensPerSec / total.count).toFixed(1)}`,
|
|
1562
|
+
],
|
|
1563
|
+
},
|
|
1564
|
+
);
|
|
1523
1565
|
|
|
1524
1566
|
await showStats(lines, `${month} 月度汇总`, ctx);
|
|
1525
1567
|
}
|
|
1526
1568
|
|
|
1527
1569
|
// ── 事件注册 ─────────────────────────────────────────
|
|
1528
1570
|
|
|
1529
|
-
// ── message renderer: 渲染 /stats
|
|
1530
|
-
pi.registerMessageRenderer("token-stats", (message, _options,
|
|
1531
|
-
return new
|
|
1571
|
+
// ── message renderer: 渲染 /stats 发出的消息(markdown 表格)──
|
|
1572
|
+
pi.registerMessageRenderer("token-stats", (message, _options, theme) => {
|
|
1573
|
+
return new Markdown(message.content, 0, 0, getMarkdownTheme(), {
|
|
1574
|
+
color: (text) => theme.fg("dim", text),
|
|
1575
|
+
});
|
|
1532
1576
|
});
|
|
1533
1577
|
|
|
1534
1578
|
// ── turn_start: 记录时间 + 检测供应商切换 ──────────
|