Skip to content

Commit ab1ea43

Browse files
sorlen008claude
andcommitted
fix: accurate Live session cost (rate + whole-transcript) — v2.6.16
Two compounding inaccuracies made the Live "$ spent" wrong: 1. Pricing bug: opus-4-7 / opus-4-8 matched the 4.0/4.1 legacy regex (/opus-4(?:-[01])?\b/ — the \b also matches "opus-4-7"), so they were charged $15/$75 instead of the current $5/$25 — a 3× overcharge across ALL cost calcs. Broaden the reduced-rate row to /opus-4-(?:[5-9]|[1-9]\d)/ so 4.5+ (incl. two-digit minors) get current rates; legacy stays 4.0/4.1 only. 2. The Live estimate summed tokens from only the last 1MB of the JSONL and applied a single blended 0.1× rate — undercounting large sessions and mis-blending cache-creation vs cache-read, and the message count was tail-only too (e.g. 188 shown vs 915 actual). Replace it with computeCost() over the whole transcript using per-type rates, cached by file mtime+size so unchanged sessions aren't re-read on each 3s refresh. Verified live: a session independently calculated at ~$79.66 now reports $80.08 (was $101.91); message counts are now full-transcript accurate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 11dc8d8 commit ab1ea43

5 files changed

Lines changed: 70 additions & 15 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-command-center",
3-
"version": "2.6.15",
3+
"version": "2.6.16",
44
"description": "Dashboard for visualizing and managing your Claude Code ecosystem",
55
"license": "MIT",
66
"type": "module",

server/scanner/live-scanner.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function getGitBranch(cwd: string): string | undefined {
6161
}
6262
}
6363

64-
import { getPricing as getModelPricingShared, getMaxTokens, getUsableContext } from "./pricing";
64+
import { getPricing as getModelPricingShared, getMaxTokens, getUsableContext, computeCost } from "./pricing";
6565

