opencode-tokenwatch 0.1.0 → 0.2.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/README.en.md +96 -0
- package/README.md +53 -60
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +208 -0
- package/dist/commands.jsx +313 -0
- package/dist/formatter.d.ts +73 -0
- package/dist/formatter.js +14 -3
- package/dist/generate-usage-html.d.ts +2 -0
- package/dist/generate-usage-html.js +739 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.js +175 -0
- package/dist/perf-tracker.d.ts +55 -0
- package/dist/perf-tracker.js +185 -0
- package/dist/sidebar.d.ts +22 -0
- package/dist/sidebar.jsx +362 -0
- package/dist/tui.d.ts +15 -1
- package/dist/tui.jsx +120 -0
- package/package.json +3 -3
- package/dist/tui.js +0 -406
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { getUsageReport, getPresetRange } from "./queries.js";
|
|
2
|
+
import { formatUsageReport } from "./formatter.js";
|
|
3
|
+
import { generateUsageHtml } from "./generate-usage-html.js";
|
|
4
|
+
import { t, setLanguage } from "./i18n.js";
|
|
5
|
+
import { readLogs } from "./perf-tracker.js";
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
sidebar: { showPerformance: true, showPricing: true, showTokenDistribution: true, showTrend: true },
|
|
12
|
+
language: "auto",
|
|
13
|
+
};
|
|
14
|
+
export async function registerCommands(api) {
|
|
15
|
+
api.command?.register(() => [
|
|
16
|
+
{
|
|
17
|
+
value: "tokenwatch-usage",
|
|
18
|
+
title: "TokenWatch",
|
|
19
|
+
description: "Token usage reports, export, and settings",
|
|
20
|
+
category: "Stats",
|
|
21
|
+
slash: { name: "usage" },
|
|
22
|
+
onSelect: async (dialog) => {
|
|
23
|
+
if (dialog)
|
|
24
|
+
showUsageMenu(api, dialog);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
function ensureReportDir() {
|
|
30
|
+
const dir = join(homedir(), ".opencode", "reports");
|
|
31
|
+
if (!existsSync(dir))
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
function openInBrowser(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
if (platform === "win32")
|
|
39
|
+
execSync(`start "" "${filePath}"`, { windowsHide: true, timeout: 5000 });
|
|
40
|
+
else if (platform === "darwin")
|
|
41
|
+
execSync(`open "${filePath}"`, { timeout: 5000 });
|
|
42
|
+
else
|
|
43
|
+
execSync(`xdg-open "${filePath}"`, { timeout: 5000 });
|
|
44
|
+
}
|
|
45
|
+
catch { /* silently fail */ }
|
|
46
|
+
}
|
|
47
|
+
function aggregatePerfStats(logs) {
|
|
48
|
+
const map = new Map();
|
|
49
|
+
for (const entry of logs) {
|
|
50
|
+
const key = entry.model;
|
|
51
|
+
let s = map.get(key);
|
|
52
|
+
if (!s) {
|
|
53
|
+
s = {
|
|
54
|
+
model: key,
|
|
55
|
+
providerID: entry.providerID,
|
|
56
|
+
requestCount: 0,
|
|
57
|
+
totalInput: 0, totalOutput: 0, totalCacheRead: 0, totalCacheWrite: 0, totalCost: 0,
|
|
58
|
+
avgTTFT: null, maxTTFT: null, minTTFT: null,
|
|
59
|
+
avgTPS: null, maxTPS: null, minTPS: null,
|
|
60
|
+
avgLatency: null, maxLatency: null, minLatency: null,
|
|
61
|
+
};
|
|
62
|
+
map.set(key, s);
|
|
63
|
+
}
|
|
64
|
+
s.requestCount++;
|
|
65
|
+
s.totalInput += entry.inputTokens;
|
|
66
|
+
s.totalOutput += entry.outputTokens;
|
|
67
|
+
s.totalCacheRead += entry.cacheReadTokens;
|
|
68
|
+
s.totalCacheWrite += entry.cacheWriteTokens;
|
|
69
|
+
s.totalCost += entry.cost;
|
|
70
|
+
const c = s.requestCount;
|
|
71
|
+
if (entry.ttft_ms != null) {
|
|
72
|
+
s.avgTTFT = s.avgTTFT != null ? s.avgTTFT + (entry.ttft_ms - s.avgTTFT) / c : entry.ttft_ms;
|
|
73
|
+
s.maxTTFT = s.maxTTFT != null ? Math.max(s.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
74
|
+
s.minTTFT = s.minTTFT != null ? Math.min(s.minTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
75
|
+
}
|
|
76
|
+
if (entry.tps != null) {
|
|
77
|
+
s.avgTPS = s.avgTPS != null ? s.avgTPS + (entry.tps - s.avgTPS) / c : entry.tps;
|
|
78
|
+
s.maxTPS = s.maxTPS != null ? Math.max(s.maxTPS, entry.tps) : entry.tps;
|
|
79
|
+
s.minTPS = s.minTPS != null ? Math.min(s.minTPS, entry.tps) : entry.tps;
|
|
80
|
+
}
|
|
81
|
+
if (entry.latency_ms != null) {
|
|
82
|
+
s.avgLatency = s.avgLatency != null ? s.avgLatency + (entry.latency_ms - s.avgLatency) / c : entry.latency_ms;
|
|
83
|
+
s.maxLatency = s.maxLatency != null ? Math.max(s.maxLatency, entry.latency_ms) : entry.latency_ms;
|
|
84
|
+
s.minLatency = s.minLatency != null ? Math.min(s.minLatency, entry.latency_ms) : entry.latency_ms;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Array.from(map.values());
|
|
88
|
+
}
|
|
89
|
+
async function buildCombinedData(api, filters = {}) {
|
|
90
|
+
const report = await getUsageReport(filters);
|
|
91
|
+
const logs = readLogs(1000);
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
94
|
+
const meta = {
|
|
95
|
+
generatedAt: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
|
96
|
+
dateRange: {
|
|
97
|
+
start: report.daily.length > 0 ? report.daily[report.daily.length - 1].day : "—",
|
|
98
|
+
end: report.daily.length > 0 ? report.daily[0].day : "—",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
...report,
|
|
103
|
+
perfLogs: logs,
|
|
104
|
+
perfSummary: aggregatePerfStats(logs),
|
|
105
|
+
meta,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function showHtmlReport(api, filters = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const data = await buildCombinedData(api, filters);
|
|
111
|
+
const html = generateUsageHtml(data);
|
|
112
|
+
const dir = ensureReportDir();
|
|
113
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
114
|
+
const filePath = join(dir, `tokenwatch-${dateStr}.html`);
|
|
115
|
+
writeFileSync(filePath, html, "utf-8");
|
|
116
|
+
api.ui.toast?.({ message: `Report: ${filePath}`, variant: "info" });
|
|
117
|
+
openInBrowser(filePath);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function showHtmlReportRangeMenu(api, dialog) {
|
|
125
|
+
dialog.replace(() => (<api.ui.DialogSelect title={t("cmdTitleHtml")} placeholder="Select date range..." options={[
|
|
126
|
+
{
|
|
127
|
+
title: t("menuToday"),
|
|
128
|
+
value: "today",
|
|
129
|
+
onSelect: () => {
|
|
130
|
+
dialog.clear();
|
|
131
|
+
const d = new Date();
|
|
132
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
133
|
+
const s = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
134
|
+
showHtmlReport(api, { startDate: s, endDate: s });
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: t("menu7d"),
|
|
139
|
+
value: "7d",
|
|
140
|
+
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("7d")); },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
title: t("menu30d"),
|
|
144
|
+
value: "30d",
|
|
145
|
+
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("30d")); },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
title: t("menuAll"),
|
|
149
|
+
value: "all",
|
|
150
|
+
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("all")); },
|
|
151
|
+
},
|
|
152
|
+
]} flat={true}/>));
|
|
153
|
+
}
|
|
154
|
+
function showUsageMenu(api, dialog) {
|
|
155
|
+
dialog.replace(() => (<api.ui.DialogSelect title={t("panelTitle")} placeholder="Select an action..." options={[
|
|
156
|
+
{
|
|
157
|
+
title: `${t("cmdTitleHtml")} ▸`,
|
|
158
|
+
value: "html",
|
|
159
|
+
description: t("cmdDescHtml"),
|
|
160
|
+
onSelect: () => showHtmlReportRangeMenu(api, dialog),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
title: t("cmdTitleJson"),
|
|
164
|
+
value: "json",
|
|
165
|
+
description: t("cmdDescJson"),
|
|
166
|
+
onSelect: () => { dialog.clear(); showJsonExport(api); },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
title: t("cmdTitleText"),
|
|
170
|
+
value: "text",
|
|
171
|
+
description: t("cmdDescText"),
|
|
172
|
+
onSelect: () => { dialog.clear(); showTextReport(api); },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
title: `${t("cmdTitleSettings")} ▸`,
|
|
176
|
+
value: "settings",
|
|
177
|
+
description: t("cmdDescSettings"),
|
|
178
|
+
onSelect: () => showSettingsDialog(api, dialog),
|
|
179
|
+
},
|
|
180
|
+
]} flat={true}/>));
|
|
181
|
+
}
|
|
182
|
+
async function showJsonExport(api) {
|
|
183
|
+
try {
|
|
184
|
+
const report = await getUsageReport({});
|
|
185
|
+
const dir = ensureReportDir();
|
|
186
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
187
|
+
const filePath = join(dir, `tokenwatch-${dateStr}.json`);
|
|
188
|
+
writeFileSync(filePath, JSON.stringify(report, null, 2), "utf-8");
|
|
189
|
+
api.ui.toast?.({ message: `JSON: ${filePath}`, variant: "info" });
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
193
|
+
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function showTextReport(api) {
|
|
197
|
+
try {
|
|
198
|
+
const report = await getUsageReport({});
|
|
199
|
+
const formatted = formatUsageReport(report);
|
|
200
|
+
const dir = ensureReportDir();
|
|
201
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
202
|
+
const filePath = join(dir, `tokenwatch-${dateStr}.md`);
|
|
203
|
+
writeFileSync(filePath, formatted, "utf-8");
|
|
204
|
+
api.ui.toast?.({ message: `Report saved to ${filePath}`, variant: "info" });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function saveConfigToStore(api, cfg) {
|
|
212
|
+
api.kv?.set?.("tokenwatch-config", cfg);
|
|
213
|
+
}
|
|
214
|
+
let lastSelectedSetting;
|
|
215
|
+
function showSettingsDialog(api, dialog) {
|
|
216
|
+
if (!dialog)
|
|
217
|
+
return;
|
|
218
|
+
const reopen = (value) => {
|
|
219
|
+
lastSelectedSetting = value;
|
|
220
|
+
setTimeout(() => showSettingsDialog(api, dialog), 0);
|
|
221
|
+
};
|
|
222
|
+
const cfg = loadConfigFromStore(api).sidebar;
|
|
223
|
+
dialog.replace(() => (<api.ui.DialogSelect title={t("settingsTitle")} placeholder={t("settingsPlaceholder")} options={[
|
|
224
|
+
{
|
|
225
|
+
title: `${cfg.showPerformance ? "✓ " : " "}${t("showPerformance")}`,
|
|
226
|
+
value: "showPerformance",
|
|
227
|
+
description: t("descShowPerformance"),
|
|
228
|
+
onSelect: () => { toggleSidebarSetting(api, "showPerformance"); reopen("showPerformance"); },
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
title: `${cfg.showPricing ? "✓ " : " "}${t("showPricing")}`,
|
|
232
|
+
value: "showPricing",
|
|
233
|
+
description: t("descShowPricing"),
|
|
234
|
+
onSelect: () => { toggleSidebarSetting(api, "showPricing"); reopen("showPricing"); },
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
title: `${cfg.showTokenDistribution ? "✓ " : " "}${t("showTokenDistribution")}`,
|
|
238
|
+
value: "showTokenDistribution",
|
|
239
|
+
description: t("descShowTokenDistribution"),
|
|
240
|
+
onSelect: () => { toggleSidebarSetting(api, "showTokenDistribution"); reopen("showTokenDistribution"); },
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
title: `${cfg.showTrend ? "✓ " : " "}${t("showTrend")}`,
|
|
244
|
+
value: "showTrend",
|
|
245
|
+
description: t("descShowTrend"),
|
|
246
|
+
onSelect: () => { toggleSidebarSetting(api, "showTrend"); reopen("showTrend"); },
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
title: `${t("settingsLanguage")} ▸`,
|
|
250
|
+
value: "language",
|
|
251
|
+
description: t("descSettingsLanguage"),
|
|
252
|
+
onSelect: () => showLanguageMenu(api, dialog),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
title: t("done"),
|
|
256
|
+
value: "done",
|
|
257
|
+
description: t("closeSettings"),
|
|
258
|
+
onSelect: () => { lastSelectedSetting = undefined; dialog.clear(); },
|
|
259
|
+
},
|
|
260
|
+
]} flat={true} current={lastSelectedSetting}/>));
|
|
261
|
+
}
|
|
262
|
+
function showLanguageMenu(api, dialog) {
|
|
263
|
+
const current = api.kv?.get?.("tokenwatch-config")?.language ?? "auto";
|
|
264
|
+
dialog.replace(() => (<api.ui.DialogSelect title={t("settingsLanguage")} placeholder={t("settingsLanguage")} options={[
|
|
265
|
+
{
|
|
266
|
+
title: `${current === "auto" ? "✓ " : " "}${t("langAuto")}`,
|
|
267
|
+
value: "auto",
|
|
268
|
+
description: "自动检测 / Auto-detect",
|
|
269
|
+
onSelect: () => { setLanguageSetting(api, "auto"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
title: `${current === "zh" ? "✓ " : " "}中文`,
|
|
273
|
+
value: "zh",
|
|
274
|
+
description: "简体中文",
|
|
275
|
+
onSelect: () => { setLanguageSetting(api, "zh"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
title: `${current === "en" ? "✓ " : " "}English`,
|
|
279
|
+
value: "en",
|
|
280
|
+
description: "English",
|
|
281
|
+
onSelect: () => { setLanguageSetting(api, "en"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
282
|
+
},
|
|
283
|
+
]} flat={true}/>));
|
|
284
|
+
}
|
|
285
|
+
function setLanguageSetting(api, lang) {
|
|
286
|
+
setLanguage(lang);
|
|
287
|
+
const current = loadConfigFromStore(api);
|
|
288
|
+
current.language = lang;
|
|
289
|
+
saveConfigToStore(api, current);
|
|
290
|
+
const v = (api.kv?.get?.("tokenwatch-config-version") ?? 0) + 1;
|
|
291
|
+
api.kv?.set?.("tokenwatch-config-version", v);
|
|
292
|
+
}
|
|
293
|
+
function toggleSidebarSetting(api, key) {
|
|
294
|
+
const current = loadConfigFromStore(api);
|
|
295
|
+
current.sidebar[key] = !current.sidebar[key];
|
|
296
|
+
saveConfigToStore(api, current);
|
|
297
|
+
const v = (api.kv?.get?.("tokenwatch-config-version") ?? 0) + 1;
|
|
298
|
+
api.kv?.set?.("tokenwatch-config-version", v);
|
|
299
|
+
}
|
|
300
|
+
function loadConfigFromStore(api) {
|
|
301
|
+
const base = { sidebar: { ...DEFAULT_CONFIG.sidebar }, language: DEFAULT_CONFIG.language };
|
|
302
|
+
try {
|
|
303
|
+
const stored = api.kv?.get?.("tokenwatch-config");
|
|
304
|
+
if (stored) {
|
|
305
|
+
if (stored.sidebar)
|
|
306
|
+
Object.assign(base.sidebar, stored.sidebar);
|
|
307
|
+
if (stored.language)
|
|
308
|
+
base.language = stored.language;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch { /* defaults */ }
|
|
312
|
+
return base;
|
|
313
|
+
}
|
package/dist/formatter.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export interface UsageReport {
|
|
|
77
77
|
}
|
|
78
78
|
export declare function formatTokens(n: number): string;
|
|
79
79
|
export declare function formatCost(n: number): string;
|
|
80
|
+
export declare function formatDuration(ms: number | null): string;
|
|
80
81
|
export declare function formatFilters(filters: UsageFilters): string;
|
|
81
82
|
export declare function formatSessionSummary(data: SessionTokenData, title?: string): string;
|
|
82
83
|
export declare function formatStatusBar(data: SessionTokenData): string;
|
|
@@ -85,3 +86,75 @@ export declare function formatProviderBreakdown(items: ProviderBreakdownItem[]):
|
|
|
85
86
|
export declare function formatDailyBreakdown(items: DailyBreakdownItem[]): string;
|
|
86
87
|
export declare function formatSessionBreakdown(items: SessionBreakdownItem[]): string;
|
|
87
88
|
export declare function formatUsageReport(report: UsageReport): string;
|
|
89
|
+
export interface SessionPerfStats {
|
|
90
|
+
models: Record<string, ModelPerfStats>;
|
|
91
|
+
totals: {
|
|
92
|
+
totalInput: number;
|
|
93
|
+
totalOutput: number;
|
|
94
|
+
totalCacheRead: number;
|
|
95
|
+
totalCacheWrite: number;
|
|
96
|
+
totalRequests: number;
|
|
97
|
+
totalCost: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export interface ModelPerfStats {
|
|
101
|
+
model: string;
|
|
102
|
+
providerID: string;
|
|
103
|
+
requestCount: number;
|
|
104
|
+
totalInput: number;
|
|
105
|
+
totalOutput: number;
|
|
106
|
+
totalCacheRead: number;
|
|
107
|
+
totalCacheWrite: number;
|
|
108
|
+
totalCost: number;
|
|
109
|
+
avgTTFT: number | null;
|
|
110
|
+
maxTTFT: number | null;
|
|
111
|
+
minTTFT: number | null;
|
|
112
|
+
avgTPS: number | null;
|
|
113
|
+
maxTPS: number | null;
|
|
114
|
+
minTPS: number | null;
|
|
115
|
+
avgLatency: number | null;
|
|
116
|
+
maxLatency: number | null;
|
|
117
|
+
minLatency: number | null;
|
|
118
|
+
}
|
|
119
|
+
export interface TokenDistribution {
|
|
120
|
+
system: number;
|
|
121
|
+
user: number;
|
|
122
|
+
agent: number;
|
|
123
|
+
toolCall: number;
|
|
124
|
+
toolResult: number;
|
|
125
|
+
output: number;
|
|
126
|
+
total: number;
|
|
127
|
+
}
|
|
128
|
+
export interface LogEntry {
|
|
129
|
+
ts: string;
|
|
130
|
+
model: string;
|
|
131
|
+
providerID: string;
|
|
132
|
+
modelID: string;
|
|
133
|
+
sessionID: string;
|
|
134
|
+
ttft_ms: number | null;
|
|
135
|
+
tps: number | null;
|
|
136
|
+
latency_ms: number | null;
|
|
137
|
+
inputTokens: number;
|
|
138
|
+
outputTokens: number;
|
|
139
|
+
reasoningTokens: number;
|
|
140
|
+
cacheReadTokens: number;
|
|
141
|
+
cacheWriteTokens: number;
|
|
142
|
+
cost: number;
|
|
143
|
+
}
|
|
144
|
+
export interface HtmlReportMeta {
|
|
145
|
+
generatedAt: string;
|
|
146
|
+
dateRange: {
|
|
147
|
+
start: string;
|
|
148
|
+
end: string;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export interface CombinedReportData {
|
|
152
|
+
summary: SessionTokenData;
|
|
153
|
+
models: ModelBreakdownItem[];
|
|
154
|
+
providers: ProviderBreakdownItem[];
|
|
155
|
+
daily: DailyBreakdownItem[];
|
|
156
|
+
sessions: SessionBreakdownItem[];
|
|
157
|
+
perfLogs: LogEntry[];
|
|
158
|
+
perfSummary: ModelPerfStats[];
|
|
159
|
+
meta: HtmlReportMeta;
|
|
160
|
+
}
|
package/dist/formatter.js
CHANGED
|
@@ -14,6 +14,17 @@ export function formatCost(n) {
|
|
|
14
14
|
return `$${n.toFixed(4)}`;
|
|
15
15
|
return `$${n.toFixed(2)}`;
|
|
16
16
|
}
|
|
17
|
+
export function formatDuration(ms) {
|
|
18
|
+
if (ms === null)
|
|
19
|
+
return "—";
|
|
20
|
+
if (ms < 1000)
|
|
21
|
+
return `${ms.toFixed(0)}ms`;
|
|
22
|
+
if (ms < 60000)
|
|
23
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
24
|
+
const m = Math.floor(ms / 60000);
|
|
25
|
+
const s = Math.floor((ms % 60000) / 1000);
|
|
26
|
+
return `${m}m ${s}s`;
|
|
27
|
+
}
|
|
17
28
|
export function formatFilters(filters) {
|
|
18
29
|
const parts = [];
|
|
19
30
|
if (filters.sessionId)
|
|
@@ -87,13 +98,13 @@ export function formatSessionSummary(data, title = "Current Session") {
|
|
|
87
98
|
`═══ ${title} ═══`,
|
|
88
99
|
`Models: ${modelLabel}`,
|
|
89
100
|
`Provider: ${data.provider || "(mixed)"}`,
|
|
90
|
-
`
|
|
101
|
+
`Req: ${data.requestCount}`,
|
|
91
102
|
`Total Tokens: ${formatTokens(data.totalTokens)}`,
|
|
92
103
|
` Input: ${formatTokens(data.inputTokens)}`,
|
|
93
104
|
` Output: ${formatTokens(data.outputTokens)}`,
|
|
94
105
|
` Reasoning: ${formatTokens(data.reasoningTokens)}`,
|
|
95
|
-
`
|
|
96
|
-
`
|
|
106
|
+
` C.Read: ${formatTokens(data.cacheRead)}`,
|
|
107
|
+
` C.Write: ${formatTokens(data.cacheWrite)}`,
|
|
97
108
|
` Cost: ${formatCost(data.totalCost)}`,
|
|
98
109
|
].join("\n");
|
|
99
110
|
}
|