tokenleak 0.3.0 → 0.4.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/package.json +1 -1
- package/tokenleak.js +259 -361
package/package.json
CHANGED
package/tokenleak.js
CHANGED
|
@@ -422,18 +422,29 @@ var DEFAULT_DAYS = 90;
|
|
|
422
422
|
var DEFAULT_CONCURRENCY = 3;
|
|
423
423
|
var MAX_JSONL_RECORD_BYTES = 10 * 1024 * 1024;
|
|
424
424
|
var SCHEMA_VERSION = 1;
|
|
425
|
+
// packages/core/dist/date-utils.js
|
|
426
|
+
var ONE_DAY_MS = 86400000;
|
|
427
|
+
function dateToUtcMs(dateString) {
|
|
428
|
+
return new Date(dateString + "T00:00:00Z").getTime();
|
|
429
|
+
}
|
|
430
|
+
function formatDateStringUtc(date) {
|
|
431
|
+
return date.toISOString().slice(0, 10);
|
|
432
|
+
}
|
|
433
|
+
function compareDateStrings(a, b) {
|
|
434
|
+
return dateToUtcMs(a) - dateToUtcMs(b);
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
// packages/core/dist/aggregation/streaks.js
|
|
426
438
|
function calculateStreaks(daily) {
|
|
427
439
|
if (daily.length === 0) {
|
|
428
440
|
return { current: 0, longest: 0 };
|
|
429
441
|
}
|
|
430
|
-
const sorted = [...daily].sort((a, b) =>
|
|
431
|
-
const ONE_DAY_MS = 86400000;
|
|
442
|
+
const sorted = [...daily].sort((a, b) => dateToUtcMs(a.date) - dateToUtcMs(b.date));
|
|
432
443
|
let longest = 1;
|
|
433
444
|
let currentRun = 1;
|
|
434
445
|
for (let i = 1;i < sorted.length; i++) {
|
|
435
|
-
const prev =
|
|
436
|
-
const curr =
|
|
446
|
+
const prev = dateToUtcMs(sorted[i - 1].date);
|
|
447
|
+
const curr = dateToUtcMs(sorted[i].date);
|
|
437
448
|
const diff = curr - prev;
|
|
438
449
|
if (diff === ONE_DAY_MS) {
|
|
439
450
|
currentRun++;
|
|
@@ -446,8 +457,8 @@ function calculateStreaks(daily) {
|
|
|
446
457
|
}
|
|
447
458
|
let current = 1;
|
|
448
459
|
for (let i = sorted.length - 1;i > 0; i--) {
|
|
449
|
-
const curr =
|
|
450
|
-
const prev =
|
|
460
|
+
const curr = dateToUtcMs(sorted[i].date);
|
|
461
|
+
const prev = dateToUtcMs(sorted[i - 1].date);
|
|
451
462
|
if (curr - prev === ONE_DAY_MS) {
|
|
452
463
|
current++;
|
|
453
464
|
} else {
|
|
@@ -461,13 +472,12 @@ function rollingWindow(daily, days, referenceDate) {
|
|
|
461
472
|
if (daily.length === 0 || days <= 0) {
|
|
462
473
|
return { tokens: 0, cost: 0 };
|
|
463
474
|
}
|
|
464
|
-
const refTime =
|
|
465
|
-
const ONE_DAY_MS = 86400000;
|
|
475
|
+
const refTime = dateToUtcMs(referenceDate);
|
|
466
476
|
const windowStart = refTime - (days - 1) * ONE_DAY_MS;
|
|
467
477
|
let tokens = 0;
|
|
468
478
|
let cost = 0;
|
|
469
479
|
for (const entry of daily) {
|
|
470
|
-
const entryTime =
|
|
480
|
+
const entryTime = dateToUtcMs(entry.date);
|
|
471
481
|
if (entryTime >= windowStart && entryTime <= refTime) {
|
|
472
482
|
tokens += entry.totalTokens;
|
|
473
483
|
cost += entry.cost;
|
|
@@ -507,7 +517,7 @@ function dayOfWeekBreakdown(daily) {
|
|
|
507
517
|
count: 0
|
|
508
518
|
}));
|
|
509
519
|
for (const entry of daily) {
|
|
510
|
-
const dayIndex = new Date(entry.date
|
|
520
|
+
const dayIndex = new Date(dateToUtcMs(entry.date)).getUTCDay();
|
|
511
521
|
const bucket = buckets[dayIndex];
|
|
512
522
|
bucket.tokens += entry.totalTokens;
|
|
513
523
|
bucket.cost += entry.cost;
|
|
@@ -577,6 +587,7 @@ function topModels(daily, limit = DEFAULT_LIMIT) {
|
|
|
577
587
|
return entries.slice(0, limit);
|
|
578
588
|
}
|
|
579
589
|
// packages/core/dist/aggregation/aggregate.js
|
|
590
|
+
var ROLLING_WINDOW_DAYS = 30;
|
|
580
591
|
function aggregate(daily, referenceDate) {
|
|
581
592
|
const streaks = calculateStreaks(daily);
|
|
582
593
|
const rolling30 = rollingWindow(daily, 30, referenceDate);
|
|
@@ -586,20 +597,18 @@ function aggregate(daily, referenceDate) {
|
|
|
586
597
|
const cache = cacheHitRate(daily);
|
|
587
598
|
const models = topModels(daily);
|
|
588
599
|
let totalTokens = 0;
|
|
600
|
+
let totalInputTokens = 0;
|
|
601
|
+
let totalOutputTokens = 0;
|
|
589
602
|
let totalCost = 0;
|
|
590
603
|
for (const entry of daily) {
|
|
591
604
|
totalTokens += entry.totalTokens;
|
|
605
|
+
totalInputTokens += entry.inputTokens;
|
|
606
|
+
totalOutputTokens += entry.outputTokens;
|
|
592
607
|
totalCost += entry.cost;
|
|
593
608
|
}
|
|
609
|
+
const rolling30dTopModel = computeRolling30dTopModel(daily, referenceDate);
|
|
594
610
|
const activeDays = daily.length;
|
|
595
|
-
|
|
596
|
-
if (daily.length > 0) {
|
|
597
|
-
const sorted = [...daily].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
598
|
-
const first = new Date(sorted[0].date).getTime();
|
|
599
|
-
const last = new Date(sorted[sorted.length - 1].date).getTime();
|
|
600
|
-
const ONE_DAY_MS = 86400000;
|
|
601
|
-
totalDays = Math.round((last - first) / ONE_DAY_MS) + 1;
|
|
602
|
-
}
|
|
611
|
+
const totalDays = computeTotalDays(daily);
|
|
603
612
|
const averages = calculateAverages(daily, totalDays);
|
|
604
613
|
return {
|
|
605
614
|
currentStreak: streaks.current,
|
|
@@ -613,13 +622,46 @@ function aggregate(daily, referenceDate) {
|
|
|
613
622
|
averageDailyCost: averages.cost,
|
|
614
623
|
cacheHitRate: cache,
|
|
615
624
|
totalTokens,
|
|
625
|
+
totalInputTokens,
|
|
626
|
+
totalOutputTokens,
|
|
616
627
|
totalCost,
|
|
617
628
|
totalDays,
|
|
618
629
|
activeDays,
|
|
619
630
|
dayOfWeek: dow,
|
|
620
|
-
topModels: models
|
|
631
|
+
topModels: models,
|
|
632
|
+
rolling30dTopModel
|
|
621
633
|
};
|
|
622
634
|
}
|
|
635
|
+
function computeRolling30dTopModel(daily, referenceDate) {
|
|
636
|
+
const refTime = dateToUtcMs(referenceDate);
|
|
637
|
+
const windowStart = refTime - (ROLLING_WINDOW_DAYS - 1) * ONE_DAY_MS;
|
|
638
|
+
const modelTokensMap = new Map;
|
|
639
|
+
for (const entry of daily) {
|
|
640
|
+
const entryTime = dateToUtcMs(entry.date);
|
|
641
|
+
if (entryTime >= windowStart && entryTime <= refTime) {
|
|
642
|
+
for (const m of entry.models) {
|
|
643
|
+
modelTokensMap.set(m.model, (modelTokensMap.get(m.model) ?? 0) + m.totalTokens);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
let topModel = null;
|
|
648
|
+
let maxTokens = 0;
|
|
649
|
+
for (const [model, tokens] of modelTokensMap) {
|
|
650
|
+
if (tokens > maxTokens) {
|
|
651
|
+
maxTokens = tokens;
|
|
652
|
+
topModel = model;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return topModel;
|
|
656
|
+
}
|
|
657
|
+
function computeTotalDays(daily) {
|
|
658
|
+
if (daily.length === 0)
|
|
659
|
+
return 0;
|
|
660
|
+
const sorted = [...daily].sort((a, b) => dateToUtcMs(a.date) - dateToUtcMs(b.date));
|
|
661
|
+
const first = dateToUtcMs(sorted[0].date);
|
|
662
|
+
const last = dateToUtcMs(sorted[sorted.length - 1].date);
|
|
663
|
+
return Math.round((last - first) / ONE_DAY_MS) + 1;
|
|
664
|
+
}
|
|
623
665
|
// packages/core/dist/aggregation/merge.js
|
|
624
666
|
function mergeProviderData(providers) {
|
|
625
667
|
const dateMap = new Map;
|
|
@@ -648,7 +690,7 @@ function mergeProviderData(providers) {
|
|
|
648
690
|
}
|
|
649
691
|
}
|
|
650
692
|
}
|
|
651
|
-
return [...dateMap.values()].sort((a, b) =>
|
|
693
|
+
return [...dateMap.values()].sort((a, b) => compareDateStrings(a.date, b.date));
|
|
652
694
|
}
|
|
653
695
|
// packages/core/dist/aggregation/compare.js
|
|
654
696
|
function computeDeltas(statsA, statsB) {
|
|
@@ -683,19 +725,18 @@ function parseCompareRange(rangeStr) {
|
|
|
683
725
|
return { since, until };
|
|
684
726
|
}
|
|
685
727
|
function computePreviousPeriod(current) {
|
|
686
|
-
const
|
|
687
|
-
const
|
|
688
|
-
const untilMs = new Date(current.until).getTime();
|
|
728
|
+
const sinceMs = dateToUtcMs(current.since);
|
|
729
|
+
const untilMs = dateToUtcMs(current.until);
|
|
689
730
|
const periodDays = Math.round((untilMs - sinceMs) / ONE_DAY_MS);
|
|
690
731
|
const prevUntil = new Date(sinceMs - ONE_DAY_MS);
|
|
691
732
|
const prevSince = new Date(prevUntil.getTime() - periodDays * ONE_DAY_MS);
|
|
692
733
|
return {
|
|
693
|
-
since: prevSince
|
|
694
|
-
until: prevUntil
|
|
734
|
+
since: formatDateStringUtc(prevSince),
|
|
735
|
+
until: formatDateStringUtc(prevUntil)
|
|
695
736
|
};
|
|
696
737
|
}
|
|
697
738
|
// packages/core/dist/index.js
|
|
698
|
-
var VERSION = "0.
|
|
739
|
+
var VERSION = "0.4.1";
|
|
699
740
|
|
|
700
741
|
// packages/registry/dist/models/normalizer.js
|
|
701
742
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -915,6 +956,13 @@ async function* splitJsonlRecords(filePath) {
|
|
|
915
956
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
916
957
|
import { join } from "path";
|
|
917
958
|
import { homedir } from "os";
|
|
959
|
+
|
|
960
|
+
// packages/registry/dist/utils.js
|
|
961
|
+
function isInRange(date, range) {
|
|
962
|
+
return date >= range.since && date <= range.until;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// packages/registry/dist/providers/claude-code.js
|
|
918
966
|
var DEFAULT_BASE_DIR = join(homedir(), ".claude", "projects");
|
|
919
967
|
var CLAUDE_CODE_COLORS = {
|
|
920
968
|
primary: "#ff6b35",
|
|
@@ -981,9 +1029,6 @@ function extractUsage(record) {
|
|
|
981
1029
|
cacheWriteTokens
|
|
982
1030
|
};
|
|
983
1031
|
}
|
|
984
|
-
function isInRange(date, range) {
|
|
985
|
-
return date >= range.since && date <= range.until;
|
|
986
|
-
}
|
|
987
1032
|
function buildDailyUsage(records) {
|
|
988
1033
|
const byDate = new Map;
|
|
989
1034
|
for (const rec of records) {
|
|
@@ -1011,7 +1056,7 @@ function buildDailyUsage(records) {
|
|
|
1011
1056
|
mb.outputTokens += rec.outputTokens;
|
|
1012
1057
|
mb.cacheReadTokens += rec.cacheReadTokens;
|
|
1013
1058
|
mb.cacheWriteTokens += rec.cacheWriteTokens;
|
|
1014
|
-
mb.totalTokens += rec.inputTokens + rec.outputTokens;
|
|
1059
|
+
mb.totalTokens += rec.inputTokens + rec.outputTokens + rec.cacheReadTokens + rec.cacheWriteTokens;
|
|
1015
1060
|
mb.cost += cost;
|
|
1016
1061
|
}
|
|
1017
1062
|
const daily = [];
|
|
@@ -1125,9 +1170,6 @@ function extractDate(timestamp) {
|
|
|
1125
1170
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
1126
1171
|
return match ? match[1] : null;
|
|
1127
1172
|
}
|
|
1128
|
-
function isInRange2(date, range) {
|
|
1129
|
-
return date >= range.since && date <= range.until;
|
|
1130
|
-
}
|
|
1131
1173
|
|
|
1132
1174
|
class CodexProvider {
|
|
1133
1175
|
name = "codex";
|
|
@@ -1161,7 +1203,7 @@ class CodexProvider {
|
|
|
1161
1203
|
continue;
|
|
1162
1204
|
}
|
|
1163
1205
|
const date = extractDate(event.timestamp);
|
|
1164
|
-
if (!date || !
|
|
1206
|
+
if (!date || !isInRange(date, range)) {
|
|
1165
1207
|
continue;
|
|
1166
1208
|
}
|
|
1167
1209
|
const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
|
|
@@ -1188,7 +1230,9 @@ class CodexProvider {
|
|
|
1188
1230
|
const breakdown = modelMap.get(normalizedModel);
|
|
1189
1231
|
breakdown.inputTokens += inputTokens;
|
|
1190
1232
|
breakdown.outputTokens += outputTokens;
|
|
1191
|
-
breakdown.
|
|
1233
|
+
breakdown.cacheReadTokens += cacheReadTokens;
|
|
1234
|
+
breakdown.cacheWriteTokens += cacheWriteTokens;
|
|
1235
|
+
breakdown.totalTokens += inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1192
1236
|
breakdown.cost += cost;
|
|
1193
1237
|
}
|
|
1194
1238
|
} catch {
|
|
@@ -1248,9 +1292,6 @@ function extractDate2(createdAt) {
|
|
|
1248
1292
|
}
|
|
1249
1293
|
return new Date(createdAt).toISOString().slice(0, 10);
|
|
1250
1294
|
}
|
|
1251
|
-
function isWithinRange(date, range) {
|
|
1252
|
-
return date >= range.since && date <= range.until;
|
|
1253
|
-
}
|
|
1254
1295
|
function buildProviderData(records) {
|
|
1255
1296
|
const byDate = new Map;
|
|
1256
1297
|
for (const record of records) {
|
|
@@ -1280,13 +1321,15 @@ function buildProviderData(records) {
|
|
|
1280
1321
|
let dayCost = 0;
|
|
1281
1322
|
for (const [model, usage] of modelMap) {
|
|
1282
1323
|
const cost = estimateCost(model, usage.inputTokens, usage.outputTokens, 0, 0);
|
|
1283
|
-
const
|
|
1324
|
+
const cacheReadTokens = 0;
|
|
1325
|
+
const cacheWriteTokens = 0;
|
|
1326
|
+
const modelTotal = usage.inputTokens + usage.outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1284
1327
|
models.push({
|
|
1285
1328
|
model,
|
|
1286
1329
|
inputTokens: usage.inputTokens,
|
|
1287
1330
|
outputTokens: usage.outputTokens,
|
|
1288
|
-
cacheReadTokens
|
|
1289
|
-
cacheWriteTokens
|
|
1331
|
+
cacheReadTokens,
|
|
1332
|
+
cacheWriteTokens,
|
|
1290
1333
|
totalTokens: modelTotal,
|
|
1291
1334
|
cost
|
|
1292
1335
|
});
|
|
@@ -1324,7 +1367,7 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1324
1367
|
const records = [];
|
|
1325
1368
|
for (const row of rows) {
|
|
1326
1369
|
const date = extractDate2(row.created_at);
|
|
1327
|
-
if (
|
|
1370
|
+
if (isInRange(date, range)) {
|
|
1328
1371
|
records.push({
|
|
1329
1372
|
date,
|
|
1330
1373
|
model: row.model,
|
|
@@ -1352,7 +1395,7 @@ function loadFromJson(sessionsDir, range) {
|
|
|
1352
1395
|
continue;
|
|
1353
1396
|
}
|
|
1354
1397
|
const date = extractDate2(msg.created_at);
|
|
1355
|
-
if (
|
|
1398
|
+
if (isInRange(date, range)) {
|
|
1356
1399
|
records.push({
|
|
1357
1400
|
date,
|
|
1358
1401
|
model: msg.model,
|
|
@@ -1413,46 +1456,42 @@ var DARK_THEME = {
|
|
|
1413
1456
|
muted: "#7d8590",
|
|
1414
1457
|
border: "#30363d",
|
|
1415
1458
|
cardBackground: "#161b22",
|
|
1416
|
-
heatmap: ["#161b22", "#
|
|
1459
|
+
heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
|
|
1417
1460
|
accent: "#58a6ff",
|
|
1418
1461
|
accentSecondary: "#bc8cff",
|
|
1419
|
-
barFill: "#
|
|
1462
|
+
barFill: "#3b82f6",
|
|
1420
1463
|
barBackground: "#21262d"
|
|
1421
1464
|
};
|
|
1422
1465
|
var LIGHT_THEME = {
|
|
1423
1466
|
background: "#ffffff",
|
|
1424
|
-
foreground: "#
|
|
1425
|
-
muted: "#
|
|
1426
|
-
border: "#
|
|
1427
|
-
cardBackground: "#
|
|
1428
|
-
heatmap: ["#ebedf0", "#
|
|
1429
|
-
accent: "#
|
|
1430
|
-
accentSecondary: "#
|
|
1431
|
-
barFill: "#
|
|
1432
|
-
barBackground: "#
|
|
1467
|
+
foreground: "#1a1a2e",
|
|
1468
|
+
muted: "#8b8fa3",
|
|
1469
|
+
border: "#e5e7eb",
|
|
1470
|
+
cardBackground: "#f8f9fc",
|
|
1471
|
+
heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
|
|
1472
|
+
accent: "#3b5bdb",
|
|
1473
|
+
accentSecondary: "#7048e8",
|
|
1474
|
+
barFill: "#5b6abf",
|
|
1475
|
+
barBackground: "#ebedf0"
|
|
1433
1476
|
};
|
|
1434
1477
|
function getTheme(mode) {
|
|
1435
1478
|
return mode === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
1436
1479
|
}
|
|
1437
1480
|
|
|
1438
1481
|
// packages/renderers/dist/svg/layout.js
|
|
1439
|
-
var PADDING =
|
|
1440
|
-
var CELL_SIZE =
|
|
1441
|
-
var CELL_GAP =
|
|
1442
|
-
var
|
|
1443
|
-
var
|
|
1444
|
-
var DAY_LABEL_WIDTH = 32;
|
|
1482
|
+
var PADDING = 40;
|
|
1483
|
+
var CELL_SIZE = 16;
|
|
1484
|
+
var CELL_GAP = 4;
|
|
1485
|
+
var MONTH_LABEL_HEIGHT = 24;
|
|
1486
|
+
var DAY_LABEL_WIDTH = 44;
|
|
1445
1487
|
var HEATMAP_ROWS = 7;
|
|
1446
|
-
var SECTION_GAP =
|
|
1447
|
-
var
|
|
1448
|
-
var BAR_HEIGHT = 20;
|
|
1449
|
-
var BAR_GAP = 8;
|
|
1450
|
-
var BAR_LABEL_WIDTH = 120;
|
|
1451
|
-
var FONT_SIZE_TITLE = 20;
|
|
1488
|
+
var SECTION_GAP = 32;
|
|
1489
|
+
var FONT_SIZE_TITLE = 28;
|
|
1452
1490
|
var FONT_SIZE_SUBTITLE = 14;
|
|
1453
|
-
var
|
|
1454
|
-
var
|
|
1455
|
-
var
|
|
1491
|
+
var FONT_SIZE_SMALL = 11;
|
|
1492
|
+
var FONT_SIZE_STAT_VALUE = 32;
|
|
1493
|
+
var FONT_SIZE_STAT_LABEL = 11;
|
|
1494
|
+
var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
1456
1495
|
|
|
1457
1496
|
// packages/renderers/dist/svg/utils.js
|
|
1458
1497
|
function escapeXml(str) {
|
|
@@ -1472,28 +1511,24 @@ function group(children, transform) {
|
|
|
1472
1511
|
}
|
|
1473
1512
|
function formatNumber(n) {
|
|
1474
1513
|
if (n >= 1e6) {
|
|
1475
|
-
|
|
1514
|
+
const millions = Number((n / 1e6).toFixed(1));
|
|
1515
|
+
if (millions >= 1000) {
|
|
1516
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
1517
|
+
}
|
|
1518
|
+
return `${millions.toFixed(1)}M`;
|
|
1476
1519
|
}
|
|
1477
1520
|
if (n >= 1000) {
|
|
1478
|
-
|
|
1521
|
+
const thousands = Number((n / 1000).toFixed(1));
|
|
1522
|
+
if (thousands >= 1000) {
|
|
1523
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1524
|
+
}
|
|
1525
|
+
return `${thousands.toFixed(1)}K`;
|
|
1479
1526
|
}
|
|
1480
1527
|
return n.toFixed(0);
|
|
1481
1528
|
}
|
|
1482
|
-
function formatCost(cost) {
|
|
1483
|
-
if (cost >= 100) {
|
|
1484
|
-
return `$${cost.toFixed(0)}`;
|
|
1485
|
-
}
|
|
1486
|
-
if (cost >= 1) {
|
|
1487
|
-
return `$${cost.toFixed(2)}`;
|
|
1488
|
-
}
|
|
1489
|
-
return `$${cost.toFixed(4)}`;
|
|
1490
|
-
}
|
|
1491
|
-
function formatPercent(rate) {
|
|
1492
|
-
return `${(rate * 100).toFixed(1)}%`;
|
|
1493
|
-
}
|
|
1494
1529
|
|
|
1495
1530
|
// packages/renderers/dist/svg/heatmap.js
|
|
1496
|
-
var DAY_LABELS2 = ["", "
|
|
1531
|
+
var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
|
|
1497
1532
|
var MONTH_NAMES = [
|
|
1498
1533
|
"Jan",
|
|
1499
1534
|
"Feb",
|
|
@@ -1538,10 +1573,10 @@ function renderHeatmap(daily, theme, options = {}) {
|
|
|
1538
1573
|
const dates = daily.map((d) => d.date).sort();
|
|
1539
1574
|
const endStr = options.endDate ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
1540
1575
|
const startStr = options.startDate ?? dates[0] ?? endStr;
|
|
1541
|
-
const end = new Date(endStr);
|
|
1542
|
-
const start = new Date(startStr);
|
|
1543
|
-
const startDay = start.
|
|
1544
|
-
start.
|
|
1576
|
+
const end = new Date(endStr + "T00:00:00Z");
|
|
1577
|
+
const start = new Date(startStr + "T00:00:00Z");
|
|
1578
|
+
const startDay = start.getUTCDay();
|
|
1579
|
+
start.setUTCDate(start.getUTCDate() - startDay);
|
|
1545
1580
|
const cells = [];
|
|
1546
1581
|
const allTokens = Array.from(tokenMap.values());
|
|
1547
1582
|
const quantiles = computeQuantiles(allTokens);
|
|
@@ -1549,19 +1584,20 @@ function renderHeatmap(daily, theme, options = {}) {
|
|
|
1549
1584
|
let col = 0;
|
|
1550
1585
|
const monthLabels = [];
|
|
1551
1586
|
let lastMonth = -1;
|
|
1587
|
+
const cellRadius = 3;
|
|
1552
1588
|
while (current <= end) {
|
|
1553
|
-
const row = current.
|
|
1589
|
+
const row = current.getUTCDay();
|
|
1554
1590
|
const dateStr = current.toISOString().slice(0, 10);
|
|
1555
1591
|
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
1556
1592
|
const level = getLevel(tokens, quantiles);
|
|
1557
1593
|
const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
|
|
1558
1594
|
const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
|
|
1559
1595
|
const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
|
|
1560
|
-
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="
|
|
1561
|
-
const month = current.
|
|
1596
|
+
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="${cellRadius}"><title>${escapeXml(title)}</title></rect>`);
|
|
1597
|
+
const month = current.getUTCMonth();
|
|
1562
1598
|
if (month !== lastMonth && row === 0) {
|
|
1563
1599
|
lastMonth = month;
|
|
1564
|
-
monthLabels.push(text(x, MONTH_LABEL_HEIGHT -
|
|
1600
|
+
monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 8, MONTH_NAMES[month] ?? "", {
|
|
1565
1601
|
fill: theme.muted,
|
|
1566
1602
|
"font-size": FONT_SIZE_SMALL,
|
|
1567
1603
|
"font-family": FONT_FAMILY
|
|
@@ -1570,12 +1606,12 @@ function renderHeatmap(daily, theme, options = {}) {
|
|
|
1570
1606
|
if (row === 6) {
|
|
1571
1607
|
col++;
|
|
1572
1608
|
}
|
|
1573
|
-
current.
|
|
1609
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
1574
1610
|
}
|
|
1575
1611
|
const dayLabels = DAY_LABELS2.map((label, i) => {
|
|
1576
1612
|
if (!label)
|
|
1577
1613
|
return "";
|
|
1578
|
-
const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE -
|
|
1614
|
+
const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
|
|
1579
1615
|
return text(0, y, label, {
|
|
1580
1616
|
fill: theme.muted,
|
|
1581
1617
|
"font-size": FONT_SIZE_SMALL,
|
|
@@ -1583,291 +1619,153 @@ function renderHeatmap(daily, theme, options = {}) {
|
|
|
1583
1619
|
});
|
|
1584
1620
|
});
|
|
1585
1621
|
const totalCols = col + 1;
|
|
1586
|
-
const
|
|
1587
|
-
const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS *
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1622
|
+
const gridWidth = DAY_LABEL_WIDTH + totalCols * CELL_SIZE + Math.max(0, totalCols - 1) * CELL_GAP;
|
|
1623
|
+
const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * CELL_SIZE + (HEATMAP_ROWS - 1) * CELL_GAP;
|
|
1624
|
+
const legendY = height + 16;
|
|
1625
|
+
const legendItems = [];
|
|
1626
|
+
const legendStartX = 0;
|
|
1627
|
+
legendItems.push(text(legendStartX, legendY + CELL_SIZE - 2, "LESS", {
|
|
1628
|
+
fill: theme.muted,
|
|
1629
|
+
"font-size": 9,
|
|
1630
|
+
"font-family": FONT_FAMILY,
|
|
1631
|
+
"font-weight": "600",
|
|
1632
|
+
"letter-spacing": "0.5"
|
|
1633
|
+
}));
|
|
1634
|
+
const legendBoxStart = legendStartX + 40;
|
|
1635
|
+
for (let i = 0;i < 5; i++) {
|
|
1636
|
+
legendItems.push(`<rect x="${legendBoxStart + i * (CELL_SIZE + 3)}" y="${legendY}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[i])}" rx="${cellRadius}"/>`);
|
|
1637
|
+
}
|
|
1638
|
+
legendItems.push(text(legendBoxStart + 5 * (CELL_SIZE + 3) + 4, legendY + CELL_SIZE - 2, "MORE", {
|
|
1639
|
+
fill: theme.muted,
|
|
1640
|
+
"font-size": 9,
|
|
1641
|
+
"font-family": FONT_FAMILY,
|
|
1642
|
+
"font-weight": "600",
|
|
1643
|
+
"letter-spacing": "0.5"
|
|
1644
|
+
}));
|
|
1645
|
+
const totalHeight = legendY + CELL_SIZE + 8;
|
|
1646
|
+
const legendRightX = legendBoxStart + 5 * (CELL_SIZE + 3) + 4 + 40;
|
|
1647
|
+
const width = Math.max(gridWidth, legendRightX);
|
|
1648
|
+
const svg = group([...monthLabels, ...dayLabels, ...cells, ...legendItems]);
|
|
1649
|
+
return { svg, width, height: totalHeight };
|
|
1590
1650
|
}
|
|
1591
1651
|
|
|
1592
|
-
// packages/renderers/dist/svg/
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
];
|
|
1605
|
-
}
|
|
1606
|
-
function renderStatsPanel(stats, theme) {
|
|
1607
|
-
const items = buildStatItems(stats);
|
|
1608
|
-
const width = 280;
|
|
1609
|
-
const children = [];
|
|
1610
|
-
for (let i = 0;i < items.length; i++) {
|
|
1611
|
-
const item = items[i];
|
|
1612
|
-
if (!item)
|
|
1613
|
-
continue;
|
|
1614
|
-
const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
|
|
1615
|
-
children.push(text(0, y, item.label, {
|
|
1652
|
+
// packages/renderers/dist/svg/svg-renderer.js
|
|
1653
|
+
var MIN_SVG_WIDTH = 1000;
|
|
1654
|
+
var MAX_STAT_VALUE_CHARS = 28;
|
|
1655
|
+
function truncateText(value, maxChars) {
|
|
1656
|
+
if (value.length <= maxChars)
|
|
1657
|
+
return value;
|
|
1658
|
+
return value.slice(0, maxChars - 1) + "\u2026";
|
|
1659
|
+
}
|
|
1660
|
+
function renderHeaderStat(x, y, label, value, theme, align = "end") {
|
|
1661
|
+
const anchor = align === "end" ? "end" : "start";
|
|
1662
|
+
return group([
|
|
1663
|
+
text(x, y, label, {
|
|
1616
1664
|
fill: theme.muted,
|
|
1617
|
-
"font-size":
|
|
1618
|
-
"font-family": FONT_FAMILY
|
|
1619
|
-
}));
|
|
1620
|
-
children.push(text(width - 8, y, item.value, {
|
|
1621
|
-
fill: theme.foreground,
|
|
1622
|
-
"font-size": FONT_SIZE_BODY,
|
|
1665
|
+
"font-size": FONT_SIZE_STAT_LABEL,
|
|
1623
1666
|
"font-family": FONT_FAMILY,
|
|
1624
|
-
"
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// packages/renderers/dist/svg/insights-panel.js
|
|
1632
|
-
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1633
|
-
function buildInsights(stats, providers) {
|
|
1634
|
-
const items = [];
|
|
1635
|
-
if (stats.peakDay) {
|
|
1636
|
-
items.push({
|
|
1637
|
-
label: "Peak Day",
|
|
1638
|
-
value: `${stats.peakDay.date} (${formatNumber(stats.peakDay.tokens)} tokens)`
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
if (stats.dayOfWeek.length > 0) {
|
|
1642
|
-
const sorted = [...stats.dayOfWeek].sort((a, b) => b.tokens - a.tokens);
|
|
1643
|
-
const top = sorted[0];
|
|
1644
|
-
if (top) {
|
|
1645
|
-
items.push({
|
|
1646
|
-
label: "Most Active Day",
|
|
1647
|
-
value: DAY_NAMES[top.day] ?? top.label
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
if (stats.topModels.length > 0) {
|
|
1652
|
-
const top = stats.topModels[0];
|
|
1653
|
-
if (top) {
|
|
1654
|
-
const pct = top.percentage < 1 ? top.percentage * 100 : top.percentage;
|
|
1655
|
-
items.push({
|
|
1656
|
-
label: "Top Model",
|
|
1657
|
-
value: `${top.model} (${pct.toFixed(1)}%)`
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
if (providers.length > 0) {
|
|
1662
|
-
const sorted = [...providers].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1663
|
-
const top = sorted[0];
|
|
1664
|
-
if (top) {
|
|
1665
|
-
items.push({
|
|
1666
|
-
label: "Top Provider",
|
|
1667
|
-
value: `${top.displayName} (${formatNumber(top.totalTokens)} tokens)`
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
return items;
|
|
1672
|
-
}
|
|
1673
|
-
function renderInsightsPanel(stats, providers, theme) {
|
|
1674
|
-
const items = buildInsights(stats, providers);
|
|
1675
|
-
const width = 360;
|
|
1676
|
-
const children = [];
|
|
1677
|
-
for (let i = 0;i < items.length; i++) {
|
|
1678
|
-
const item = items[i];
|
|
1679
|
-
if (!item)
|
|
1680
|
-
continue;
|
|
1681
|
-
const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
|
|
1682
|
-
children.push(text(0, y, item.label, {
|
|
1683
|
-
fill: theme.muted,
|
|
1684
|
-
"font-size": FONT_SIZE_SMALL,
|
|
1685
|
-
"font-family": FONT_FAMILY
|
|
1686
|
-
}));
|
|
1687
|
-
children.push(text(width - 8, y, item.value, {
|
|
1667
|
+
"font-weight": "700",
|
|
1668
|
+
"text-anchor": anchor,
|
|
1669
|
+
"letter-spacing": "1"
|
|
1670
|
+
}),
|
|
1671
|
+
text(x, y + 34, value, {
|
|
1688
1672
|
fill: theme.foreground,
|
|
1689
|
-
"font-size":
|
|
1673
|
+
"font-size": FONT_SIZE_STAT_VALUE,
|
|
1690
1674
|
"font-family": FONT_FAMILY,
|
|
1691
|
-
"
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
return { svg: group(children), width, height };
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// packages/renderers/dist/svg/day-of-week-chart.js
|
|
1699
|
-
function renderDayOfWeekChart(dayOfWeek, theme) {
|
|
1700
|
-
const chartWidth = 300;
|
|
1701
|
-
const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
|
|
1702
|
-
const maxTokens = Math.max(...dayOfWeek.map((d) => d.tokens), 1);
|
|
1703
|
-
const children = [];
|
|
1704
|
-
for (let i = 0;i < dayOfWeek.length; i++) {
|
|
1705
|
-
const entry = dayOfWeek[i];
|
|
1706
|
-
if (!entry)
|
|
1707
|
-
continue;
|
|
1708
|
-
const y = i * (BAR_HEIGHT + BAR_GAP);
|
|
1709
|
-
const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
|
|
1710
|
-
children.push(text(0, y + BAR_HEIGHT - 4, entry.label, {
|
|
1711
|
-
fill: theme.muted,
|
|
1712
|
-
"font-size": FONT_SIZE_SMALL,
|
|
1713
|
-
"font-family": FONT_FAMILY
|
|
1714
|
-
}));
|
|
1715
|
-
children.push(rect(BAR_LABEL_WIDTH, y, barAreaWidth, BAR_HEIGHT, theme.barBackground, 3));
|
|
1716
|
-
if (barWidth > 0) {
|
|
1717
|
-
children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.barFill, 3));
|
|
1718
|
-
}
|
|
1719
|
-
children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, formatNumber(entry.tokens), {
|
|
1720
|
-
fill: theme.foreground,
|
|
1721
|
-
"font-size": FONT_SIZE_SMALL,
|
|
1722
|
-
"font-family": FONT_FAMILY
|
|
1723
|
-
}));
|
|
1724
|
-
}
|
|
1725
|
-
const height = dayOfWeek.length * (BAR_HEIGHT + BAR_GAP);
|
|
1726
|
-
const width = chartWidth + 60;
|
|
1727
|
-
return { svg: group(children), width, height };
|
|
1675
|
+
"font-weight": "700",
|
|
1676
|
+
"text-anchor": anchor
|
|
1677
|
+
})
|
|
1678
|
+
]);
|
|
1728
1679
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
const chartWidth = 360;
|
|
1733
|
-
const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
|
|
1734
|
-
const maxTokens = Math.max(...topModels2.map((m) => m.tokens), 1);
|
|
1735
|
-
const children = [];
|
|
1736
|
-
for (let i = 0;i < topModels2.length; i++) {
|
|
1737
|
-
const entry = topModels2[i];
|
|
1738
|
-
if (!entry)
|
|
1739
|
-
continue;
|
|
1740
|
-
const y = i * (BAR_HEIGHT + BAR_GAP);
|
|
1741
|
-
const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
|
|
1742
|
-
const label = entry.model.length > 18 ? entry.model.slice(0, 17) + "\u2026" : entry.model;
|
|
1743
|
-
children.push(text(0, y + BAR_HEIGHT - 4, label, {
|
|
1680
|
+
function renderBottomStat(x, y, label, value, theme) {
|
|
1681
|
+
return group([
|
|
1682
|
+
text(x, y, label, {
|
|
1744
1683
|
fill: theme.muted,
|
|
1745
|
-
"font-size":
|
|
1746
|
-
"font-family": FONT_FAMILY
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
const pct = entry.percentage < 1 ? entry.percentage * 100 : entry.percentage;
|
|
1753
|
-
const valueStr = `${formatNumber(entry.tokens)} (${pct.toFixed(1)}%)`;
|
|
1754
|
-
children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, valueStr, {
|
|
1684
|
+
"font-size": FONT_SIZE_STAT_LABEL,
|
|
1685
|
+
"font-family": FONT_FAMILY,
|
|
1686
|
+
"font-weight": "700",
|
|
1687
|
+
"letter-spacing": "0.8"
|
|
1688
|
+
}),
|
|
1689
|
+
text(x, y + 32, value, {
|
|
1755
1690
|
fill: theme.foreground,
|
|
1756
|
-
"font-size":
|
|
1757
|
-
"font-family": FONT_FAMILY
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const width = chartWidth + 100;
|
|
1762
|
-
return { svg: group(children), width, height };
|
|
1691
|
+
"font-size": 18,
|
|
1692
|
+
"font-family": FONT_FAMILY,
|
|
1693
|
+
"font-weight": "700"
|
|
1694
|
+
})
|
|
1695
|
+
]);
|
|
1763
1696
|
}
|
|
1764
1697
|
|
|
1765
|
-
// packages/renderers/dist/svg/svg-renderer.js
|
|
1766
|
-
var MIN_SVG_WIDTH = 520;
|
|
1767
|
-
|
|
1768
1698
|
class SvgRenderer {
|
|
1769
1699
|
format = "svg";
|
|
1770
1700
|
async render(output, options) {
|
|
1771
1701
|
const theme = getTheme(options.theme);
|
|
1772
|
-
let y = PADDING;
|
|
1773
1702
|
const sections = [];
|
|
1774
1703
|
const sectionWidths = [];
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
y += HEADER_HEIGHT + SECTION_GAP;
|
|
1789
|
-
if (output.providers.length > 0) {
|
|
1790
|
-
const providerNames = output.providers.map((p) => p.displayName).join(" \xB7 ");
|
|
1791
|
-
sections.push(text(PADDING, y, providerNames, {
|
|
1792
|
-
fill: theme.accent,
|
|
1793
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
1794
|
-
"font-family": FONT_FAMILY
|
|
1795
|
-
}));
|
|
1796
|
-
y += SECTION_GAP;
|
|
1797
|
-
}
|
|
1704
|
+
let y = PADDING;
|
|
1705
|
+
const providerName = output.providers.length > 0 ? output.providers.map((p) => p.displayName).join(" + ") : "Tokenleak";
|
|
1706
|
+
sections.push(text(PADDING, y + FONT_SIZE_TITLE, providerName, {
|
|
1707
|
+
fill: theme.foreground,
|
|
1708
|
+
"font-size": FONT_SIZE_TITLE,
|
|
1709
|
+
"font-family": FONT_FAMILY,
|
|
1710
|
+
"font-weight": "700"
|
|
1711
|
+
}));
|
|
1712
|
+
sections.push(text(PADDING, y + FONT_SIZE_TITLE + 22, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
|
|
1713
|
+
fill: theme.muted,
|
|
1714
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1715
|
+
"font-family": FONT_FAMILY
|
|
1716
|
+
}));
|
|
1798
1717
|
const allDaily = output.providers.flatMap((p) => p.daily);
|
|
1718
|
+
const heatmap = renderHeatmap(allDaily, theme, {
|
|
1719
|
+
startDate: output.dateRange.since,
|
|
1720
|
+
endDate: output.dateRange.until
|
|
1721
|
+
});
|
|
1722
|
+
sectionWidths.push(heatmap.width);
|
|
1723
|
+
const contentWidth = Math.max(heatmap.width, MIN_SVG_WIDTH - PADDING * 2);
|
|
1724
|
+
const stats = output.aggregated;
|
|
1725
|
+
const headerStatSpacing = 180;
|
|
1726
|
+
const headerStatsX = PADDING + contentWidth;
|
|
1727
|
+
sections.push(renderHeaderStat(headerStatsX, y, "INPUT TOKENS", formatNumber(stats.totalInputTokens), theme));
|
|
1728
|
+
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing, y, "OUTPUT TOKENS", formatNumber(stats.totalOutputTokens), theme));
|
|
1729
|
+
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing * 2, y, "TOTAL TOKENS", formatNumber(stats.totalTokens), theme));
|
|
1730
|
+
y += FONT_SIZE_TITLE + 22 + SECTION_GAP;
|
|
1799
1731
|
if (allDaily.length > 0) {
|
|
1800
|
-
sections.push(text(PADDING, y, "Activity", {
|
|
1801
|
-
fill: theme.foreground,
|
|
1802
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
1803
|
-
"font-family": FONT_FAMILY,
|
|
1804
|
-
"font-weight": "bold"
|
|
1805
|
-
}));
|
|
1806
|
-
y += 16;
|
|
1807
|
-
const heatmap = renderHeatmap(allDaily, theme, {
|
|
1808
|
-
startDate: output.dateRange.since,
|
|
1809
|
-
endDate: output.dateRange.until
|
|
1810
|
-
});
|
|
1811
1732
|
sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1733
|
+
y += heatmap.height + SECTION_GAP + 8;
|
|
1734
|
+
}
|
|
1735
|
+
sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1736
|
+
y += SECTION_GAP;
|
|
1737
|
+
const col1Width = contentWidth * 0.35;
|
|
1738
|
+
const col2Width = contentWidth * 0.35;
|
|
1739
|
+
const col3Width = contentWidth * 0.15;
|
|
1740
|
+
const col4Width = contentWidth * 0.15;
|
|
1741
|
+
const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
|
|
1742
|
+
const topModelLabel = topModel ? truncateText(`${topModel.model} (${formatNumber(topModel.tokens)})`, MAX_STAT_VALUE_CHARS) : "N/A";
|
|
1743
|
+
sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
|
|
1744
|
+
const recent30Label = stats.rolling30dTopModel ? truncateText(`${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})`, MAX_STAT_VALUE_CHARS) : formatNumber(stats.rolling30dTokens);
|
|
1745
|
+
sections.push(renderBottomStat(PADDING + col1Width, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
|
|
1746
|
+
sections.push(renderBottomStat(PADDING + col1Width + col2Width, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
|
|
1747
|
+
sections.push(renderBottomStat(PADDING + col1Width + col2Width + col3Width, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
|
|
1748
|
+
y += 56 + SECTION_GAP;
|
|
1749
|
+
const evenCardWidth = contentWidth / 4;
|
|
1750
|
+
sections.push(`<line x1="${PADDING}" y1="${y - SECTION_GAP / 2}" x2="${PADDING + contentWidth}" y2="${y - SECTION_GAP / 2}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1751
|
+
sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
|
|
1752
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
|
|
1753
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
|
|
1754
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
|
|
1755
|
+
y += 56 + PADDING;
|
|
1756
|
+
const totalHeight = y;
|
|
1757
|
+
const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
1758
|
+
sections.push(text(svgWidth - PADDING, totalHeight - 16, "tokenleak", {
|
|
1759
|
+
fill: theme.muted,
|
|
1760
|
+
"font-size": 10,
|
|
1818
1761
|
"font-family": FONT_FAMILY,
|
|
1819
|
-
"
|
|
1762
|
+
"text-anchor": "end",
|
|
1763
|
+
opacity: "0.4"
|
|
1820
1764
|
}));
|
|
1821
|
-
y += 16;
|
|
1822
|
-
const stats = renderStatsPanel(output.aggregated, theme);
|
|
1823
|
-
sections.push(group([stats.svg], `translate(${PADDING}, ${y})`));
|
|
1824
|
-
sectionWidths.push(stats.width);
|
|
1825
|
-
y += stats.height + SECTION_GAP;
|
|
1826
|
-
if (output.aggregated.dayOfWeek.length > 0) {
|
|
1827
|
-
sections.push(text(PADDING, y, "Day of Week", {
|
|
1828
|
-
fill: theme.foreground,
|
|
1829
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
1830
|
-
"font-family": FONT_FAMILY,
|
|
1831
|
-
"font-weight": "bold"
|
|
1832
|
-
}));
|
|
1833
|
-
y += 16;
|
|
1834
|
-
const dowChart = renderDayOfWeekChart(output.aggregated.dayOfWeek, theme);
|
|
1835
|
-
sections.push(group([dowChart.svg], `translate(${PADDING}, ${y})`));
|
|
1836
|
-
sectionWidths.push(dowChart.width);
|
|
1837
|
-
y += dowChart.height + SECTION_GAP;
|
|
1838
|
-
}
|
|
1839
|
-
if (output.aggregated.topModels.length > 0) {
|
|
1840
|
-
sections.push(text(PADDING, y, "Top Models", {
|
|
1841
|
-
fill: theme.foreground,
|
|
1842
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
1843
|
-
"font-family": FONT_FAMILY,
|
|
1844
|
-
"font-weight": "bold"
|
|
1845
|
-
}));
|
|
1846
|
-
y += 16;
|
|
1847
|
-
const modelChart = renderModelChart(output.aggregated.topModels, theme);
|
|
1848
|
-
sections.push(group([modelChart.svg], `translate(${PADDING}, ${y})`));
|
|
1849
|
-
sectionWidths.push(modelChart.width);
|
|
1850
|
-
y += modelChart.height + SECTION_GAP;
|
|
1851
|
-
}
|
|
1852
|
-
if (options.showInsights) {
|
|
1853
|
-
sections.push(text(PADDING, y, "Insights", {
|
|
1854
|
-
fill: theme.foreground,
|
|
1855
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
1856
|
-
"font-family": FONT_FAMILY,
|
|
1857
|
-
"font-weight": "bold"
|
|
1858
|
-
}));
|
|
1859
|
-
y += 16;
|
|
1860
|
-
const insights = renderInsightsPanel(output.aggregated, output.providers, theme);
|
|
1861
|
-
sections.push(group([insights.svg], `translate(${PADDING}, ${y})`));
|
|
1862
|
-
sectionWidths.push(insights.width);
|
|
1863
|
-
y += insights.height + SECTION_GAP;
|
|
1864
|
-
}
|
|
1865
|
-
const totalHeight = y + PADDING;
|
|
1866
|
-
const maxContentWidth = sectionWidths.length > 0 ? Math.max(...sectionWidths) : MIN_SVG_WIDTH - PADDING * 2;
|
|
1867
|
-
const svgWidth = Math.max(maxContentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
1868
1765
|
const svgContent = [
|
|
1869
1766
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
|
|
1870
|
-
|
|
1767
|
+
`<defs><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');</style></defs>`,
|
|
1768
|
+
rect(0, 0, svgWidth, totalHeight, theme.background, 12),
|
|
1871
1769
|
...sections,
|
|
1872
1770
|
"</svg>"
|
|
1873
1771
|
].join(`
|
|
@@ -2034,7 +1932,7 @@ var BOX_TL = "\u250C";
|
|
|
2034
1932
|
var BOX_TR = "\u2510";
|
|
2035
1933
|
var BOX_BL = "\u2514";
|
|
2036
1934
|
var BOX_BR = "\u2518";
|
|
2037
|
-
var
|
|
1935
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2038
1936
|
var BAR_CHAR = "\u2588";
|
|
2039
1937
|
var MAX_BAR_LENGTH = 20;
|
|
2040
1938
|
function formatTokens(count) {
|
|
@@ -2049,7 +1947,7 @@ function formatTokens(count) {
|
|
|
2049
1947
|
function formatCost2(cost) {
|
|
2050
1948
|
return `$${cost.toFixed(2)}`;
|
|
2051
1949
|
}
|
|
2052
|
-
function
|
|
1950
|
+
function formatPercent(rate) {
|
|
2053
1951
|
return `${(rate * 100).toFixed(1)}%`;
|
|
2054
1952
|
}
|
|
2055
1953
|
function divider(width) {
|
|
@@ -2089,7 +1987,7 @@ function renderStats(stats, width, noColor2) {
|
|
|
2089
1987
|
["7d Cost", formatCost2(stats.rolling7dCost)],
|
|
2090
1988
|
["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
|
|
2091
1989
|
["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
|
|
2092
|
-
["Cache Hit Rate",
|
|
1990
|
+
["Cache Hit Rate", formatPercent(stats.cacheHitRate)],
|
|
2093
1991
|
["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
|
|
2094
1992
|
];
|
|
2095
1993
|
if (stats.peakDay) {
|
|
@@ -2106,7 +2004,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2106
2004
|
const lines = [];
|
|
2107
2005
|
const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
|
|
2108
2006
|
for (const entry of stats.dayOfWeek) {
|
|
2109
|
-
const label =
|
|
2007
|
+
const label = DAY_NAMES[entry.day] ?? `Day${entry.day}`;
|
|
2110
2008
|
const bar = dayBar(entry.tokens, maxTokens, noColor2);
|
|
2111
2009
|
const tokenStr = formatTokens(entry.tokens);
|
|
2112
2010
|
const line = ` ${label} ${bar} ${tokenStr}`;
|
|
@@ -2118,7 +2016,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2118
2016
|
function renderTopModels(stats, width, noColor2) {
|
|
2119
2017
|
const lines = [];
|
|
2120
2018
|
for (const model of stats.topModels.slice(0, 5)) {
|
|
2121
|
-
const pct =
|
|
2019
|
+
const pct = formatPercent(model.percentage);
|
|
2122
2020
|
const tokens = formatTokens(model.tokens);
|
|
2123
2021
|
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2124
2022
|
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
@@ -2132,7 +2030,7 @@ function renderInsights(stats, noColor2) {
|
|
|
2132
2030
|
insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
|
|
2133
2031
|
}
|
|
2134
2032
|
if (stats.cacheHitRate > 0.5) {
|
|
2135
|
-
insights.push(`Cache hit rate is ${
|
|
2033
|
+
insights.push(`Cache hit rate is ${formatPercent(stats.cacheHitRate)} - good cache reuse.`);
|
|
2136
2034
|
}
|
|
2137
2035
|
if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
|
|
2138
2036
|
insights.push("Cache hit rate is low - consider enabling prompt caching.");
|