token-stats-timer 1.0.16 → 1.0.18
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.md +2 -1
- package/package.json +1 -1
- package/token-stats.ts +172 -16
package/README.md
CHANGED
|
@@ -76,8 +76,9 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
76
76
|
|
|
77
77
|
## 命令
|
|
78
78
|
|
|
79
|
-
- `/stats` ——
|
|
79
|
+
- `/stats` —— 无参默认显示**当天 token 统计**(等价 `/stats day`)
|
|
80
80
|
- `/stats day [YYYY-MM-DD]` / `hour` / `week` / `month [YYYY-MM]` —— 统计查询
|
|
81
|
+
- `/stats limit` —— 套餐配置(为当前 provider 选择/关闭配额套餐;选 GLM 后会继续询问是否配置团队套餐凭证)
|
|
81
82
|
- `/stats config` —— 显示样式 / 显示内容 / 配额刷新时间 / GLM 团队凭证
|
|
82
83
|
- `/notify [on|off|test]` —— 通知开关 / 测试(无参查看状态)
|
|
83
84
|
|
package/package.json
CHANGED
package/token-stats.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
appendFile,
|
|
21
21
|
mkdir,
|
|
22
22
|
readFile,
|
|
23
|
+
readdir,
|
|
23
24
|
writeFile,
|
|
24
25
|
} from "node:fs/promises";
|
|
25
26
|
import { join } from "node:path";
|
|
@@ -195,7 +196,7 @@ type QuotaError =
|
|
|
195
196
|
* Token 格式化(对齐 @firstpick/pi-utils formatTokens)
|
|
196
197
|
*/
|
|
197
198
|
function formatTokens(count: number): string {
|
|
198
|
-
if (count < 1000) return count.
|
|
199
|
+
if (count < 1000) return count.toFixed(1);
|
|
199
200
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
200
201
|
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
201
202
|
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
@@ -249,6 +250,134 @@ function renderTable(
|
|
|
249
250
|
return lines;
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
// ── 按模型统计(读取 raw 明细,按 model 聚合)────────────────────
|
|
254
|
+
|
|
255
|
+
/** 读取若干日期的原始逐条记录 */
|
|
256
|
+
async function readRawRecordsForDates(dates: string[]): Promise<RawRecord[]> {
|
|
257
|
+
const out: RawRecord[] = [];
|
|
258
|
+
for (const d of dates) {
|
|
259
|
+
try {
|
|
260
|
+
const content = await readFile(join(RAW_DIR, `${d}.jsonl`), "utf-8");
|
|
261
|
+
for (const line of content.trim().split("\n")) {
|
|
262
|
+
if (line) out.push(JSON.parse(line));
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// 该日无原始数据
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 读取 [startDate, endDate] 闭区间内所有日期的原始记录 */
|
|
272
|
+
async function readRawRecordsInRange(
|
|
273
|
+
startDate: string,
|
|
274
|
+
endDate: string,
|
|
275
|
+
): Promise<RawRecord[]> {
|
|
276
|
+
const dates: string[] = [];
|
|
277
|
+
try {
|
|
278
|
+
const files = await readdir(RAW_DIR);
|
|
279
|
+
for (const f of files) {
|
|
280
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
281
|
+
const d = f.slice(0, 10);
|
|
282
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= startDate && d <= endDate) {
|
|
283
|
+
dates.push(d);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// RAW_DIR 不存在
|
|
288
|
+
}
|
|
289
|
+
return readRawRecordsForDates(dates);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface ModelAgg {
|
|
293
|
+
count: number;
|
|
294
|
+
input: number;
|
|
295
|
+
cacheRead: number;
|
|
296
|
+
cacheWrite: number;
|
|
297
|
+
output: number;
|
|
298
|
+
tokensPerSecSum: number;
|
|
299
|
+
hitRateSum: number;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 按模型生成 markdown 用量表(模型行按总token降序,末尾合计) */
|
|
303
|
+
function renderModelBreakdown(records: RawRecord[]): string[] {
|
|
304
|
+
if (records.length === 0) return ["", "> 按模型:该范围暂无明细数据"];
|
|
305
|
+
|
|
306
|
+
const byModel = new Map<string, ModelAgg>();
|
|
307
|
+
for (const r of records) {
|
|
308
|
+
const key = r.model || "unknown";
|
|
309
|
+
const agg = byModel.get(key) ?? {
|
|
310
|
+
count: 0,
|
|
311
|
+
input: 0,
|
|
312
|
+
cacheRead: 0,
|
|
313
|
+
cacheWrite: 0,
|
|
314
|
+
output: 0,
|
|
315
|
+
tokensPerSecSum: 0,
|
|
316
|
+
hitRateSum: 0,
|
|
317
|
+
};
|
|
318
|
+
agg.count++;
|
|
319
|
+
agg.input += r.input;
|
|
320
|
+
agg.cacheRead += r.cacheRead;
|
|
321
|
+
agg.cacheWrite += r.cacheWrite;
|
|
322
|
+
agg.output += r.output;
|
|
323
|
+
agg.tokensPerSecSum += r.tokensPerSec;
|
|
324
|
+
agg.hitRateSum += r.cacheHitRate;
|
|
325
|
+
byModel.set(key, agg);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 与汇总口径一致:总token = 新增输入 + 缓存输入
|
|
329
|
+
const totalTokensOf = (a: ModelAgg) => a.input + a.cacheRead + a.cacheWrite;
|
|
330
|
+
const rows = [...byModel.entries()].sort(
|
|
331
|
+
(a, b) => totalTokensOf(b[1]) - totalTokensOf(a[1]),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const total: ModelAgg = {
|
|
335
|
+
count: 0, input: 0, cacheRead: 0, cacheWrite: 0,
|
|
336
|
+
output: 0, tokensPerSecSum: 0, hitRateSum: 0,
|
|
337
|
+
};
|
|
338
|
+
const body: string[][] = rows.map(([model, agg]) => {
|
|
339
|
+
total.count += agg.count;
|
|
340
|
+
total.input += agg.input;
|
|
341
|
+
total.cacheRead += agg.cacheRead;
|
|
342
|
+
total.cacheWrite += agg.cacheWrite;
|
|
343
|
+
total.output += agg.output;
|
|
344
|
+
total.tokensPerSecSum += agg.tokensPerSecSum;
|
|
345
|
+
total.hitRateSum += agg.hitRateSum;
|
|
346
|
+
return [
|
|
347
|
+
model,
|
|
348
|
+
String(agg.count),
|
|
349
|
+
formatTokens(agg.input),
|
|
350
|
+
formatTokens(agg.cacheRead),
|
|
351
|
+
formatTokens(agg.output),
|
|
352
|
+
formatTokens(totalTokensOf(agg)),
|
|
353
|
+
`${(agg.count > 0 ? agg.hitRateSum / agg.count : 0).toFixed(1)}%`,
|
|
354
|
+
`${(agg.count > 0 ? agg.tokensPerSecSum / agg.count : 0).toFixed(1)}`,
|
|
355
|
+
];
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return [
|
|
359
|
+
"",
|
|
360
|
+
"**按模型**",
|
|
361
|
+
...renderTable(
|
|
362
|
+
["模型", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
|
|
363
|
+
body,
|
|
364
|
+
{
|
|
365
|
+
aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
|
|
366
|
+
totalRow: [
|
|
367
|
+
"合计",
|
|
368
|
+
String(total.count),
|
|
369
|
+
formatTokens(total.input),
|
|
370
|
+
formatTokens(total.cacheRead),
|
|
371
|
+
formatTokens(total.output),
|
|
372
|
+
formatTokens(total.input + total.cacheRead + total.cacheWrite),
|
|
373
|
+
`${(total.count > 0 ? total.hitRateSum / total.count : 0).toFixed(1)}%`,
|
|
374
|
+
`${(total.count > 0 ? total.tokensPerSecSum / total.count : 0).toFixed(1)}`,
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
),
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
252
381
|
function isReasonableTokenSpeed(tokensPerSecond: number): boolean {
|
|
253
382
|
return Number.isFinite(tokensPerSecond) && tokensPerSecond > 0 && tokensPerSecond <= MAX_REASONABLE_TOKEN_SPEED;
|
|
254
383
|
}
|
|
@@ -1478,7 +1607,7 @@ export function createTokenStats(
|
|
|
1478
1607
|
}
|
|
1479
1608
|
|
|
1480
1609
|
await showStats(
|
|
1481
|
-
renderDaySummary(daily),
|
|
1610
|
+
[...renderDaySummary(daily), ...renderModelBreakdown(await readRawRecordsForDates([date]))],
|
|
1482
1611
|
`Token 统计 | ${date}`,
|
|
1483
1612
|
ctx,
|
|
1484
1613
|
);
|
|
@@ -1503,16 +1632,23 @@ export function createTokenStats(
|
|
|
1503
1632
|
records.sort((a, b) => a.hour - b.hour);
|
|
1504
1633
|
|
|
1505
1634
|
const lines = renderTable(
|
|
1506
|
-
["时间", "次数", "
|
|
1507
|
-
records.map((r) =>
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1635
|
+
["时间", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
|
|
1636
|
+
records.map((r) => {
|
|
1637
|
+
const totalPrompt = r.sumInput + r.sumCacheRead + r.sumCacheWrite;
|
|
1638
|
+
return [
|
|
1639
|
+
String(r.hour).padStart(2, "0"),
|
|
1640
|
+
String(r.count),
|
|
1641
|
+
formatTokens(r.sumInput),
|
|
1642
|
+
formatTokens(r.sumCacheRead),
|
|
1643
|
+
formatTokens(r.sumOutput),
|
|
1644
|
+
formatTokens(totalPrompt),
|
|
1645
|
+
`${weightedCacheHitRate(r).toFixed(1)}%`,
|
|
1646
|
+
`${(r.sumTokensPerSec / r.count).toFixed(1)}`,
|
|
1647
|
+
];
|
|
1648
|
+
}),
|
|
1649
|
+
{
|
|
1650
|
+
aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
|
|
1651
|
+
},
|
|
1516
1652
|
);
|
|
1517
1653
|
|
|
1518
1654
|
await showStats(lines, `按小时分布 | ${date}`, ctx);
|
|
@@ -1562,7 +1698,11 @@ export function createTokenStats(
|
|
|
1562
1698
|
},
|
|
1563
1699
|
);
|
|
1564
1700
|
|
|
1565
|
-
await showStats(
|
|
1701
|
+
await showStats(
|
|
1702
|
+
[...lines, ...renderModelBreakdown(await readRawRecordsInRange(sevenDaysAgo, today))],
|
|
1703
|
+
"本周每天汇总",
|
|
1704
|
+
ctx,
|
|
1705
|
+
);
|
|
1566
1706
|
}
|
|
1567
1707
|
|
|
1568
1708
|
function getMonthStr(date: Date = new Date()): string {
|
|
@@ -1636,7 +1776,17 @@ export function createTokenStats(
|
|
|
1636
1776
|
},
|
|
1637
1777
|
);
|
|
1638
1778
|
|
|
1639
|
-
|
|
1779
|
+
const monthDates = monthRecords.map((r) => r.date).sort();
|
|
1780
|
+
await showStats(
|
|
1781
|
+
[
|
|
1782
|
+
...lines,
|
|
1783
|
+
...renderModelBreakdown(
|
|
1784
|
+
await readRawRecordsInRange(monthDates[0], monthDates[monthDates.length - 1]),
|
|
1785
|
+
),
|
|
1786
|
+
],
|
|
1787
|
+
`${month} 月度汇总`,
|
|
1788
|
+
ctx,
|
|
1789
|
+
);
|
|
1640
1790
|
}
|
|
1641
1791
|
|
|
1642
1792
|
// ── 事件注册 ─────────────────────────────────────────
|
|
@@ -1857,12 +2007,18 @@ export function createTokenStats(
|
|
|
1857
2007
|
// ── /stats 命令 ─────────────────────────────────────
|
|
1858
2008
|
|
|
1859
2009
|
pi.registerCommand("stats", {
|
|
1860
|
-
description: "Token 统计 (day | hour | week | month | config)
|
|
2010
|
+
description: "Token 统计 (day | hour | week | month | config | limit) 无参默认显示当天统计;limit 进入套餐配置",
|
|
1861
2011
|
handler: async (args, ctx) => {
|
|
1862
2012
|
const arg = args.trim();
|
|
1863
2013
|
|
|
1864
|
-
// 无参 →
|
|
2014
|
+
// 无参 → 当天统计(等价于 /stats day)
|
|
1865
2015
|
if (!arg) {
|
|
2016
|
+
await showDay(getDateStr(), ctx);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// limit → 套餐配置(原无参行为)
|
|
2021
|
+
if (arg === "limit") {
|
|
1866
2022
|
const provider = ctx.model?.provider;
|
|
1867
2023
|
if (!provider) {
|
|
1868
2024
|
ctx.ui.notify("无法获取当前供应商,请先切换对话", "warning");
|