tokenleak 0.3.0 → 0.4.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/package.json +1 -1
- package/tokenleak.js +250 -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.0";
|
|
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,144 @@ 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
|
-
{ label: "Total Cost", value: formatCost(stats.totalCost) },
|
|
1599
|
-
{ label: "30-Day Tokens", value: formatNumber(stats.rolling30dTokens) },
|
|
1600
|
-
{ label: "30-Day Cost", value: formatCost(stats.rolling30dCost) },
|
|
1601
|
-
{ label: "Avg Daily Tokens", value: formatNumber(stats.averageDailyTokens) },
|
|
1602
|
-
{ label: "Cache Hit Rate", value: formatPercent(stats.cacheHitRate) },
|
|
1603
|
-
{ label: "Active Days", value: `${stats.activeDays} / ${stats.totalDays}` }
|
|
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 = 800;
|
|
1654
|
+
function renderHeaderStat(x, y, label, value, theme, align = "end") {
|
|
1655
|
+
const anchor = align === "end" ? "end" : "start";
|
|
1656
|
+
return group([
|
|
1657
|
+
text(x, y, label, {
|
|
1616
1658
|
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,
|
|
1659
|
+
"font-size": FONT_SIZE_STAT_LABEL,
|
|
1623
1660
|
"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, {
|
|
1661
|
+
"font-weight": "700",
|
|
1662
|
+
"text-anchor": anchor,
|
|
1663
|
+
"letter-spacing": "1"
|
|
1664
|
+
}),
|
|
1665
|
+
text(x, y + 34, value, {
|
|
1688
1666
|
fill: theme.foreground,
|
|
1689
|
-
"font-size":
|
|
1667
|
+
"font-size": FONT_SIZE_STAT_VALUE,
|
|
1690
1668
|
"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 };
|
|
1669
|
+
"font-weight": "700",
|
|
1670
|
+
"text-anchor": anchor
|
|
1671
|
+
})
|
|
1672
|
+
]);
|
|
1728
1673
|
}
|
|
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, {
|
|
1674
|
+
function renderBottomStat(x, y, label, value, theme) {
|
|
1675
|
+
return group([
|
|
1676
|
+
text(x, y, label, {
|
|
1744
1677
|
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, {
|
|
1678
|
+
"font-size": FONT_SIZE_STAT_LABEL,
|
|
1679
|
+
"font-family": FONT_FAMILY,
|
|
1680
|
+
"font-weight": "700",
|
|
1681
|
+
"letter-spacing": "0.8"
|
|
1682
|
+
}),
|
|
1683
|
+
text(x, y + 32, value, {
|
|
1755
1684
|
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 };
|
|
1685
|
+
"font-size": 22,
|
|
1686
|
+
"font-family": FONT_FAMILY,
|
|
1687
|
+
"font-weight": "700"
|
|
1688
|
+
})
|
|
1689
|
+
]);
|
|
1763
1690
|
}
|
|
1764
1691
|
|
|
1765
|
-
// packages/renderers/dist/svg/svg-renderer.js
|
|
1766
|
-
var MIN_SVG_WIDTH = 520;
|
|
1767
|
-
|
|
1768
1692
|
class SvgRenderer {
|
|
1769
1693
|
format = "svg";
|
|
1770
1694
|
async render(output, options) {
|
|
1771
1695
|
const theme = getTheme(options.theme);
|
|
1772
|
-
let y = PADDING;
|
|
1773
1696
|
const sections = [];
|
|
1774
1697
|
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
|
-
}
|
|
1698
|
+
let y = PADDING;
|
|
1699
|
+
const providerName = output.providers.length > 0 ? output.providers.map((p) => p.displayName).join(" + ") : "Tokenleak";
|
|
1700
|
+
sections.push(text(PADDING, y + FONT_SIZE_TITLE, providerName, {
|
|
1701
|
+
fill: theme.foreground,
|
|
1702
|
+
"font-size": FONT_SIZE_TITLE,
|
|
1703
|
+
"font-family": FONT_FAMILY,
|
|
1704
|
+
"font-weight": "700"
|
|
1705
|
+
}));
|
|
1706
|
+
sections.push(text(PADDING, y + FONT_SIZE_TITLE + 22, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
|
|
1707
|
+
fill: theme.muted,
|
|
1708
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1709
|
+
"font-family": FONT_FAMILY
|
|
1710
|
+
}));
|
|
1798
1711
|
const allDaily = output.providers.flatMap((p) => p.daily);
|
|
1712
|
+
const heatmap = renderHeatmap(allDaily, theme, {
|
|
1713
|
+
startDate: output.dateRange.since,
|
|
1714
|
+
endDate: output.dateRange.until
|
|
1715
|
+
});
|
|
1716
|
+
sectionWidths.push(heatmap.width);
|
|
1717
|
+
const contentWidth = Math.max(heatmap.width, MIN_SVG_WIDTH - PADDING * 2);
|
|
1718
|
+
const stats = output.aggregated;
|
|
1719
|
+
const headerStatSpacing = 180;
|
|
1720
|
+
const headerStatsX = PADDING + contentWidth;
|
|
1721
|
+
sections.push(renderHeaderStat(headerStatsX, y, "INPUT TOKENS", formatNumber(stats.totalInputTokens), theme));
|
|
1722
|
+
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing, y, "OUTPUT TOKENS", formatNumber(stats.totalOutputTokens), theme));
|
|
1723
|
+
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing * 2, y, "TOTAL TOKENS", formatNumber(stats.totalTokens), theme));
|
|
1724
|
+
y += FONT_SIZE_TITLE + 22 + SECTION_GAP;
|
|
1799
1725
|
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
1726
|
sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1727
|
+
y += heatmap.height + SECTION_GAP + 8;
|
|
1728
|
+
}
|
|
1729
|
+
sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1730
|
+
y += SECTION_GAP;
|
|
1731
|
+
const numCards = 4;
|
|
1732
|
+
const cardWidth = contentWidth / numCards;
|
|
1733
|
+
const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
|
|
1734
|
+
const topModelLabel = topModel ? `${topModel.model} (${formatNumber(topModel.tokens)})` : "N/A";
|
|
1735
|
+
sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
|
|
1736
|
+
const recent30Label = stats.rolling30dTopModel ? `${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})` : formatNumber(stats.rolling30dTokens);
|
|
1737
|
+
sections.push(renderBottomStat(PADDING + cardWidth, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
|
|
1738
|
+
sections.push(renderBottomStat(PADDING + cardWidth * 2, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
|
|
1739
|
+
sections.push(renderBottomStat(PADDING + cardWidth * 3, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
|
|
1740
|
+
y += 56 + SECTION_GAP;
|
|
1741
|
+
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"/>`);
|
|
1742
|
+
sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
|
|
1743
|
+
sections.push(renderBottomStat(PADDING + cardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
|
|
1744
|
+
sections.push(renderBottomStat(PADDING + cardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
|
|
1745
|
+
sections.push(renderBottomStat(PADDING + cardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
|
|
1746
|
+
y += 56 + PADDING;
|
|
1747
|
+
const totalHeight = y;
|
|
1748
|
+
const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
1749
|
+
sections.push(text(svgWidth - PADDING, totalHeight - 16, "tokenleak", {
|
|
1750
|
+
fill: theme.muted,
|
|
1751
|
+
"font-size": 10,
|
|
1818
1752
|
"font-family": FONT_FAMILY,
|
|
1819
|
-
"
|
|
1753
|
+
"text-anchor": "end",
|
|
1754
|
+
opacity: "0.4"
|
|
1820
1755
|
}));
|
|
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
1756
|
const svgContent = [
|
|
1869
1757
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
|
|
1870
|
-
|
|
1758
|
+
`<defs><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');</style></defs>`,
|
|
1759
|
+
rect(0, 0, svgWidth, totalHeight, theme.background, 12),
|
|
1871
1760
|
...sections,
|
|
1872
1761
|
"</svg>"
|
|
1873
1762
|
].join(`
|
|
@@ -2034,7 +1923,7 @@ var BOX_TL = "\u250C";
|
|
|
2034
1923
|
var BOX_TR = "\u2510";
|
|
2035
1924
|
var BOX_BL = "\u2514";
|
|
2036
1925
|
var BOX_BR = "\u2518";
|
|
2037
|
-
var
|
|
1926
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2038
1927
|
var BAR_CHAR = "\u2588";
|
|
2039
1928
|
var MAX_BAR_LENGTH = 20;
|
|
2040
1929
|
function formatTokens(count) {
|
|
@@ -2049,7 +1938,7 @@ function formatTokens(count) {
|
|
|
2049
1938
|
function formatCost2(cost) {
|
|
2050
1939
|
return `$${cost.toFixed(2)}`;
|
|
2051
1940
|
}
|
|
2052
|
-
function
|
|
1941
|
+
function formatPercent(rate) {
|
|
2053
1942
|
return `${(rate * 100).toFixed(1)}%`;
|
|
2054
1943
|
}
|
|
2055
1944
|
function divider(width) {
|
|
@@ -2089,7 +1978,7 @@ function renderStats(stats, width, noColor2) {
|
|
|
2089
1978
|
["7d Cost", formatCost2(stats.rolling7dCost)],
|
|
2090
1979
|
["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
|
|
2091
1980
|
["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
|
|
2092
|
-
["Cache Hit Rate",
|
|
1981
|
+
["Cache Hit Rate", formatPercent(stats.cacheHitRate)],
|
|
2093
1982
|
["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
|
|
2094
1983
|
];
|
|
2095
1984
|
if (stats.peakDay) {
|
|
@@ -2106,7 +1995,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2106
1995
|
const lines = [];
|
|
2107
1996
|
const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
|
|
2108
1997
|
for (const entry of stats.dayOfWeek) {
|
|
2109
|
-
const label =
|
|
1998
|
+
const label = DAY_NAMES[entry.day] ?? `Day${entry.day}`;
|
|
2110
1999
|
const bar = dayBar(entry.tokens, maxTokens, noColor2);
|
|
2111
2000
|
const tokenStr = formatTokens(entry.tokens);
|
|
2112
2001
|
const line = ` ${label} ${bar} ${tokenStr}`;
|
|
@@ -2118,7 +2007,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2118
2007
|
function renderTopModels(stats, width, noColor2) {
|
|
2119
2008
|
const lines = [];
|
|
2120
2009
|
for (const model of stats.topModels.slice(0, 5)) {
|
|
2121
|
-
const pct =
|
|
2010
|
+
const pct = formatPercent(model.percentage);
|
|
2122
2011
|
const tokens = formatTokens(model.tokens);
|
|
2123
2012
|
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2124
2013
|
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
@@ -2132,7 +2021,7 @@ function renderInsights(stats, noColor2) {
|
|
|
2132
2021
|
insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
|
|
2133
2022
|
}
|
|
2134
2023
|
if (stats.cacheHitRate > 0.5) {
|
|
2135
|
-
insights.push(`Cache hit rate is ${
|
|
2024
|
+
insights.push(`Cache hit rate is ${formatPercent(stats.cacheHitRate)} - good cache reuse.`);
|
|
2136
2025
|
}
|
|
2137
2026
|
if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
|
|
2138
2027
|
insights.push("Cache hit rate is low - consider enabling prompt caching.");
|