6666
/** Find the session JSONL file across all project dirs.
6767
* Claude Code creates a new JSONL file (with a new session ID) after context
@@ -148,6 +148,50 @@ interface SessionDetails {
148148
}
149149

150150
/** Extract all session details in a single pass over the tail of the JSONL */
151+
/**
152+
* Accurate per-session cost + message count over the WHOLE transcript, using
153+
* per-token-type rates (input/output/cache-read/cache-creation) — not the old
154+
* blended estimate over just the tail. Cached by file mtime+size so unchanged
155+
* sessions aren't re-read on every 3s live refresh; only growing (active)
156+
* sessions recompute.
157+
*/
158+
const costCache = new Map<string, { mtimeMs: number; size: number; cost: number; messageCount: number }>();
159+
function computeSessionCostAndCount(filePath: string): { cost: number; messageCount: number } {
160+
try {
161+
const st = fs.statSync(filePath);
162+
const hit = costCache.get(filePath);
163+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
164+
return { cost: hit.cost, messageCount: hit.messageCount };
165+
}
166+
const content = fs.readFileSync(filePath, "utf-8");
167+
let input = 0, output = 0, cc = 0, cr = 0, model = "", messageCount = 0;
168+
for (const line of content.split("\n")) {
169+
if (!line) continue;
170+
try {
171+
const r = JSON.parse(line);
172+
if (r.type !== "assistant") continue;
173+
messageCount++;
174+
const u = r.message?.usage;
175+
if (u) {
176+
input += u.input_tokens || 0;
177+
output += u.output_tokens || 0;
178+
cc += u.cache_creation_input_tokens || 0;
179+
cr += u.cache_read_input_tokens || 0;
180+
if (!model && r.message?.model) model = r.message.model;
181+
}
182+
} catch {}
183+
}
184+
const cost = Math.round(computeCost(getModelPricingShared(model), input, output, cr, cc) * 100) / 100;
185+
const result = { cost, messageCount };
186+
costCache.set(filePath, { mtimeMs: st.mtimeMs, size: st.size, ...result });
187+
// Keep the cache from growing unbounded across many sessions.
188+
if (costCache.size > 500) { const k = costCache.keys().next().value; if (k) costCache.delete(k); }
189+
return result;
190+
} catch {
191+
return { cost: 0, messageCount: 0 };
192+
}
193+
}
194+
151195
/** Collapse a model id to its family for context-window tracking. */
152196
function modelFamily(model: string): string {
153197
const m = (model || "").toLowerCase();
@@ -250,13 +294,10 @@ function getSessionDetails(filePath: string): SessionDetails {
250294
contextUsage = { tokensUsed, maxTokens, usableTokens, percentage, model };
251295
}
252296

253-
// Estimate cost (note: we only have partial data from the tail chunk)
254-
// totalInputTokens here includes input + cache_create + cache_read from the tail chunk.
255-
// Most tokens are cache reads (90% cheaper). Use a blended rate.
256-
const pricing = getModelPricing(model);
257-
const cacheReadRate = pricing.input * 0.1;
258-
const blendedInputRate = totalInputTokens > 0 ? cacheReadRate : 0; // Most input is cache reads
259-
const costEstimate = (totalInputTokens / 1_000_000 * blendedInputRate) + (totalOutputTokens / 1_000_000 * pricing.output);
297+
// Accurate cost + message count over the whole transcript with per-type rates
298+
// (mtime-cached). Replaces the old tail-only blended estimate, which both
299+
// undercounted large sessions and mis-blended cache-creation vs cache-read.
300+
const { cost: costEstimate, messageCount: accurateMessageCount } = computeSessionCostAndCount(filePath);
260301

261302
// Per-session permission mode from the last "permission-mode" record. It's
262303
// usually written at session start (head), but can be toggled mid-session
@@ -277,9 +318,9 @@ function getSessionDetails(filePath: string): SessionDetails {
277318
return {
278319
contextUsage,
279320
lastMessage,
280-
messageCount,
321+
messageCount: accurateMessageCount || messageCount,
281322
sizeBytes,
282-
costEstimate: Math.round(costEstimate * 1000) / 1000, // 3 decimal places
323+
costEstimate,
283324
permissionMode: mapPermissionMode(permRaw),
284325
};
285326
}

server/scanner/pricing.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ export interface ModelPricing {
1515

1616
// Regex-matched in order. First match wins.
1717
const MODEL_RATE_TABLE: Array<{ match: RegExp; pricing: ModelPricing; note: string }> = [
18-
// Opus 4.5 / 4.6 — current pricing (reduced from 4.0/4.1)
19-
{ match: /opus-4-[56]/i, pricing: { input: 5, output: 25, cacheRead: 0.5, cacheCreation: 6.25 }, note: "opus-4.5/4.6" },
18+
// Opus 4.5 and up (4.5, 4.6, 4.7, 4.8, … incl. two-digit minors) — current
19+
// reduced pricing. MUST precede the 4.0/4.1 legacy row, whose \b would
20+
// otherwise also match "opus-4-7"/"opus-4-8" and overcharge them 3×.
21+
{ match: /opus-4-(?:[5-9]|[1-9]\d)/i, pricing: { input: 5, output: 25, cacheRead: 0.5, cacheCreation: 6.25 }, note: "opus-4.5+" },
2022
// Opus 4.0 / 4.1 — legacy, original Claude 4 pricing
2123
{ match: /opus-4(?:-[01])?\b/i, pricing: { input: 15, output: 75, cacheRead: 1.5, cacheCreation: 18.75 }, note: "opus-4.0/4.1 legacy" },
2224
// Opus 3 — legacy

tests/pricing.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ describe("getPricing — 2026-04 rate update", () => {
1616
expect(p.output).toBe(25);
1717
});
1818

19+
// Regression: opus-4-7/4-8 matched the 4.0/4.1 legacy "\b" pattern and were
20+
// overcharged 3× ($15 vs $5). They must use the current reduced rates.
21+
it("Opus 4.7 / 4.8 use current reduced rates (not legacy $15)", () => {
22+
for (const m of ["claude-opus-4-7", "claude-opus-4-8", "claude-opus-4-9", "claude-opus-4-10"]) {
23+
const p = getPricing(m);
24+
expect(p.input, m).toBe(5);
25+
expect(p.output, m).toBe(25);
26+
expect(p.cacheRead, m).toBe(0.5);
27+
expect(p.cacheCreation, m).toBe(6.25);
28+
}
29+
});
30+
1931
it("Opus 4 / 4.1 keeps legacy $15/$75 rates for historical accuracy", () => {
2032
expect(getPricing("claude-opus-4").input).toBe(15);
2133
expect(getPricing("claude-opus-4-0").input).toBe(15);

0 commit comments

Comments
 (0)