Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/panel/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,15 @@ async function loadOverview() {
document.getElementById('period-info').textContent = stats.period || '';

if (stats.opusCost > 0) {
const saved = stats.saved || (stats.opusCost - stats.totalCostUsd);
const pct = stats.savedPct || ((1 - stats.totalCostUsd / stats.opusCost) * 100);
// tracker.ts now returns saved already clamped to >= 0 and opusCost
// already inclusive of media (so comparing to totalCostUsd is
// apples-to-apples). Older summaries — or the rare path where saved
// is undefined — get the same Math.max clamp here so the panel
// never shows a negative dollar amount.
const saved = Math.max(0, stats.saved != null ? stats.saved : (stats.opusCost - stats.totalCostUsd));
const pct = stats.savedPct != null
? Math.max(0, stats.savedPct)
: (stats.opusCost > 0 ? Math.max(0, (saved / stats.opusCost) * 100) : 0);
document.getElementById('savings-hero').style.display = 'flex';
document.getElementById('savings-amount').textContent = usdBig(saved);
document.getElementById('savings-pct').textContent = pct.toFixed(0) + '%';
Expand Down
38 changes: 33 additions & 5 deletions src/stats/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,19 +257,47 @@ export function recordUsage(
export function getStatsSummary(): {
stats: Stats;
opusCost: number;
/** All chat / token-billed model spend (excludes image / video / music). */
chatOnlyCost: number;
/** Per-image / per-second / per-track media generation spend. */
mediaCost: number;
saved: number;
savedPct: number;
avgCostPerRequest: number;
period: string;
} {
const stats = loadStats();

// Calculate what it would cost with the Opus-tier baseline
const opusCost =
// Hypothetical "if you'd used Opus for everything" baseline. Opus is a
// chat model — it can't replace ImageGen / VideoGen / Music (per_image,
// per_second, per_track billing), so for those rows the Opus-equivalent
// cost IS just the actual cost (no alternative). For chat rows, the
// baseline is the same tokens repriced at Opus rates.
//
// Walk byModel: rows with zero tokens are media (recordUsage stores
// image/video calls with inputTokens=0 outputTokens=0). Those count
// towards both sides equally; chat rows count at actual price on the
// "actual" side and at Opus rates on the "baseline" side. Keeping them
// on both sides means the displayed totals match the user's real
// spend rather than an unfamiliar chat-only subset.
let chatOnlyCost = 0;
let mediaCost = 0;
for (const m of Object.values(stats.byModel)) {
if ((m.inputTokens + m.outputTokens) > 0) chatOnlyCost += m.costUsd;
else mediaCost += m.costUsd;
}
const opusChatCost =
(stats.totalInputTokens / 1_000_000) * OPUS_PRICING.input +
(stats.totalOutputTokens / 1_000_000) * OPUS_PRICING.output;

const saved = opusCost - stats.totalCostUsd;
// Display-side baseline: include media on both sides so "you spent X
// instead of Y" shows real, comparable totals.
const opusCost = opusChatCost + mediaCost;

// Saved is the chat-side delta only — media nets to zero. Clamp to 0
// so a session where the user paid more than Opus-equivalent for chat
// (e.g. Sonnet 4.6 with extended thinking enabled) doesn't show a
// negative "savings" number; we just say zero saved.
const saved = Math.max(0, opusChatCost - chatOnlyCost);
const savedPct = opusCost > 0 ? (saved / opusCost) * 100 : 0;
const avgCostPerRequest =
stats.totalRequests > 0 ? stats.totalCostUsd / stats.totalRequests : 0;
Expand All @@ -285,5 +313,5 @@ export function getStatsSummary(): {
else period = `${days} days`;
}

return { stats, opusCost, saved, savedPct, avgCostPerRequest, period };
return { stats, opusCost, chatOnlyCost, mediaCost, saved, savedPct, avgCostPerRequest, period };
}
Loading