llm-usage-metrics 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.js +674 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2459,6 +2459,214 @@ function createDefaultAdapters(options) {
|
|
|
2459
2459
|
return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
|
|
2460
2460
|
}
|
|
2461
2461
|
|
|
2462
|
+
// src/render/share-svg-theme.ts
|
|
2463
|
+
var shareTheme = {
|
|
2464
|
+
bg: "#0d1117",
|
|
2465
|
+
cardBg: "#161b22",
|
|
2466
|
+
cardBorder: "#30363d",
|
|
2467
|
+
textPrimary: "#e6edf3",
|
|
2468
|
+
textSecondary: "#8b949e",
|
|
2469
|
+
textMuted: "#484f58",
|
|
2470
|
+
gridLine: "#21262d",
|
|
2471
|
+
font: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
2472
|
+
mono: "ui-monospace, 'SF Mono', 'Fira Code', monospace"
|
|
2473
|
+
};
|
|
2474
|
+
var knownSourceColors = {
|
|
2475
|
+
pi: "#ec4899",
|
|
2476
|
+
codex: "#22c55e",
|
|
2477
|
+
gemini: "#eab308",
|
|
2478
|
+
droid: "#3b82f6",
|
|
2479
|
+
opencode: "#a855f7"
|
|
2480
|
+
};
|
|
2481
|
+
var fallbackColors = ["#f97316", "#06b6d4", "#ef4444", "#84cc16", "#f43f5e"];
|
|
2482
|
+
function getSourceColor(source, index) {
|
|
2483
|
+
return knownSourceColors[source] ?? fallbackColors[index % fallbackColors.length];
|
|
2484
|
+
}
|
|
2485
|
+
function escapeSvg(value) {
|
|
2486
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2487
|
+
}
|
|
2488
|
+
function formatCompact(n) {
|
|
2489
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
|
|
2490
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
|
|
2491
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
|
|
2492
|
+
return String(n);
|
|
2493
|
+
}
|
|
2494
|
+
var intFmt = new Intl.NumberFormat("en-US");
|
|
2495
|
+
var decFmt = new Intl.NumberFormat("en-US", {
|
|
2496
|
+
minimumFractionDigits: 2,
|
|
2497
|
+
maximumFractionDigits: 2
|
|
2498
|
+
});
|
|
2499
|
+
var usdFmt = new Intl.NumberFormat("en-US", {
|
|
2500
|
+
style: "currency",
|
|
2501
|
+
currency: "USD",
|
|
2502
|
+
minimumFractionDigits: 2,
|
|
2503
|
+
maximumFractionDigits: 2
|
|
2504
|
+
});
|
|
2505
|
+
function formatInteger(n) {
|
|
2506
|
+
return intFmt.format(n);
|
|
2507
|
+
}
|
|
2508
|
+
function formatDecimal(n) {
|
|
2509
|
+
return n === void 0 ? "-" : decFmt.format(n);
|
|
2510
|
+
}
|
|
2511
|
+
function formatUsd(n) {
|
|
2512
|
+
return n === void 0 ? "-" : usdFmt.format(n);
|
|
2513
|
+
}
|
|
2514
|
+
function catmullRom(points, tension = 0.3, yFloor) {
|
|
2515
|
+
if (points.length < 2) return "";
|
|
2516
|
+
const clamp = (y) => yFloor !== void 0 ? Math.min(y, yFloor) : y;
|
|
2517
|
+
let d = `M${points[0].x.toFixed(2)},${points[0].y.toFixed(2)}`;
|
|
2518
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
2519
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
2520
|
+
const p1 = points[i];
|
|
2521
|
+
const p2 = points[i + 1];
|
|
2522
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
2523
|
+
const cp1x = p1.x + (p2.x - p0.x) * tension / 3;
|
|
2524
|
+
const cp1y = clamp(p1.y + (p2.y - p0.y) * tension / 3);
|
|
2525
|
+
const cp2x = p2.x - (p3.x - p1.x) * tension / 3;
|
|
2526
|
+
const cp2y = clamp(p2.y - (p3.y - p1.y) * tension / 3);
|
|
2527
|
+
d += ` C${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
|
|
2528
|
+
}
|
|
2529
|
+
return d;
|
|
2530
|
+
}
|
|
2531
|
+
function scaleY(value, max, top, bottom) {
|
|
2532
|
+
if (max <= 0) return bottom;
|
|
2533
|
+
return bottom - Math.max(0, value) / max * (bottom - top);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// src/render/render-efficiency-share-svg.ts
|
|
2537
|
+
var W = 1500;
|
|
2538
|
+
var H = 640;
|
|
2539
|
+
var ACCENT_H = 4;
|
|
2540
|
+
var FOOTER_H = 36;
|
|
2541
|
+
var pad = { top: 160, right: 130, bottom: 70 + FOOTER_H, left: 110 };
|
|
2542
|
+
var chartColors = {
|
|
2543
|
+
commits: "#8b949e",
|
|
2544
|
+
usdPerCommit: "#f97316"
|
|
2545
|
+
};
|
|
2546
|
+
function toMonthlyRows(rows) {
|
|
2547
|
+
return rows.filter((row) => row.rowType === "period").sort((a, b) => a.periodKey.localeCompare(b.periodKey));
|
|
2548
|
+
}
|
|
2549
|
+
function toAllRow(rows) {
|
|
2550
|
+
return rows.find((row) => row.periodKey === "ALL");
|
|
2551
|
+
}
|
|
2552
|
+
function renderSummaryStats(allRow, vcenter) {
|
|
2553
|
+
const cost = formatUsd(allRow?.costUsd);
|
|
2554
|
+
const commits = formatInteger(allRow?.commitCount ?? 0);
|
|
2555
|
+
const usdPerCommit = formatUsd(allRow?.usdPerCommit);
|
|
2556
|
+
const tokPerCommit = formatDecimal(allRow?.tokensPerCommit);
|
|
2557
|
+
const y = vcenter;
|
|
2558
|
+
const items = [
|
|
2559
|
+
{ label: "Total Cost", value: cost, x: pad.left },
|
|
2560
|
+
{ label: "Commits", value: commits, x: 280 },
|
|
2561
|
+
{ label: "$/Commit", value: usdPerCommit, x: 480 },
|
|
2562
|
+
{ label: "Tokens/Commit", value: tokPerCommit, x: 680 }
|
|
2563
|
+
];
|
|
2564
|
+
return items.map(
|
|
2565
|
+
(item) => `<text x="${item.x}" y="${y}" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(item.label)}</text><text x="${item.x}" y="${y + 20}" font-size="18" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">${escapeSvg(item.value)}</text>`
|
|
2566
|
+
).join("\n");
|
|
2567
|
+
}
|
|
2568
|
+
function renderLegend(x, y) {
|
|
2569
|
+
const items = [
|
|
2570
|
+
{ label: "Commits", color: chartColors.commits, shape: "rect" },
|
|
2571
|
+
{ label: "$ / Commit", color: chartColors.usdPerCommit, shape: "line" }
|
|
2572
|
+
];
|
|
2573
|
+
return items.map((item, i) => {
|
|
2574
|
+
const ix = x + i * 180;
|
|
2575
|
+
const shape = item.shape === "rect" ? `<rect x="${ix}" y="${y - 6}" width="14" height="14" rx="3" fill="${item.color}" opacity="0.5"/>` : `<line x1="${ix}" y1="${y + 1}" x2="${ix + 14}" y2="${y + 1}" stroke="${item.color}" stroke-width="3" stroke-linecap="round"/>`;
|
|
2576
|
+
return `${shape}<text x="${ix + 20}" y="${y + 5}" font-size="13" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${escapeSvg(item.label)}</text>`;
|
|
2577
|
+
}).join("\n");
|
|
2578
|
+
}
|
|
2579
|
+
function renderCommitBarLabels(monthlyRows, chartLeft, stepX, maxCommits, chartTop, chartBottom) {
|
|
2580
|
+
return monthlyRows.map((row, i) => {
|
|
2581
|
+
const x = chartLeft + i * stepX;
|
|
2582
|
+
const yTop = scaleY(row.commitCount, maxCommits, chartTop, chartBottom);
|
|
2583
|
+
return `<text x="${x.toFixed(2)}" y="${(yTop - 8).toFixed(0)}" text-anchor="middle" font-size="12" font-weight="600" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${escapeSvg(formatInteger(row.commitCount))}</text>`;
|
|
2584
|
+
}).join("\n");
|
|
2585
|
+
}
|
|
2586
|
+
function renderUsdDataLabels(monthlyRows, usdPoints) {
|
|
2587
|
+
return monthlyRows.map((row, i) => {
|
|
2588
|
+
const p = usdPoints[i];
|
|
2589
|
+
const val = row.usdPerCommit;
|
|
2590
|
+
if (val === void 0) return "";
|
|
2591
|
+
const labelY = p.y - 14;
|
|
2592
|
+
return `<text x="${p.x.toFixed(2)}" y="${labelY.toFixed(0)}" text-anchor="middle" font-size="12" font-weight="600" fill="${chartColors.usdPerCommit}" font-family="${shareTheme.font}">${escapeSvg(formatUsd(val))}</text>`;
|
|
2593
|
+
}).join("\n");
|
|
2594
|
+
}
|
|
2595
|
+
function renderEfficiencyMonthlyShareSvg(efficiencyData) {
|
|
2596
|
+
const monthlyRows = toMonthlyRows(efficiencyData.rows);
|
|
2597
|
+
const allRow = toAllRow(efficiencyData.rows);
|
|
2598
|
+
const chartLeft = pad.left;
|
|
2599
|
+
const chartTop = pad.top;
|
|
2600
|
+
const chartRight = W - pad.right;
|
|
2601
|
+
const chartBottom = H - pad.bottom;
|
|
2602
|
+
const chartW = chartRight - chartLeft;
|
|
2603
|
+
const count = Math.max(1, monthlyRows.length);
|
|
2604
|
+
const stepX = count === 1 ? 0 : chartW / (count - 1);
|
|
2605
|
+
const maxCommits = Math.max(1, ...monthlyRows.map((r) => r.commitCount));
|
|
2606
|
+
const actualMaxUsd = Math.max(0, ...monthlyRows.map((r) => Math.max(0, r.usdPerCommit ?? 0)));
|
|
2607
|
+
const scaleMaxUsd = Math.max(0.01, actualMaxUsd);
|
|
2608
|
+
const barWidth = Math.min(48, Math.max(18, chartW / (count * 2.2)));
|
|
2609
|
+
const commitBars = monthlyRows.map((row, i) => {
|
|
2610
|
+
const x = chartLeft + i * stepX;
|
|
2611
|
+
const yTop = scaleY(row.commitCount, maxCommits, chartTop, chartBottom);
|
|
2612
|
+
return `<rect x="${(x - barWidth / 2).toFixed(2)}" y="${yTop.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${(chartBottom - yTop).toFixed(2)}" rx="4" fill="${chartColors.commits}" fill-opacity="0.35"/>`;
|
|
2613
|
+
}).join("\n");
|
|
2614
|
+
const usdPoints = monthlyRows.map((row, i) => ({
|
|
2615
|
+
x: chartLeft + i * stepX,
|
|
2616
|
+
y: scaleY(row.usdPerCommit ?? 0, scaleMaxUsd, chartTop, chartBottom)
|
|
2617
|
+
}));
|
|
2618
|
+
const usdLine = usdPoints.length >= 2 ? `<path d="${catmullRom(usdPoints, 0.3, chartBottom)}" fill="none" stroke="${chartColors.usdPerCommit}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>` : "";
|
|
2619
|
+
const dotRadius = count <= 4 ? 6 : 4.5;
|
|
2620
|
+
const usdDots = usdPoints.map(
|
|
2621
|
+
(p) => `<circle cx="${p.x.toFixed(2)}" cy="${p.y.toFixed(2)}" r="${dotRadius}" fill="${chartColors.usdPerCommit}"/>`
|
|
2622
|
+
).join("\n");
|
|
2623
|
+
const monthLabels = monthlyRows.map((row, i) => {
|
|
2624
|
+
const x = chartLeft + i * stepX;
|
|
2625
|
+
return `<text x="${x.toFixed(2)}" y="${(chartBottom + 28).toFixed(0)}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(row.periodKey)}</text>`;
|
|
2626
|
+
}).join("\n");
|
|
2627
|
+
const axisLabels = [
|
|
2628
|
+
`<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartTop - 10).toFixed(0)}" text-anchor="end" font-size="12" font-weight="600" fill="${chartColors.commits}" font-family="${shareTheme.font}">Commits</text>`,
|
|
2629
|
+
`<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartTop + 5).toFixed(0)}" text-anchor="end" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(formatInteger(maxCommits))}</text>`,
|
|
2630
|
+
`<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartBottom + 5).toFixed(0)}" text-anchor="end" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">0</text>`,
|
|
2631
|
+
`<text x="${(chartRight + 12).toFixed(0)}" y="${(chartTop - 10).toFixed(0)}" font-size="12" font-weight="600" fill="${chartColors.usdPerCommit}" font-family="${shareTheme.font}">$/Commit</text>`,
|
|
2632
|
+
`<text x="${(chartRight + 12).toFixed(0)}" y="${(chartTop + 5).toFixed(0)}" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(formatUsd(actualMaxUsd))}</text>`,
|
|
2633
|
+
`<text x="${(chartRight + 12).toFixed(0)}" y="${(chartBottom + 5).toFixed(0)}" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">$0.00</text>`
|
|
2634
|
+
].join("\n");
|
|
2635
|
+
const noData = monthlyRows.length === 0 ? `<text x="${(W / 2).toFixed(0)}" y="${(H / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No monthly efficiency data available</text>` : "";
|
|
2636
|
+
const vcenter = 70;
|
|
2637
|
+
const commandText = "llm-usage efficiency monthly --share";
|
|
2638
|
+
const badgeW = commandText.length * 9.5 + 28;
|
|
2639
|
+
const badgeX = W - pad.right - badgeW;
|
|
2640
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
2641
|
+
<defs>
|
|
2642
|
+
<linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
|
|
2643
|
+
<stop offset="0%" stop-color="#10b981"/>
|
|
2644
|
+
<stop offset="100%" stop-color="#06b6d4"/>
|
|
2645
|
+
</linearGradient>
|
|
2646
|
+
</defs>
|
|
2647
|
+
<rect width="${W}" height="${H}" fill="${shareTheme.bg}"/>
|
|
2648
|
+
<rect width="${W}" height="${ACCENT_H}" fill="url(#accent-grad)"/>
|
|
2649
|
+
<text x="${pad.left}" y="52" font-size="32" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Monthly Efficiency</text>
|
|
2650
|
+
<rect x="${badgeX.toFixed(0)}" y="30" width="${badgeW.toFixed(0)}" height="34" rx="17" fill="none" stroke="${shareTheme.cardBorder}"/>
|
|
2651
|
+
<text x="${(badgeX + badgeW / 2).toFixed(0)}" y="52" text-anchor="middle" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(commandText)}</text>
|
|
2652
|
+
${renderSummaryStats(allRow, vcenter)}
|
|
2653
|
+
${renderLegend(pad.left, vcenter + 50)}
|
|
2654
|
+
<line x1="${chartLeft}" y1="${chartBottom}" x2="${chartRight}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
|
|
2655
|
+
<line x1="${chartLeft}" y1="${chartTop}" x2="${chartLeft}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
|
|
2656
|
+
<line x1="${chartRight}" y1="${chartTop}" x2="${chartRight}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
|
|
2657
|
+
${axisLabels}
|
|
2658
|
+
${commitBars}
|
|
2659
|
+
${renderCommitBarLabels(monthlyRows, chartLeft, stepX, maxCommits, chartTop, chartBottom)}
|
|
2660
|
+
${usdLine}
|
|
2661
|
+
${usdDots}
|
|
2662
|
+
${renderUsdDataLabels(monthlyRows, usdPoints)}
|
|
2663
|
+
${monthLabels}
|
|
2664
|
+
${noData}
|
|
2665
|
+
<line x1="0" y1="${H - FOOTER_H + 1}" x2="${W}" y2="${H - FOOTER_H + 1}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
|
|
2666
|
+
<text x="60" y="${H - FOOTER_H / 2 + 5}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>
|
|
2667
|
+
</svg>`;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2462
2670
|
// src/utils/time-buckets.ts
|
|
2463
2671
|
var formatterCache = /* @__PURE__ */ new Map();
|
|
2464
2672
|
function getDateFormatter(timezone) {
|
|
@@ -2783,8 +2991,7 @@ function computeDerivedMetrics(usage, outcomes) {
|
|
|
2783
2991
|
return {
|
|
2784
2992
|
usdPerCommit: costUsd !== void 0 && outcomes.commitCount > 0 ? costUsd / outcomes.commitCount : void 0,
|
|
2785
2993
|
usdPer1kLinesChanged: costUsd !== void 0 && outcomes.linesChanged > 0 ? costUsd / (outcomes.linesChanged / 1e3) : void 0,
|
|
2786
|
-
tokensPerCommit: outcomes.commitCount > 0 ?
|
|
2787
|
-
nonCacheTokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
|
|
2994
|
+
tokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
|
|
2788
2995
|
commitsPerUsd: costUsd !== void 0 && costUsd > 0 ? outcomes.commitCount / costUsd : void 0
|
|
2789
2996
|
};
|
|
2790
2997
|
}
|
|
@@ -5129,6 +5336,15 @@ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
|
|
|
5129
5336
|
}
|
|
5130
5337
|
}
|
|
5131
5338
|
|
|
5339
|
+
// src/cli/share-artifact.ts
|
|
5340
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
5341
|
+
import path13 from "path";
|
|
5342
|
+
async function writeShareSvgFile(fileName, svgContent) {
|
|
5343
|
+
const outputPath = path13.resolve(process.cwd(), fileName);
|
|
5344
|
+
await writeFile4(outputPath, svgContent, "utf8");
|
|
5345
|
+
return outputPath;
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5132
5348
|
// src/render/table-text-layout.ts
|
|
5133
5349
|
var ansiEscapePattern = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
|
|
5134
5350
|
var combiningMarkPattern = /\p{Mark}/u;
|
|
@@ -5379,8 +5595,7 @@ var efficiencyTableHeaders = [
|
|
|
5379
5595
|
"Cost",
|
|
5380
5596
|
"$/Commit",
|
|
5381
5597
|
"$/1k Lines",
|
|
5382
|
-
"
|
|
5383
|
-
"Non-Cache/Commit",
|
|
5598
|
+
"Tokens/Commit",
|
|
5384
5599
|
"Commits/$"
|
|
5385
5600
|
];
|
|
5386
5601
|
var integerFormatter = new Intl.NumberFormat("en-US");
|
|
@@ -5400,10 +5615,10 @@ var usdRateFormatter = new Intl.NumberFormat("en-US", {
|
|
|
5400
5615
|
minimumFractionDigits: 4,
|
|
5401
5616
|
maximumFractionDigits: 4
|
|
5402
5617
|
});
|
|
5403
|
-
function
|
|
5618
|
+
function formatInteger2(value) {
|
|
5404
5619
|
return integerFormatter.format(value);
|
|
5405
5620
|
}
|
|
5406
|
-
function
|
|
5621
|
+
function formatUsd2(value, options = {}) {
|
|
5407
5622
|
if (value === void 0) {
|
|
5408
5623
|
return "-";
|
|
5409
5624
|
}
|
|
@@ -5417,7 +5632,7 @@ function formatUsdRate(value, options = {}) {
|
|
|
5417
5632
|
const formatted = usdRateFormatter.format(value);
|
|
5418
5633
|
return options.approximate ? `~${formatted}` : formatted;
|
|
5419
5634
|
}
|
|
5420
|
-
function
|
|
5635
|
+
function formatDecimal2(value, options = {}) {
|
|
5421
5636
|
if (value === void 0) {
|
|
5422
5637
|
return "-";
|
|
5423
5638
|
}
|
|
@@ -5427,22 +5642,21 @@ function formatDecimal(value, options = {}) {
|
|
|
5427
5642
|
function toEfficiencyTableCells(rows) {
|
|
5428
5643
|
return rows.map((row) => [
|
|
5429
5644
|
row.periodKey,
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5645
|
+
formatInteger2(row.commitCount),
|
|
5646
|
+
formatInteger2(row.linesAdded),
|
|
5647
|
+
formatInteger2(row.linesDeleted),
|
|
5648
|
+
formatInteger2(row.linesChanged),
|
|
5649
|
+
formatInteger2(row.inputTokens),
|
|
5650
|
+
formatInteger2(row.outputTokens),
|
|
5651
|
+
formatInteger2(row.reasoningTokens),
|
|
5652
|
+
formatInteger2(row.cacheReadTokens),
|
|
5653
|
+
formatInteger2(row.cacheWriteTokens),
|
|
5654
|
+
formatInteger2(row.totalTokens),
|
|
5655
|
+
formatUsd2(row.costUsd, { approximate: row.costIncomplete }),
|
|
5441
5656
|
formatUsdRate(row.usdPerCommit, { approximate: row.costIncomplete }),
|
|
5442
5657
|
formatUsdRate(row.usdPer1kLinesChanged, { approximate: row.costIncomplete }),
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
formatDecimal(row.commitsPerUsd, { approximate: row.costIncomplete })
|
|
5658
|
+
formatDecimal2(row.tokensPerCommit),
|
|
5659
|
+
formatDecimal2(row.commitsPerUsd, { approximate: row.costIncomplete })
|
|
5446
5660
|
]);
|
|
5447
5661
|
}
|
|
5448
5662
|
|
|
@@ -5553,7 +5767,7 @@ function formatSource(row) {
|
|
|
5553
5767
|
function formatTokenCount(value) {
|
|
5554
5768
|
return integerFormatter2.format(value ?? 0);
|
|
5555
5769
|
}
|
|
5556
|
-
function
|
|
5770
|
+
function formatUsd3(value, options = {}) {
|
|
5557
5771
|
if (value === void 0) {
|
|
5558
5772
|
return "-";
|
|
5559
5773
|
}
|
|
@@ -5588,13 +5802,13 @@ function formatModelMetric(row, selector, formatter, layout) {
|
|
|
5588
5802
|
}
|
|
5589
5803
|
function formatModelCostMetric(row, layout) {
|
|
5590
5804
|
if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
|
|
5591
|
-
return
|
|
5805
|
+
return formatUsd3(row.costUsd, { incomplete: row.costIncomplete });
|
|
5592
5806
|
}
|
|
5593
5807
|
const lines = row.modelBreakdown.map(
|
|
5594
|
-
(modelUsage) =>
|
|
5808
|
+
(modelUsage) => formatUsd3(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
|
|
5595
5809
|
);
|
|
5596
5810
|
if (row.modelBreakdown.length > 1) {
|
|
5597
|
-
lines.push(
|
|
5811
|
+
lines.push(formatUsd3(row.costUsd, { incomplete: row.costIncomplete }));
|
|
5598
5812
|
}
|
|
5599
5813
|
return lines.join("\n");
|
|
5600
5814
|
}
|
|
@@ -6198,13 +6412,23 @@ function resolveReportFormat(options) {
|
|
|
6198
6412
|
}
|
|
6199
6413
|
return "terminal";
|
|
6200
6414
|
}
|
|
6415
|
+
function validateShareOption(granularity, options) {
|
|
6416
|
+
if (!options.share) {
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
if (granularity !== "monthly") {
|
|
6420
|
+
throw new Error("--share is only supported for efficiency monthly");
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6201
6423
|
async function prepareEfficiencyReport(granularity, options) {
|
|
6202
6424
|
validateOutputFormatOptions(options);
|
|
6425
|
+
validateShareOption(granularity, options);
|
|
6203
6426
|
const efficiencyData = await buildEfficiencyData(granularity, options);
|
|
6204
6427
|
const format = resolveReportFormat(options);
|
|
6205
6428
|
return {
|
|
6206
6429
|
format,
|
|
6207
6430
|
diagnostics: efficiencyData.diagnostics,
|
|
6431
|
+
shareSvg: options.share ? renderEfficiencyMonthlyShareSvg(efficiencyData) : void 0,
|
|
6208
6432
|
output: renderEfficiencyReport(efficiencyData, format, {
|
|
6209
6433
|
granularity
|
|
6210
6434
|
})
|
|
@@ -6229,9 +6453,141 @@ async function runEfficiencyReport(granularity, options) {
|
|
|
6229
6453
|
logger.warn(message);
|
|
6230
6454
|
});
|
|
6231
6455
|
}
|
|
6456
|
+
if (preparedReport.shareSvg) {
|
|
6457
|
+
const outputPath = await writeShareSvgFile(
|
|
6458
|
+
"efficiency-monthly-share.svg",
|
|
6459
|
+
preparedReport.shareSvg
|
|
6460
|
+
);
|
|
6461
|
+
logger.info(`Wrote efficiency share SVG: ${outputPath}`);
|
|
6462
|
+
}
|
|
6232
6463
|
console.log(preparedReport.output);
|
|
6233
6464
|
}
|
|
6234
6465
|
|
|
6466
|
+
// src/render/render-optimize-share-svg.ts
|
|
6467
|
+
var W2 = 1500;
|
|
6468
|
+
var H2 = 780;
|
|
6469
|
+
var ACCENT_H2 = 4;
|
|
6470
|
+
var FOOTER_H2 = 36;
|
|
6471
|
+
var pad2 = { top: 180, right: 70, bottom: 60 + FOOTER_H2, left: 260 };
|
|
6472
|
+
function formatPercent(value) {
|
|
6473
|
+
if (value === void 0) return "-";
|
|
6474
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
6475
|
+
}
|
|
6476
|
+
function cellFill(value) {
|
|
6477
|
+
if (value === void 0) return "rgba(139,148,158,0.12)";
|
|
6478
|
+
const mag = Math.min(1, Math.abs(value));
|
|
6479
|
+
const alpha = 0.22 + mag * 0.58;
|
|
6480
|
+
if (value > 0) return `rgba(34,197,94,${alpha.toFixed(3)})`;
|
|
6481
|
+
if (value < 0) return `rgba(239,68,68,${alpha.toFixed(3)})`;
|
|
6482
|
+
return "rgba(139,148,158,0.15)";
|
|
6483
|
+
}
|
|
6484
|
+
function cellTextFill(value) {
|
|
6485
|
+
if (value === void 0) return shareTheme.textMuted;
|
|
6486
|
+
if (Math.abs(value) >= 0.35) return "#ffffff";
|
|
6487
|
+
return shareTheme.textPrimary;
|
|
6488
|
+
}
|
|
6489
|
+
function toCandidateRows(data) {
|
|
6490
|
+
return data.rows.filter((r) => r.rowType === "candidate");
|
|
6491
|
+
}
|
|
6492
|
+
function sortPeriodKeys(keys) {
|
|
6493
|
+
return [...keys].sort(compareByCodePoint);
|
|
6494
|
+
}
|
|
6495
|
+
function renderOptimizeMonthlyShareSvg(optimizeData) {
|
|
6496
|
+
const candidateRows = toCandidateRows(optimizeData);
|
|
6497
|
+
const periodKeys = sortPeriodKeys(
|
|
6498
|
+
new Set(candidateRows.map((r) => r.periodKey).filter((k) => k !== "ALL"))
|
|
6499
|
+
);
|
|
6500
|
+
const candidateModels = [...new Set(candidateRows.map((r) => r.candidateModel))].sort(
|
|
6501
|
+
compareByCodePoint
|
|
6502
|
+
);
|
|
6503
|
+
const cellMap = /* @__PURE__ */ new Map();
|
|
6504
|
+
for (const row of candidateRows) {
|
|
6505
|
+
cellMap.set(`${row.candidateModel}__${row.periodKey}`, row);
|
|
6506
|
+
}
|
|
6507
|
+
const allByCandidate = /* @__PURE__ */ new Map();
|
|
6508
|
+
for (const row of candidateRows) {
|
|
6509
|
+
if (row.periodKey === "ALL") allByCandidate.set(row.candidateModel, row);
|
|
6510
|
+
}
|
|
6511
|
+
const chartLeft = pad2.left;
|
|
6512
|
+
const chartTop = pad2.top;
|
|
6513
|
+
const chartRight = W2 - pad2.right;
|
|
6514
|
+
const chartBottom = H2 - pad2.bottom;
|
|
6515
|
+
const chartW = chartRight - chartLeft;
|
|
6516
|
+
const chartH = chartBottom - chartTop;
|
|
6517
|
+
const rowCount = Math.max(1, candidateModels.length);
|
|
6518
|
+
const colCount = Math.max(1, periodKeys.length);
|
|
6519
|
+
const cellW = chartW / colCount;
|
|
6520
|
+
const cellH = chartH / rowCount;
|
|
6521
|
+
const gridCells = [];
|
|
6522
|
+
const colLabels = [];
|
|
6523
|
+
const rowLabels = [];
|
|
6524
|
+
for (let c = 0; c < periodKeys.length; c++) {
|
|
6525
|
+
const x = chartLeft + c * cellW + cellW / 2;
|
|
6526
|
+
colLabels.push(
|
|
6527
|
+
`<text x="${x.toFixed(2)}" y="${(chartTop - 14).toFixed(0)}" text-anchor="middle" font-size="14" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${escapeSvg(periodKeys[c])}</text>`
|
|
6528
|
+
);
|
|
6529
|
+
}
|
|
6530
|
+
for (let r = 0; r < candidateModels.length; r++) {
|
|
6531
|
+
const model = candidateModels[r];
|
|
6532
|
+
const y = chartTop + r * cellH + cellH / 2 + 5;
|
|
6533
|
+
const allRow = allByCandidate.get(model);
|
|
6534
|
+
const allLabel = allRow?.savingsUsd === void 0 ? "-" : formatUsd(Math.abs(allRow.savingsUsd));
|
|
6535
|
+
const allColor = allRow?.savingsUsd === void 0 ? shareTheme.textMuted : allRow.savingsUsd > 0 ? "#22c55e" : allRow.savingsUsd < 0 ? "#ef4444" : shareTheme.textSecondary;
|
|
6536
|
+
const prefix = allRow?.savingsUsd === void 0 ? "" : allRow.savingsUsd > 0 ? "+" : allRow.savingsUsd < 0 ? "-" : "";
|
|
6537
|
+
rowLabels.push(
|
|
6538
|
+
`<text x="${(chartLeft - 16).toFixed(0)}" y="${y.toFixed(0)}" text-anchor="end" font-size="14" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">${escapeSvg(model)}</text>`
|
|
6539
|
+
);
|
|
6540
|
+
rowLabels.push(
|
|
6541
|
+
`<text x="${(chartLeft - 16).toFixed(0)}" y="${(y + 16).toFixed(0)}" text-anchor="end" font-size="12" fill="${allColor}" font-family="${shareTheme.font}">ALL: ${escapeSvg(prefix + allLabel)}</text>`
|
|
6542
|
+
);
|
|
6543
|
+
for (let c = 0; c < periodKeys.length; c++) {
|
|
6544
|
+
const row = cellMap.get(`${model}__${periodKeys[c]}`);
|
|
6545
|
+
const x = chartLeft + c * cellW;
|
|
6546
|
+
const yTop = chartTop + r * cellH;
|
|
6547
|
+
const pct = row?.savingsPct;
|
|
6548
|
+
gridCells.push(
|
|
6549
|
+
`<rect x="${(x + 2).toFixed(2)}" y="${(yTop + 2).toFixed(2)}" width="${Math.max(0, cellW - 4).toFixed(2)}" height="${Math.max(0, cellH - 4).toFixed(2)}" fill="${cellFill(pct)}" rx="6"/>`
|
|
6550
|
+
);
|
|
6551
|
+
gridCells.push(
|
|
6552
|
+
`<text x="${(x + cellW / 2).toFixed(2)}" y="${(yTop + cellH / 2 + 5).toFixed(2)}" text-anchor="middle" font-size="13" font-weight="600" fill="${cellTextFill(pct)}" font-family="${shareTheme.font}">${escapeSvg(formatPercent(pct))}</text>`
|
|
6553
|
+
);
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
const provider = optimizeData.diagnostics.provider;
|
|
6557
|
+
const missing = optimizeData.diagnostics.candidatesWithMissingPricing;
|
|
6558
|
+
const warning = optimizeData.diagnostics.warning ?? "";
|
|
6559
|
+
const commandText = "llm-usage optimize monthly --share";
|
|
6560
|
+
const badgeW = commandText.length * 9.5 + 28;
|
|
6561
|
+
const badgeX = W2 - pad2.right - badgeW;
|
|
6562
|
+
const noData = candidateModels.length === 0 || periodKeys.length === 0 ? `<text x="${(W2 / 2).toFixed(0)}" y="${(H2 / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No monthly optimize data available</text>` : "";
|
|
6563
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W2}" height="${H2}" viewBox="0 0 ${W2} ${H2}">
|
|
6564
|
+
<defs>
|
|
6565
|
+
<linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
|
|
6566
|
+
<stop offset="0%" stop-color="#10b981"/>
|
|
6567
|
+
<stop offset="100%" stop-color="#06b6d4"/>
|
|
6568
|
+
</linearGradient>
|
|
6569
|
+
</defs>
|
|
6570
|
+
<rect width="${W2}" height="${H2}" fill="${shareTheme.bg}"/>
|
|
6571
|
+
<rect width="${W2}" height="${ACCENT_H2}" fill="url(#accent-grad)"/>
|
|
6572
|
+
<text x="${pad2.left}" y="52" font-size="32" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Monthly Optimize</text>
|
|
6573
|
+
<text x="${pad2.left}" y="78" font-size="15" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">Savings % heatmap by candidate and month</text>
|
|
6574
|
+
<rect x="${badgeX.toFixed(0)}" y="30" width="${badgeW.toFixed(0)}" height="34" rx="17" fill="none" stroke="${shareTheme.cardBorder}"/>
|
|
6575
|
+
<text x="${(badgeX + badgeW / 2).toFixed(0)}" y="52" text-anchor="middle" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(commandText)}</text>
|
|
6576
|
+
<text x="${pad2.left}" y="112" font-size="15" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Provider: <tspan font-weight="700">${escapeSvg(provider)}</tspan></text>
|
|
6577
|
+
<text x="${pad2.left + 280}" y="112" font-size="14" fill="#22c55e" font-family="${shareTheme.font}">\u25CF positive = savings</text>
|
|
6578
|
+
<text x="${pad2.left + 480}" y="112" font-size="14" fill="#ef4444" font-family="${shareTheme.font}">\u25CF negative = higher cost</text>
|
|
6579
|
+
${missing.length > 0 ? `<text x="${pad2.left}" y="136" font-size="13" fill="#eab308" font-family="${shareTheme.font}">Missing pricing: ${escapeSvg(missing.join(", "))}</text>` : ""}
|
|
6580
|
+
${warning ? `<text x="${pad2.left}" y="158" font-size="13" fill="#eab308" font-family="${shareTheme.font}">${escapeSvg(warning)}</text>` : ""}
|
|
6581
|
+
<rect x="${chartLeft}" y="${chartTop}" width="${chartW}" height="${chartH}" fill="${shareTheme.cardBg}" rx="10"/>
|
|
6582
|
+
${gridCells.join("\n")}
|
|
6583
|
+
${colLabels.join("\n")}
|
|
6584
|
+
${rowLabels.join("\n")}
|
|
6585
|
+
${noData}
|
|
6586
|
+
<line x1="0" y1="${H2 - FOOTER_H2 + 1}" x2="${W2}" y2="${H2 - FOOTER_H2 + 1}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
|
|
6587
|
+
<text x="60" y="${H2 - FOOTER_H2 / 2 + 5}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>
|
|
6588
|
+
</svg>`;
|
|
6589
|
+
}
|
|
6590
|
+
|
|
6235
6591
|
// src/render/render-optimize-report.ts
|
|
6236
6592
|
import { markdownTable as markdownTable2 } from "markdown-table";
|
|
6237
6593
|
import pc6 from "picocolors";
|
|
@@ -6268,14 +6624,14 @@ function getReportTitle2(granularity) {
|
|
|
6268
6624
|
return "Monthly Optimize Report";
|
|
6269
6625
|
}
|
|
6270
6626
|
}
|
|
6271
|
-
function
|
|
6627
|
+
function formatUsd4(value, options = {}) {
|
|
6272
6628
|
if (value === void 0) {
|
|
6273
6629
|
return "-";
|
|
6274
6630
|
}
|
|
6275
6631
|
const formatted = usdFormatter3.format(value);
|
|
6276
6632
|
return options.approximate ? `~${formatted}` : formatted;
|
|
6277
6633
|
}
|
|
6278
|
-
function
|
|
6634
|
+
function formatPercent2(value) {
|
|
6279
6635
|
if (value === void 0) {
|
|
6280
6636
|
return "-";
|
|
6281
6637
|
}
|
|
@@ -6329,7 +6685,7 @@ function resolveTerminalContextLines(optimizeData, options) {
|
|
|
6329
6685
|
lines.push(options.useColor ? pc6.cyan(providerLine) : providerLine);
|
|
6330
6686
|
if (allBaselineRow) {
|
|
6331
6687
|
lines.push(
|
|
6332
|
-
`ALL baseline cost: ${
|
|
6688
|
+
`ALL baseline cost: ${formatUsd4(allBaselineRow.baselineCostUsd, { approximate: allBaselineRow.baselineCostIncomplete })}`
|
|
6333
6689
|
);
|
|
6334
6690
|
}
|
|
6335
6691
|
if (allCandidateRows.length > 0) {
|
|
@@ -6341,11 +6697,11 @@ function resolveTerminalContextLines(optimizeData, options) {
|
|
|
6341
6697
|
lines.push("ALL best candidate: unavailable (missing baseline or candidate pricing)");
|
|
6342
6698
|
} else if (bestRow.savingsUsd > 0) {
|
|
6343
6699
|
lines.push(
|
|
6344
|
-
`ALL best candidate: ${bestRow.candidateModel} saves ${formatAbsoluteUsd(bestRow.savingsUsd)} (${
|
|
6700
|
+
`ALL best candidate: ${bestRow.candidateModel} saves ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent2(bestRow.savingsPct)})`
|
|
6345
6701
|
);
|
|
6346
6702
|
} else if (bestRow.savingsUsd < 0) {
|
|
6347
6703
|
lines.push(
|
|
6348
|
-
`ALL best candidate: ${bestRow.candidateModel} increases cost by ${formatAbsoluteUsd(bestRow.savingsUsd)} (${
|
|
6704
|
+
`ALL best candidate: ${bestRow.candidateModel} increases cost by ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent2(bestRow.savingsPct)})`
|
|
6349
6705
|
);
|
|
6350
6706
|
} else {
|
|
6351
6707
|
lines.push(`ALL best candidate: ${bestRow.candidateModel} matches baseline cost`);
|
|
@@ -6371,20 +6727,20 @@ function toTableCells(optimizeData, options) {
|
|
|
6371
6727
|
periodCell,
|
|
6372
6728
|
styleCandidateCell("BASELINE", "baseline", options.useColor),
|
|
6373
6729
|
"-",
|
|
6374
|
-
|
|
6730
|
+
formatUsd4(row.baselineCostUsd, { approximate: row.baselineCostIncomplete }),
|
|
6375
6731
|
"-",
|
|
6376
6732
|
"-"
|
|
6377
6733
|
];
|
|
6378
6734
|
return options.includeNotesColumn ? [...baselineCells, "-"] : baselineCells;
|
|
6379
6735
|
}
|
|
6380
|
-
const savingsCell =
|
|
6381
|
-
const savingsPctCell =
|
|
6736
|
+
const savingsCell = formatUsd4(row.savingsUsd);
|
|
6737
|
+
const savingsPctCell = formatPercent2(row.savingsPct);
|
|
6382
6738
|
const notesCell = formatNotes(row.notes);
|
|
6383
6739
|
const candidateCells = [
|
|
6384
6740
|
periodCell,
|
|
6385
6741
|
styleCandidateCell(row.candidateModel, "candidate", options.useColor),
|
|
6386
|
-
|
|
6387
|
-
|
|
6742
|
+
formatUsd4(row.hypotheticalCostUsd, { approximate: row.hypotheticalCostIncomplete }),
|
|
6743
|
+
formatUsd4(baselineRow?.baselineCostUsd, {
|
|
6388
6744
|
approximate: baselineRow?.baselineCostIncomplete === true
|
|
6389
6745
|
}),
|
|
6390
6746
|
styleDeltaCell2(row.savingsUsd, savingsCell, options.useColor),
|
|
@@ -6825,8 +7181,17 @@ function resolveReportFormat2(options) {
|
|
|
6825
7181
|
}
|
|
6826
7182
|
return "terminal";
|
|
6827
7183
|
}
|
|
7184
|
+
function validateShareOption2(granularity, options) {
|
|
7185
|
+
if (!options.share) {
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
if (granularity !== "monthly") {
|
|
7189
|
+
throw new Error("--share is only supported for optimize monthly");
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
6828
7192
|
async function prepareOptimizeReport(granularity, options) {
|
|
6829
7193
|
validateOutputFormatOptions2(options);
|
|
7194
|
+
validateShareOption2(granularity, options);
|
|
6830
7195
|
const optimizeData = await buildOptimizeData(granularity, options);
|
|
6831
7196
|
const format = resolveReportFormat2(options);
|
|
6832
7197
|
return {
|
|
@@ -6835,6 +7200,7 @@ async function prepareOptimizeReport(granularity, options) {
|
|
|
6835
7200
|
candidateCount: optimizeData.rows.filter(
|
|
6836
7201
|
(row) => row.rowType === "candidate" && row.periodKey === "ALL"
|
|
6837
7202
|
).length,
|
|
7203
|
+
shareSvg: options.share ? renderOptimizeMonthlyShareSvg(optimizeData) : void 0,
|
|
6838
7204
|
output: renderOptimizeReport(optimizeData, format, {
|
|
6839
7205
|
granularity
|
|
6840
7206
|
})
|
|
@@ -6860,6 +7226,13 @@ async function runOptimizeReport(granularity, options) {
|
|
|
6860
7226
|
logger.warn(message);
|
|
6861
7227
|
});
|
|
6862
7228
|
}
|
|
7229
|
+
if (preparedReport.shareSvg) {
|
|
7230
|
+
const outputPath = await writeShareSvgFile(
|
|
7231
|
+
"optimize-monthly-share.svg",
|
|
7232
|
+
preparedReport.shareSvg
|
|
7233
|
+
);
|
|
7234
|
+
logger.info(`Wrote optimize share SVG: ${outputPath}`);
|
|
7235
|
+
}
|
|
6863
7236
|
console.log(preparedReport.output);
|
|
6864
7237
|
}
|
|
6865
7238
|
|
|
@@ -6917,6 +7290,255 @@ function renderUsageReport(usageData, format, options) {
|
|
|
6917
7290
|
}
|
|
6918
7291
|
}
|
|
6919
7292
|
|
|
7293
|
+
// src/render/render-usage-share-svg.ts
|
|
7294
|
+
var W3 = 1500;
|
|
7295
|
+
var H3 = 560;
|
|
7296
|
+
var ACCENT_H3 = 4;
|
|
7297
|
+
var FOOTER_H3 = 36;
|
|
7298
|
+
var pad3 = { top: 140, right: 80, bottom: 60 + FOOTER_H3, left: 200 };
|
|
7299
|
+
function extractPeriodSourceRows(rows) {
|
|
7300
|
+
return rows.filter((r) => r.rowType === "period_source");
|
|
7301
|
+
}
|
|
7302
|
+
function extractGrandTotal(rows) {
|
|
7303
|
+
return rows.find((r) => r.rowType === "grand_total");
|
|
7304
|
+
}
|
|
7305
|
+
function buildSourceSeries(sourceRows, periods, sources) {
|
|
7306
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
7307
|
+
const sourceTotals = /* @__PURE__ */ new Map();
|
|
7308
|
+
for (const row of sourceRows) {
|
|
7309
|
+
const key = `${row.source}__${row.periodKey}`;
|
|
7310
|
+
lookup.set(key, (lookup.get(key) ?? 0) + row.totalTokens);
|
|
7311
|
+
sourceTotals.set(row.source, (sourceTotals.get(row.source) ?? 0) + row.totalTokens);
|
|
7312
|
+
}
|
|
7313
|
+
return sources.map((source, index) => ({
|
|
7314
|
+
source,
|
|
7315
|
+
color: getSourceColor(source, index),
|
|
7316
|
+
total: sourceTotals.get(source) ?? 0,
|
|
7317
|
+
values: periods.map((period) => lookup.get(`${source}__${period}`) ?? 0)
|
|
7318
|
+
}));
|
|
7319
|
+
}
|
|
7320
|
+
function buildStackedValues(series) {
|
|
7321
|
+
if (series.length === 0) return [];
|
|
7322
|
+
const periodCount = series[0].values.length;
|
|
7323
|
+
const stacked = [];
|
|
7324
|
+
for (let s = 0; s < series.length; s++) {
|
|
7325
|
+
stacked.push(
|
|
7326
|
+
Array.from({ length: periodCount }, (_, p) => {
|
|
7327
|
+
let sum = 0;
|
|
7328
|
+
for (let si = 0; si <= s; si++) sum += series[si].values[p];
|
|
7329
|
+
return sum;
|
|
7330
|
+
})
|
|
7331
|
+
);
|
|
7332
|
+
}
|
|
7333
|
+
return stacked;
|
|
7334
|
+
}
|
|
7335
|
+
function renderAccentBar() {
|
|
7336
|
+
return `<rect width="${W3}" height="${ACCENT_H3}" fill="url(#accent-grad)"/>`;
|
|
7337
|
+
}
|
|
7338
|
+
function renderStatColumn(totalTokens, costUsd, sourceCount) {
|
|
7339
|
+
const x = 60;
|
|
7340
|
+
const baseY = ACCENT_H3 + 48;
|
|
7341
|
+
let svg = "";
|
|
7342
|
+
svg += `<text x="${x}" y="${baseY}" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}" font-size="52" font-weight="800">${escapeSvg(formatCompact(totalTokens))}</text>
|
|
7343
|
+
`;
|
|
7344
|
+
svg += `<text x="${x}" y="${baseY + 22}" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="14" letter-spacing="3" font-weight="600">TOKENS</text>
|
|
7345
|
+
`;
|
|
7346
|
+
if (costUsd !== void 0) {
|
|
7347
|
+
svg += `<text x="${x}" y="${baseY + 50}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="22" font-weight="600">${escapeSvg(formatUsd(costUsd))}</text>
|
|
7348
|
+
`;
|
|
7349
|
+
}
|
|
7350
|
+
svg += `<text x="${x}" y="${baseY + 74}" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${sourceCount} source${sourceCount !== 1 ? "s" : ""}</text>
|
|
7351
|
+
`;
|
|
7352
|
+
return svg;
|
|
7353
|
+
}
|
|
7354
|
+
function renderSourcePills(series) {
|
|
7355
|
+
let svg = "";
|
|
7356
|
+
let cx = pad3.left + 10;
|
|
7357
|
+
const pillY = ACCENT_H3 + 30;
|
|
7358
|
+
for (const s of series) {
|
|
7359
|
+
const label = `${s.source} ${formatCompact(s.total)}`;
|
|
7360
|
+
const textW = label.length * 8.5;
|
|
7361
|
+
const pillW = textW + 28;
|
|
7362
|
+
const pillH = 30;
|
|
7363
|
+
svg += `<rect x="${cx}" y="${pillY}" width="${pillW.toFixed(0)}" height="${pillH}" rx="${pillH / 2}" fill="${s.color}" fill-opacity="0.15" stroke="${s.color}" stroke-opacity="0.4" stroke-width="1"/>
|
|
7364
|
+
`;
|
|
7365
|
+
svg += `<circle cx="${cx + 14}" cy="${pillY + pillH / 2}" r="4" fill="${s.color}"/>
|
|
7366
|
+
`;
|
|
7367
|
+
svg += `<text x="${cx + 24}" y="${pillY + pillH / 2 + 5}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="14">${escapeSvg(label)}</text>
|
|
7368
|
+
`;
|
|
7369
|
+
cx += pillW + 10;
|
|
7370
|
+
}
|
|
7371
|
+
return svg;
|
|
7372
|
+
}
|
|
7373
|
+
function renderCommandBadge(command) {
|
|
7374
|
+
const textW = command.length * 9;
|
|
7375
|
+
const badgeW = textW + 28;
|
|
7376
|
+
const badgeH = 30;
|
|
7377
|
+
const x = W3 - 60 - badgeW;
|
|
7378
|
+
const y = ACCENT_H3 + 30;
|
|
7379
|
+
return [
|
|
7380
|
+
`<rect x="${x}" y="${y}" width="${badgeW}" height="${badgeH}" rx="${badgeH / 2}" fill="none" stroke="${shareTheme.cardBorder}" stroke-width="1"/>`,
|
|
7381
|
+
`<text x="${x + badgeW / 2}" y="${y + badgeH / 2 + 5}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(command)}</text>`
|
|
7382
|
+
].join("\n");
|
|
7383
|
+
}
|
|
7384
|
+
function renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY) {
|
|
7385
|
+
const gridCount = 4;
|
|
7386
|
+
let svg = "";
|
|
7387
|
+
for (let i = 1; i <= gridCount; i++) {
|
|
7388
|
+
const val = maxY / gridCount * i;
|
|
7389
|
+
const y = chartTop + chartH - i / gridCount * chartH;
|
|
7390
|
+
svg += `<line x1="${chartLeft}" y1="${y.toFixed(2)}" x2="${chartRight}" y2="${y.toFixed(2)}" stroke="${shareTheme.gridLine}" stroke-width="1" stroke-dasharray="4 4"/>
|
|
7391
|
+
`;
|
|
7392
|
+
svg += `<text x="${(chartLeft - 12).toFixed(0)}" y="${(y + 4).toFixed(0)}" text-anchor="end" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="11">${escapeSvg(formatCompact(val))}</text>
|
|
7393
|
+
`;
|
|
7394
|
+
}
|
|
7395
|
+
return svg;
|
|
7396
|
+
}
|
|
7397
|
+
function renderGradientDefs(series) {
|
|
7398
|
+
return series.map(
|
|
7399
|
+
(s, i) => `<linearGradient id="area-grad-${i}" x1="0" y1="0" x2="0" y2="1">
|
|
7400
|
+
<stop offset="0%" stop-color="${s.color}" stop-opacity="0.6"/>
|
|
7401
|
+
<stop offset="100%" stop-color="${s.color}" stop-opacity="0.15"/>
|
|
7402
|
+
</linearGradient>`
|
|
7403
|
+
).join("\n");
|
|
7404
|
+
}
|
|
7405
|
+
function renderStackedAreas(series, stacked, periodCount, toX, toChartY, chartBottom) {
|
|
7406
|
+
if (periodCount < 2 || series.length === 0) return "";
|
|
7407
|
+
let svg = "";
|
|
7408
|
+
for (let s = series.length - 1; s >= 0; s--) {
|
|
7409
|
+
const topPoints = Array.from({ length: periodCount }, (_, p) => ({
|
|
7410
|
+
x: toX(p),
|
|
7411
|
+
y: toChartY(stacked[s][p])
|
|
7412
|
+
}));
|
|
7413
|
+
const topPath = catmullRom(topPoints, 0.3, chartBottom);
|
|
7414
|
+
let botPath;
|
|
7415
|
+
if (s === 0) {
|
|
7416
|
+
botPath = `L${toX(periodCount - 1).toFixed(2)},${chartBottom} L${toX(0).toFixed(2)},${chartBottom}`;
|
|
7417
|
+
} else {
|
|
7418
|
+
const botPoints = Array.from({ length: periodCount }, (_, p) => ({
|
|
7419
|
+
x: toX(p),
|
|
7420
|
+
y: toChartY(stacked[s - 1][p])
|
|
7421
|
+
})).reverse();
|
|
7422
|
+
botPath = catmullRom(botPoints, 0.3, chartBottom).replace("M", "L");
|
|
7423
|
+
}
|
|
7424
|
+
svg += `<path d="${topPath} ${botPath} Z" fill="url(#area-grad-${s})" clip-path="url(#chart-clip)"/>
|
|
7425
|
+
`;
|
|
7426
|
+
}
|
|
7427
|
+
const totalPoints = Array.from({ length: periodCount }, (_, p) => ({
|
|
7428
|
+
x: toX(p),
|
|
7429
|
+
y: toChartY(stacked[stacked.length - 1][p])
|
|
7430
|
+
}));
|
|
7431
|
+
const topLinePath = catmullRom(totalPoints, 0.3, chartBottom);
|
|
7432
|
+
svg += `<path d="${topLinePath}" fill="none" stroke="${shareTheme.textPrimary}" stroke-width="2" stroke-opacity="0.5" stroke-linejoin="round" stroke-linecap="round" clip-path="url(#chart-clip)"/>
|
|
7433
|
+
`;
|
|
7434
|
+
for (const pt of totalPoints) {
|
|
7435
|
+
svg += `<circle cx="${pt.x.toFixed(2)}" cy="${pt.y.toFixed(2)}" r="3" fill="${shareTheme.textPrimary}" fill-opacity="0.6" clip-path="url(#chart-clip)"/>
|
|
7436
|
+
`;
|
|
7437
|
+
}
|
|
7438
|
+
return svg;
|
|
7439
|
+
}
|
|
7440
|
+
function renderSinglePeriodBars(series, stacked, toX, toChartY, chartBottom, chartW) {
|
|
7441
|
+
let svg = "";
|
|
7442
|
+
const barWidth = Math.min(120, chartW * 0.4);
|
|
7443
|
+
const xCenter = toX(0);
|
|
7444
|
+
for (let s = series.length - 1; s >= 0; s--) {
|
|
7445
|
+
const yTop = toChartY(stacked[s][0]);
|
|
7446
|
+
const yBot = s === 0 ? chartBottom : toChartY(stacked[s - 1][0]);
|
|
7447
|
+
if (yBot - yTop > 0) {
|
|
7448
|
+
svg += `<rect x="${(xCenter - barWidth / 2).toFixed(2)}" y="${yTop.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${(yBot - yTop).toFixed(2)}" fill="url(#area-grad-${s})" rx="4"/>
|
|
7449
|
+
`;
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
return svg;
|
|
7453
|
+
}
|
|
7454
|
+
function renderPeriodLabels(periods, toX, chartBottom) {
|
|
7455
|
+
const periodCount = periods.length;
|
|
7456
|
+
const maxLabels = 12;
|
|
7457
|
+
const labelStep = periodCount <= maxLabels ? 1 : Math.ceil(periodCount / maxLabels);
|
|
7458
|
+
let svg = "";
|
|
7459
|
+
for (let p = 0; p < periodCount; p += labelStep) {
|
|
7460
|
+
svg += `<text x="${toX(p).toFixed(2)}" y="${(chartBottom + 24).toFixed(0)}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(periods[p])}</text>
|
|
7461
|
+
`;
|
|
7462
|
+
}
|
|
7463
|
+
return svg;
|
|
7464
|
+
}
|
|
7465
|
+
function renderFooter(periods) {
|
|
7466
|
+
const y = H3 - FOOTER_H3;
|
|
7467
|
+
const lineY = y + 1;
|
|
7468
|
+
const textY = y + FOOTER_H3 / 2 + 5;
|
|
7469
|
+
const range = periods.length >= 2 ? `${periods[0]} \u2192 ${periods[periods.length - 1]}` : periods[0] ?? "";
|
|
7470
|
+
return [
|
|
7471
|
+
`<line x1="0" y1="${lineY}" x2="${W3}" y2="${lineY}" stroke="${shareTheme.gridLine}" stroke-width="1"/>`,
|
|
7472
|
+
`<text x="60" y="${textY}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>`,
|
|
7473
|
+
`<text x="${W3 - 60}" y="${textY}" text-anchor="end" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${escapeSvg(range)}</text>`
|
|
7474
|
+
].join("\n");
|
|
7475
|
+
}
|
|
7476
|
+
function renderUsageShareSvg(usageData, granularity) {
|
|
7477
|
+
const sourceRows = extractPeriodSourceRows(usageData.rows);
|
|
7478
|
+
const grandTotal = extractGrandTotal(usageData.rows);
|
|
7479
|
+
const periods = [...new Set(sourceRows.map((r) => r.periodKey))].sort(compareByCodePoint);
|
|
7480
|
+
const sources = [...new Set(sourceRows.map((r) => r.source))].sort(compareByCodePoint);
|
|
7481
|
+
const allSeries = buildSourceSeries(sourceRows, periods, sources);
|
|
7482
|
+
const activeSeries = allSeries.filter((s) => s.total > 0);
|
|
7483
|
+
const totalTokens = grandTotal?.totalTokens ?? 0;
|
|
7484
|
+
const totalCost = grandTotal?.costUsd;
|
|
7485
|
+
const chartLeft = pad3.left;
|
|
7486
|
+
const chartTop = pad3.top;
|
|
7487
|
+
const chartRight = W3 - pad3.right;
|
|
7488
|
+
const chartBottom = H3 - pad3.bottom;
|
|
7489
|
+
const chartW = chartRight - chartLeft;
|
|
7490
|
+
const chartH = chartBottom - chartTop;
|
|
7491
|
+
const periodCount = periods.length;
|
|
7492
|
+
const stacked = buildStackedValues(activeSeries);
|
|
7493
|
+
const maxY = periodCount > 0 && stacked.length > 0 ? Math.max(1, ...stacked[stacked.length - 1]) * 1.08 : 1;
|
|
7494
|
+
const toX = (p) => chartLeft + (periodCount <= 1 ? chartW / 2 : p / (periodCount - 1) * chartW);
|
|
7495
|
+
const toChartY = (val) => scaleY(val, maxY, chartTop, chartBottom);
|
|
7496
|
+
const commandText = `llm-usage ${granularity} --share`;
|
|
7497
|
+
let chartContent;
|
|
7498
|
+
if (periodCount === 0) {
|
|
7499
|
+
chartContent = `<text x="${(W3 / 2).toFixed(0)}" y="${(H3 / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No usage data available</text>`;
|
|
7500
|
+
} else if (periodCount === 1) {
|
|
7501
|
+
chartContent = renderSinglePeriodBars(
|
|
7502
|
+
activeSeries,
|
|
7503
|
+
stacked,
|
|
7504
|
+
toX,
|
|
7505
|
+
toChartY,
|
|
7506
|
+
chartBottom,
|
|
7507
|
+
chartW
|
|
7508
|
+
);
|
|
7509
|
+
} else {
|
|
7510
|
+
chartContent = renderStackedAreas(
|
|
7511
|
+
activeSeries,
|
|
7512
|
+
stacked,
|
|
7513
|
+
periodCount,
|
|
7514
|
+
toX,
|
|
7515
|
+
toChartY,
|
|
7516
|
+
chartBottom
|
|
7517
|
+
);
|
|
7518
|
+
}
|
|
7519
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${H3}" viewBox="0 0 ${W3} ${H3}">
|
|
7520
|
+
<defs>
|
|
7521
|
+
<linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
|
|
7522
|
+
<stop offset="0%" stop-color="#10b981"/>
|
|
7523
|
+
<stop offset="100%" stop-color="#06b6d4"/>
|
|
7524
|
+
</linearGradient>
|
|
7525
|
+
<clipPath id="chart-clip">
|
|
7526
|
+
<rect x="${chartLeft}" y="${chartTop - 4}" width="${chartW}" height="${chartH + 8}"/>
|
|
7527
|
+
</clipPath>
|
|
7528
|
+
${renderGradientDefs(activeSeries)}
|
|
7529
|
+
</defs>
|
|
7530
|
+
<rect width="${W3}" height="${H3}" fill="${shareTheme.bg}"/>
|
|
7531
|
+
${renderAccentBar()}
|
|
7532
|
+
${renderStatColumn(totalTokens, totalCost, activeSeries.length)}
|
|
7533
|
+
${renderSourcePills(activeSeries)}
|
|
7534
|
+
${renderCommandBadge(commandText)}
|
|
7535
|
+
${renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY)}
|
|
7536
|
+
${chartContent}
|
|
7537
|
+
${renderPeriodLabels(periods, toX, chartBottom)}
|
|
7538
|
+
${renderFooter(periods)}
|
|
7539
|
+
</svg>`;
|
|
7540
|
+
}
|
|
7541
|
+
|
|
6920
7542
|
// src/cli/run-usage-report.ts
|
|
6921
7543
|
function validateOutputFormatOptions3(options) {
|
|
6922
7544
|
if (options.markdown && options.json) {
|
|
@@ -6935,6 +7557,9 @@ function resolveReportFormat3(options) {
|
|
|
6935
7557
|
function resolveTableLayout(options) {
|
|
6936
7558
|
return options.perModelColumns ? "per_model_columns" : "compact";
|
|
6937
7559
|
}
|
|
7560
|
+
function resolveShareFileName(granularity) {
|
|
7561
|
+
return `usage-${granularity}-share.svg`;
|
|
7562
|
+
}
|
|
6938
7563
|
async function prepareUsageReport(granularity, options) {
|
|
6939
7564
|
validateOutputFormatOptions3(options);
|
|
6940
7565
|
const usageData = await buildUsageData(granularity, options);
|
|
@@ -6942,6 +7567,7 @@ async function prepareUsageReport(granularity, options) {
|
|
|
6942
7567
|
return {
|
|
6943
7568
|
format,
|
|
6944
7569
|
diagnostics: usageData.diagnostics,
|
|
7570
|
+
shareSvg: options.share ? renderUsageShareSvg(usageData, granularity) : void 0,
|
|
6945
7571
|
output: renderUsageReport(usageData, format, {
|
|
6946
7572
|
granularity,
|
|
6947
7573
|
tableLayout: resolveTableLayout(options)
|
|
@@ -6957,6 +7583,13 @@ async function runUsageReport(granularity, options) {
|
|
|
6957
7583
|
logger.warn(message);
|
|
6958
7584
|
});
|
|
6959
7585
|
}
|
|
7586
|
+
if (preparedReport.shareSvg) {
|
|
7587
|
+
const outputPath = await writeShareSvgFile(
|
|
7588
|
+
resolveShareFileName(granularity),
|
|
7589
|
+
preparedReport.shareSvg
|
|
7590
|
+
);
|
|
7591
|
+
logger.info(`Wrote usage share SVG: ${outputPath}`);
|
|
7592
|
+
}
|
|
6960
7593
|
console.log(preparedReport.output);
|
|
6961
7594
|
}
|
|
6962
7595
|
|
|
@@ -7006,6 +7639,9 @@ function addSharedOptions(command, options = {}) {
|
|
|
7006
7639
|
"Render per-model metrics as multiline aligned table columns (terminal/markdown)"
|
|
7007
7640
|
);
|
|
7008
7641
|
}
|
|
7642
|
+
function addShareOption(command) {
|
|
7643
|
+
return command.option("--share", "Write a share SVG image to the current directory");
|
|
7644
|
+
}
|
|
7009
7645
|
function commandDescription(granularity) {
|
|
7010
7646
|
switch (granularity) {
|
|
7011
7647
|
case "daily":
|
|
@@ -7018,7 +7654,7 @@ function commandDescription(granularity) {
|
|
|
7018
7654
|
}
|
|
7019
7655
|
function createCommand(granularity) {
|
|
7020
7656
|
const command = new Command(granularity);
|
|
7021
|
-
addSharedOptions(command).description(commandDescription(granularity)).action(async (options) => {
|
|
7657
|
+
addShareOption(addSharedOptions(command)).description(commandDescription(granularity)).action(async (options) => {
|
|
7022
7658
|
await runUsageReport(granularity, options);
|
|
7023
7659
|
});
|
|
7024
7660
|
return command;
|
|
@@ -7032,14 +7668,14 @@ function parseGranularityArgument(value) {
|
|
|
7032
7668
|
}
|
|
7033
7669
|
function createEfficiencyCommand() {
|
|
7034
7670
|
const command = new Command("efficiency");
|
|
7035
|
-
addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option("--repo-dir <path>", "Path to repository for Git outcome metrics").option("--include-merge-commits", "Include merge commits in Git outcome metrics").description("Show efficiency report by correlating usage metrics with local Git outcomes").action(async (granularity, options) => {
|
|
7671
|
+
addShareOption(addSharedOptions(command, { includePerModelColumns: false })).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option("--repo-dir <path>", "Path to repository for Git outcome metrics").option("--include-merge-commits", "Include merge commits in Git outcome metrics").description("Show efficiency report by correlating usage metrics with local Git outcomes").action(async (granularity, options) => {
|
|
7036
7672
|
await runEfficiencyReport(granularity, options);
|
|
7037
7673
|
});
|
|
7038
7674
|
return command;
|
|
7039
7675
|
}
|
|
7040
7676
|
function createOptimizeCommand() {
|
|
7041
7677
|
const command = new Command("optimize");
|
|
7042
|
-
addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
|
|
7678
|
+
addShareOption(addSharedOptions(command, { includePerModelColumns: false })).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
|
|
7043
7679
|
"--candidate-model <name>",
|
|
7044
7680
|
"Candidate model for counterfactual pricing (repeatable or comma-separated)",
|
|
7045
7681
|
collectRepeatedOption,
|