Skip to content

Commit 9c58e36

Browse files
committed
vscode-extension: v0.5.1 — fix negative savings on dashboard
Mirror of upstream PR #36 (fix/savings-includes-media-cost). The "Saved vs Opus" panel hero would show negative dollar amounts as soon as a user spent meaningfully on ImageGen / VideoGen, e.g. $-8.79 You spent $20.4896 instead of $11.70 Root cause: getStatsSummary() compared an Opus-token baseline (chat only — image/video log inputTokens=0/outputTokens=0) against totalCostUsd (chat + media combined), so once media spend exceeded the chat-vs-Opus delta the difference flipped negative. Fix: split byModel into chatOnlyCost (rows with tokens) and mediaCost (rows without). opusCost on the display side now equals opusChatCost + mediaCost so "you spent X instead of Y" stays apples-to-apples; saved = max(0, opusChatCost - chatOnlyCost) is the chat-side delta only and is clamped non-negative. Bumps vscode-extension to 0.5.1; updates README changelog.
1 parent e4b0988 commit 9c58e36

6 files changed

Lines changed: 57 additions & 14 deletions

File tree

src/panel/html.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -684,8 +684,13 @@ async function loadOverview() {
684684
document.getElementById('period-info').textContent = stats.period || '';
685685
686686
if (stats.opusCost > 0) {
687-
const saved = stats.saved || (stats.opusCost - stats.totalCostUsd);
688-
const pct = stats.savedPct || ((1 - stats.totalCostUsd / stats.opusCost) * 100);
687+
// tracker.ts now returns saved already clamped to >= 0 and opusCost
688+
// already inclusive of media (so comparing to totalCostUsd is
689+
// apples-to-apples). Older summaries fall back to clamped recompute.
690+
const saved = stats.saved != null
691+
? Math.max(0, stats.saved)
692+
: Math.max(0, stats.opusCost - stats.totalCostUsd);
693+
const pct = stats.opusCost > 0 ? (saved / stats.opusCost) * 100 : 0;
689694
document.getElementById('savings-hero').style.display = 'flex';
690695
document.getElementById('savings-amount').textContent = usdBig(saved);
691696
document.getElementById('savings-pct').textContent = pct.toFixed(0) + '%';

src/stats/tracker.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,45 @@ 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.
281+
let chatOnlyCost = 0;
282+
let mediaCost = 0;
283+
for (const m of Object.values(stats.byModel)) {
284+
if ((m.inputTokens + m.outputTokens) > 0) chatOnlyCost += m.costUsd;
285+
else mediaCost += m.costUsd;
286+
}
287+
const opusChatCost =
269288
(stats.totalInputTokens / 1_000_000) * OPUS_PRICING.input +
270289
(stats.totalOutputTokens / 1_000_000) * OPUS_PRICING.output;
271-
272-
const saved = opusCost - stats.totalCostUsd;
290+
// What the bill would have been if every chat request had hit Opus
291+
// pricing — media stays the same on both sides because there's no
292+
// Opus alternative for image/video gen.
293+
const opusCost = opusChatCost + mediaCost;
294+
295+
// Saved is the chat-side delta only (media nets to zero in the diff).
296+
// Clamp to 0 so a session where the user paid more than Opus would have
297+
// (e.g. Sonnet 4.6 with thinking enabled) doesn't show negative savings.
298+
const saved = Math.max(0, opusChatCost - chatOnlyCost);
273299
const savedPct = opusCost > 0 ? (saved / opusCost) * 100 : 0;
274300
const avgCostPerRequest =
275301
stats.totalRequests > 0 ? stats.totalCostUsd / stats.totalRequests : 0;
@@ -285,5 +311,5 @@ export function getStatsSummary(): {
285311
else period = `${days} days`;
286312
}
287313

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

vscode-extension/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ Franklin is an autonomous AI agent that runs directly in VS Code. It doesn't jus
4141

4242
## Changelog
4343

44+
### 0.5.1
45+
- **Fix: \"Saved vs Opus\" no longer goes negative** — when meaningful spend hit ImageGen / VideoGen, the savings widget showed numbers like `$-8.79` because chat-only Opus baseline was being compared against total spend (chat + media). Now compared apples-to-apples (media counts on both sides; saved is the chat-side delta, clamped to >= 0). Mirror of upstream PR #36.
46+
4447
### 0.5.0
4548
- **Major core sync** — extension now rides on Franklin core v3.10.0, picking up two months of upstream work since 0.4.5 (no more cherry-pick lag):
4649
- **Detached background tasks** (v3.10.0) — `Detach` capability spawns long-running work as a separate process; `franklin task list / tail / wait / cancel` to drive them

vscode-extension/out/extension.cjs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147707,8 +147707,17 @@ function recordUsage(model, inputTokens, outputTokens, costUsd, latencyMs, fallb
147707147707
}
147708147708
function getStatsSummary() {
147709147709
const stats = loadStats();
147710-
const opusCost = stats.totalInputTokens / 1e6 * OPUS_PRICING.input + stats.totalOutputTokens / 1e6 * OPUS_PRICING.output;
147711-
const saved = opusCost - stats.totalCostUsd;
147710+
let chatOnlyCost = 0;
147711+
let mediaCost = 0;
147712+
for (const m2 of Object.values(stats.byModel)) {
147713+
if (m2.inputTokens + m2.outputTokens > 0)
147714+
chatOnlyCost += m2.costUsd;
147715+
else
147716+
mediaCost += m2.costUsd;
147717+
}
147718+
const opusChatCost = stats.totalInputTokens / 1e6 * OPUS_PRICING.input + stats.totalOutputTokens / 1e6 * OPUS_PRICING.output;
147719+
const opusCost = opusChatCost + mediaCost;
147720+
const saved = Math.max(0, opusChatCost - chatOnlyCost);
147712147721
const savedPct = opusCost > 0 ? saved / opusCost * 100 : 0;
147713147722
const avgCostPerRequest = stats.totalRequests > 0 ? stats.totalCostUsd / stats.totalRequests : 0;
147714147723
let period = "No data";
@@ -147721,7 +147730,7 @@ function getStatsSummary() {
147721147730
else
147722147731
period = `${days} days`;
147723147732
}
147724-
return { stats, opusCost, saved, savedPct, avgCostPerRequest, period };
147733+
return { stats, opusCost, chatOnlyCost, mediaCost, saved, savedPct, avgCostPerRequest, period };
147725147734
}
147726147735
var import_node_fs3, import_node_path2, import_node_os2, resolvedStatsFile, EMPTY_STATS, cachedStats, flushTimer, FLUSH_DELAY_MS;
147727147736
var init_tracker = __esm({

vscode-extension/out/extension.cjs.map

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

vscode-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "franklin-vscode",
33
"displayName": "Franklin",
44
"description": "The AI agent with a wallet. Autonomous marketing & trading agent powered by x402 micropayments.",
5-
"version": "0.5.0",
5+
"version": "0.5.1",
66
"publisher": "blockrun",
77
"icon": "images/icon.png",
88
"repository": {

0 commit comments

Comments
 (0)