#!/usr/bin/env node // Sums Claude Code token usage and cost per model across .claude/sessions/**/*.jsonl. // // Token field mapping (Anthropic usage semantics): // uncachedInputTokens = input_tokens + cache_creation_input_tokens // cachedInputTokens = cache_read_input_tokens // inputTokens = cachedInputTokens + uncachedInputTokens // totalTokens = inputTokens + outputTokens // // Cost model (per-MTok prices as of 2026-07-05): // fresh input → base input rate // cache writes → 1.25x base (5m TTL) or 2x base (1h TTL), // from usage.cache_creation when present // cache reads (cached input) → 0.1x base input rate // output → output rate // The pseudo-model (harness-injected messages) is skipped. const fs = require("node:fs"); const path = require("node:path"); const readline = require("node:readline"); const sessionsDir = path.join(process.cwd(), ".claude", "sessions"); // USD per million tokens: [input, output]. // Sonnet 5 uses the introductory price ($2/$10), in effect through 2026-08-31 // (sticker price is $3/$15). const PRICES = { "claude-fable-5": [10.0, 50.0], "claude-mythos-5": [10.0, 50.0], "claude-opus-4-8": [5.0, 25.0], "claude-opus-4-7": [5.0, 25.0], "claude-opus-4-6": [5.0, 25.0], "claude-opus-4-5": [5.0, 25.0], "claude-sonnet-5": [2.0, 10.0], "claude-sonnet-4-6": [3.0, 15.0], "claude-haiku-4-5": [1.0, 5.0], }; const CACHE_READ_MULT = 0.1; const CACHE_WRITE_5M_MULT = 1.25; const CACHE_WRITE_1H_MULT = 2.0; function priceFor(model) { if (PRICES[model]) return PRICES[model]; // Match date-suffixed IDs like claude-haiku-4-5-20251001. for (const [id, price] of Object.entries(PRICES)) { if (model.startsWith(`${id}-`)) return price; } return null; } function* jsonlFiles(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { yield* jsonlFiles(full); } else if (entry.isFile() && entry.name.endsWith(".jsonl")) { yield full; } } } const round = (usd) => Math.round(usd * 100) / 100; async function main() { if (!fs.existsSync(sessionsDir)) { console.error(`no such directory: ${sessionsDir}`); process.exit(1); } const perModel = {}; for (const file of jsonlFiles(sessionsDir)) { const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity, }); for await (const line of rl) { if (line.trim() === "") continue; let entry; try { entry = JSON.parse(line); } catch { continue; // skip malformed lines } const usage = entry?.message?.usage; const model = entry?.message?.model; if (!usage || !model || model === "") continue; const stats = (perModel[model] ??= { tokens: { fresh: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0, output: 0, }, }); const t = stats.tokens; const cacheWrite = usage.cache_creation_input_tokens ?? 0; // TTL breakdown when recorded; older entries without it are treated as // 5m writes (the cheaper assumption). const write1h = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0; t.fresh += usage.input_tokens ?? 0; t.cacheWrite1h += write1h; t.cacheWrite5m += cacheWrite - write1h; t.cacheRead += usage.cache_read_input_tokens ?? 0; t.output += usage.output_tokens ?? 0; } } const report = {}; let grandTotal = 0; for (const [model, { tokens: t }] of Object.entries(perModel)) { const uncachedInput = t.fresh + t.cacheWrite5m + t.cacheWrite1h; const entry = { inputTokens: t.cacheRead + uncachedInput, cachedInputTokens: t.cacheRead, uncachedInputTokens: uncachedInput, outputTokens: t.output, totalTokens: t.cacheRead + uncachedInput + t.output, }; const price = priceFor(model); if (price) { const [inputRate, outputRate] = price.map((p) => p / 1_000_000); const uncachedInputCost = t.fresh * inputRate + t.cacheWrite5m * inputRate * CACHE_WRITE_5M_MULT + t.cacheWrite1h * inputRate * CACHE_WRITE_1H_MULT; const cachedInputCost = t.cacheRead * inputRate * CACHE_READ_MULT; const outputCost = t.output * outputRate; entry.uncachedInputCost = round(uncachedInputCost); entry.cachedInputCost = round(cachedInputCost); entry.outputCost = round(outputCost); entry.totalCost = round(uncachedInputCost + cachedInputCost + outputCost); grandTotal += uncachedInputCost + cachedInputCost + outputCost; } else { console.error(`warning: no pricing known for model ${model}`); entry.uncachedInputCost = null; entry.cachedInputCost = null; entry.outputCost = null; entry.totalCost = null; } report[model] = entry; } report.totalCostUSD = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(grandTotal); console.log(JSON.stringify(report, null, 2)); } main();