diff --git a/src/data.js b/src/data.js
index 557d875..4c14fdc 100644
--- a/src/data.js
+++ b/src/data.js
@@ -2698,6 +2698,19 @@ function _computeCostAnalytics(sessions) {
_saveCostDiskCache();
+ // Cost breakdown by token type (approximated using Sonnet pricing as baseline).
+ // Not perfectly accurate for mixed-model usage, but directionally correct for attribution.
+ const p = MODEL_PRICING['claude-sonnet-4-6'];
+ const inputCostEst = totalInputTokens * p.input;
+ const outputCostEst = totalOutputTokens * p.output;
+ const cacheReadCostEst = totalCacheReadTokens * p.cache_read;
+ const cacheCreateCostEst = totalCacheCreateTokens * p.cache_create;
+ // Cache savings: what cache-read tokens would have cost at full input price
+ const cacheSavings = totalCacheReadTokens * (p.input - p.cache_read);
+ const totalInputSide = totalInputTokens + totalCacheReadTokens + totalCacheCreateTokens;
+ const cacheHitRate = totalInputSide > 0
+ ? Math.round(totalCacheReadTokens / totalInputSide * 100) : 0;
+
return {
totalCost,
totalTokens,
@@ -2721,6 +2734,12 @@ function _computeCostAnalytics(sessions) {
last1hCost,
todayCost,
hoursElapsedToday: Math.max(1, hoursElapsedToday),
+ inputCostEst,
+ outputCostEst,
+ cacheReadCostEst,
+ cacheCreateCostEst,
+ cacheSavings,
+ cacheHitRate,
};
}
diff --git a/src/frontend/analytics.js b/src/frontend/analytics.js
index cabf110..017ba3b 100644
--- a/src/frontend/analytics.js
+++ b/src/frontend/analytics.js
@@ -98,6 +98,49 @@ async function renderAnalytics(container) {
html += '
' + data.avgContextPct + '%Avg context usedof 200K
';
}
html += '';
+
+ // ── Cost attribution stacked bar ──────────────────────────
+ // Uses Sonnet-baseline ratios projected onto actual totalCost.
+ // Ratios are model-agnostic (Claude output/input is ~5:1 across all tiers).
+ if (data.outputCostEst !== undefined && data.totalCost > 0) {
+ var estTotal = data.inputCostEst + data.outputCostEst + data.cacheReadCostEst + data.cacheCreateCostEst;
+ var sharePct = function(v) { return estTotal > 0 ? (v / estTotal * 100) : 0; };
+ var actualOf = function(v) { return (sharePct(v) / 100 * data.totalCost); };
+
+ var outPct = sharePct(data.outputCostEst).toFixed(1);
+ var inPct = sharePct(data.inputCostEst).toFixed(1);
+ var cwPct = sharePct(data.cacheCreateCostEst).toFixed(1);
+ var crPct = sharePct(data.cacheReadCostEst).toFixed(1);
+
+ html += '';
+ html += '
Where your money goes
';
+ html += '
';
+ if (parseFloat(outPct) > 0) html += '
';
+ if (parseFloat(inPct) > 0) html += '
';
+ if (parseFloat(cwPct) > 0) html += '
';
+ if (parseFloat(crPct) > 0) html += '
';
+ html += '
';
+ html += '
';
+ html += 'Output ~' + outPct + '% (~$' + actualOf(data.outputCostEst).toFixed(2) + ')';
+ html += 'Input ~' + inPct + '% (~$' + actualOf(data.inputCostEst).toFixed(2) + ')';
+ if (parseFloat(cwPct) > 0) html += 'Cache write ~' + cwPct + '%';
+ if (parseFloat(crPct) > 0) html += 'Cache read ~' + crPct + '%';
+ html += '
';
+
+ if (data.cacheHitRate > 0 || data.cacheSavings > 0) {
+ html += '
';
+ if (data.cacheHitRate > 0) {
+ var hitColor = data.cacheHitRate >= 60 ? 'var(--accent-green)' : data.cacheHitRate >= 30 ? '#f59e0b' : 'var(--text-muted)';
+ html += 'Cache hit rate: ' + data.cacheHitRate + '%';
+ }
+ if (data.cacheSavings > 0.001) {
+ html += 'Cache saved ~$' + data.cacheSavings.toFixed(0) + ' vs no-cache';
+ }
+ html += '
';
+ }
+ html += '
';
+ }
+
html += '';
}
diff --git a/src/frontend/styles.css b/src/frontend/styles.css
index be798f4..4cd1dc3 100644
--- a/src/frontend/styles.css
+++ b/src/frontend/styles.css
@@ -2741,6 +2741,78 @@ body {
.token-cache-create { border-color: rgba(251, 191, 36, 0.3); }
.token-context { border-color: rgba(168, 85, 247, 0.3); }
+/* ── Cost attribution bar ─────────────────────────────────── */
+.cost-attr-section {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+}
+
+.cost-attr-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 10px;
+}
+
+.cost-attr-note {
+ font-size: 11px;
+ font-weight: 400;
+ color: var(--text-muted);
+}
+
+.cost-attr-bar {
+ display: flex;
+ height: 20px;
+ border-radius: 6px;
+ overflow: hidden;
+ gap: 1px;
+ margin-bottom: 10px;
+}
+
+.cost-attr-seg {
+ transition: opacity 0.15s;
+}
+.cost-attr-seg:hover { opacity: 0.75; cursor: default; }
+
+.seg-output { background: #ef4444; }
+.seg-input { background: var(--accent-blue); }
+.seg-cw { background: #f59e0b; }
+.seg-cr { background: var(--accent-green); }
+
+.cost-attr-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 14px;
+ margin-bottom: 10px;
+}
+
+.cost-attr-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.cost-attr-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.cache-metrics {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+.cache-metric {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
/* ── Subscription vs API ──────────────────────────────────── */
.subscription-section { margin-top: 8px; }