Skip to content

Commit 261b0e0

Browse files
author
Vicky
authored
Merge pull request #36 from BlockRunAI/fix/savings-includes-media-cost
fix(stats): savings calc no longer goes negative when user spends on media
2 parents ad9c789 + d944721 commit 261b0e0

2 files changed

Lines changed: 42 additions & 7 deletions

File tree

src/panel/html.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,8 +780,15 @@ async function loadOverview() {
780780
document.getElementById('period-info').textContent = stats.period || '';
781781
782782
if (stats.opusCost > 0) {
783-
const saved = stats.saved || (stats.opusCost - stats.totalCostUsd);
784-
const pct = stats.savedPct || ((1 - stats.totalCostUsd / stats.opusCost) * 100);
783+
// tracker.ts now returns saved already clamped to >= 0 and opusCost
784+
// already inclusive of media (so comparing to totalCostUsd is
785+
// apples-to-apples). Older summaries — or the rare path where saved
786+
// is undefined — get the same Math.max clamp here so the panel
787+
// never shows a negative dollar amount.
788+
const saved = Math.max(0, stats.saved != null ? stats.saved : (stats.opusCost - stats.totalCostUsd));
789+
const pct = stats.savedPct != null
790+
? Math.max(0, stats.savedPct)
791+
: (stats.opusCost > 0 ? Math.max(0, (saved / stats.opusCost) * 100) : 0);
785792
document.getElementById('savings-hero').style.display = 'flex';
786793
document.getElementById('savings-amount').textContent = usdBig(saved);
787794
document.getElementById('savings-pct').textContent = pct.toFixed(0) + '%';

src/stats/tracker.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,47 @@ export function recordUsage(
257257
export function getStatsSummary(): {
258258
stats: Stats;
259259
opusCost: number;
260+
/** All chat / token-billed model spend (excludes image / video / music). */
261+
chatOnlyCost: number;
262+
/** Per-image / per-second / per-track media generation spend. */
263+
mediaCost: number;
260264
saved: number;
261265
savedPct: number;
262266
avgCostPerRequest: number;
263267
period: string;
264268
} {
265269
const stats = loadStats();
266270

267-
// Calculate what it would cost with the Opus-tier baseline
268-
const opusCost =
271+
// Hypothetical "if you'd used Opus for everything" baseline. Opus is a
272+
// chat model — it can't replace ImageGen / VideoGen / Music (per_image,
273+
// per_second, per_track billing), so for those rows the Opus-equivalent
274+
// cost IS just the actual cost (no alternative). For chat rows, the
275+
// baseline is the same tokens repriced at Opus rates.
276+
//
277+
// Walk byModel: rows with zero tokens are media (recordUsage stores
278+
// image/video calls with inputTokens=0 outputTokens=0). Those count
279+
// towards both sides equally; chat rows count at actual price on the
280+
// "actual" side and at Opus rates on the "baseline" side. Keeping them
281+
// on both sides means the displayed totals match the user's real
282+
// spend rather than an unfamiliar chat-only subset.
283+
let chatOnlyCost = 0;
284+
let mediaCost = 0;
285+
for (const m of Object.values(stats.byModel)) {
286+
if ((m.inputTokens + m.outputTokens) > 0) chatOnlyCost += m.costUsd;
287+
else mediaCost += m.costUsd;
288+
}
289+
const opusChatCost =
269290
(stats.totalInputTokens / 1_000_000) * OPUS_PRICING.input +
270291
(stats.totalOutputTokens / 1_000_000) * OPUS_PRICING.output;
271-
272-
const saved = opusCost - stats.totalCostUsd;
292+
// Display-side baseline: include media on both sides so "you spent X
293+
// instead of Y" shows real, comparable totals.
294+
const opusCost = opusChatCost + mediaCost;
295+
296+
// Saved is the chat-side delta only — media nets to zero. Clamp to 0
297+
// so a session where the user paid more than Opus-equivalent for chat
298+
// (e.g. Sonnet 4.6 with extended thinking enabled) doesn't show a
299+
// negative "savings" number; we just say zero saved.
300+
const saved = Math.max(0, opusChatCost - chatOnlyCost);
273301
const savedPct = opusCost > 0 ? (saved / opusCost) * 100 : 0;
274302
const avgCostPerRequest =
275303
stats.totalRequests > 0 ? stats.totalCostUsd / stats.totalRequests : 0;
@@ -285,5 +313,5 @@ export function getStatsSummary(): {
285313
else period = `${days} days`;
286314
}
287315

288-
return { stats, opusCost, saved, savedPct, avgCostPerRequest, period };
316+
return { stats, opusCost, chatOnlyCost, mediaCost, saved, savedPct, avgCostPerRequest, period };
289317
}

0 commit comments

Comments
 (0)