repospend 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to RepoSpend will be documented in this file.
4
4
 
5
+ ## 0.1.2
6
+
7
+ - Add bundled GPT-5.6 API-equivalent pricing for the Sol, Terra, and Luna tiers, including cache-write and cached-input rates.
8
+ - Add bundled Claude Sonnet 5 introductory pricing through August 31, 2026, with dated model ID family pricing and a note for the standard September 1, 2026 rate.
9
+ - Add Claude Opus 4.8 fast mode pricing and keep special fast-mode rate cards separate from regular Opus family fallback.
10
+ - Add GitHub Copilot pricing coverage for MAI-Code-1-Flash and Kimi K2.7 Code, including suffix variant matching for local Copilot model IDs.
11
+ - Add the OpenAI GPT-5.6 preview pricing source to the bundled pricing metadata shown in Settings.
12
+ - Ignore local generated `marketing/` assets so release diffs stay focused on source, docs, and packaged files.
13
+
14
+ ## 0.1.1
15
+
16
+ - Add bundled Claude Fable 5 and Claude Mythos 5 API-equivalent pricing, including dated model ID family pricing and future Fable/Mythos version fallback.
17
+ - Price Claude cache writes from the local 5-minute vs 1-hour TTL split when transcripts expose it, with the 1-hour rate kept as the fallback for unsplit cache creation.
18
+ - Improve the dashboard filter bar by replacing always-expanded filter groups with compact dropdowns that close when another filter opens, Escape is pressed, or the user clicks outside.
19
+ - Move the Overview timeline and chart controls directly under the KPI row, keeping diagnostics such as Token Counting in Settings instead of the daily-glance flow.
20
+ - Replace Settings save and local-cache actions that forced full page reloads with React refetches that preserve page context.
21
+ - Improve chart readability with stable repo colors, positive-only minimum bar size, and a model distribution share panel for skewed model usage.
22
+ - Reduce badge and copy noise by quieting provider badges, preserving outcome color, simplifying empty filter copy, and showing a compact filtered-view notice on pages without the filter bar.
23
+ - Improve responsive and accessibility polish with a horizontal narrow-width nav, safer filter popover alignment, system font fallback, tabular numeric values, and reduced-motion CSS.
24
+
5
25
  ## 0.1.0
6
26
 
7
27
  - Add GitHub Copilot support for local OTEL exports, Copilot session-state files, and VS Code Copilot Chat transcript/debug files, leaving cost unknown when local data lacks full token splits.
package/README.md CHANGED
@@ -35,11 +35,11 @@ Best for developers who want to:
35
35
 
36
36
  ## Preview
37
37
 
38
- ![RepoSpend overview dashboard with fictional Middle-earth usage data](docs/screenshots/dashboard-overview.png)
38
+ ![Animated RepoSpend overview and sessions tour with fictional Middle-earth usage data](docs/screenshots/overview-sessions-tour.gif)
39
39
 
40
- Screenshots use fictional Middle-earth demo data. The Lord of the Rings themed
41
- repo names, sessions, prompts, token counts, and costs are intentional; no private
42
- repository data is shown.
40
+ Preview media uses fictional Middle-earth demo data. The Lord of the Rings themed
41
+ repo names, sessions, prompts, token counts, models, and costs are intentional; no
42
+ private repository data is shown.
43
43
 
44
44
  ## What is RepoSpend?
45
45
 
@@ -196,9 +196,12 @@ RepoSpend uses normalized model-work totals:
196
196
  totalTokens = inputTokens + outputTokens + reasoningTokens
197
197
  ```
198
198
 
199
- Cache reads and cache writes are kept as input sub-buckets and priced once. This
200
- means RepoSpend token totals may look lower than tools that display cache
201
- reads/writes as separate addable token columns.
199
+ Cache reads and cache writes are kept as input sub-buckets and priced once. For
200
+ Claude, RepoSpend also uses the 5-minute vs 1-hour cache-write split recorded in
201
+ local transcripts when available. This means RepoSpend token totals may look
202
+ lower than tools that display cache reads/writes as separate addable token
203
+ columns, and Claude API-equivalent cost may differ from tools that collapse all
204
+ cache writes into one rate.
202
205
 
203
206
  For the detailed accounting model and comparison with `ccusage` and Tokscale,
204
207
  see [docs/token-accounting.md](docs/token-accounting.md).
@@ -275,6 +278,17 @@ view that is close to Claude Code's local usage files. Use RepoSpend when you
275
278
  want a local dashboard that compares AI coding usage across repos, sessions,
276
279
  models, and tools.
277
280
 
281
+ When comparing totals, expect some intentional differences:
282
+
283
+ - RepoSpend includes Claude Desktop/local-agent session files when they exist;
284
+ many terminal-first tools count only `~/.claude/projects`.
285
+ - RepoSpend keeps cached input and cache writes as input sub-buckets rather than
286
+ adding them again to headline token totals.
287
+ - RepoSpend prices Claude cache writes by the recorded 5-minute vs 1-hour TTL
288
+ split when local transcripts expose it.
289
+ - RepoSpend separates Codex visible output from reasoning output so reasoning is
290
+ priced once.
291
+
278
292
  Generic Claude Code monitors usually focus on one source. RepoSpend is designed
279
293
  as a repo-level AI coding cost tracker: it brings together local Codex, Claude
280
294
  Code, and GitHub Copilot data, includes experimental Cursor imports when enabled,
package/dist/cli.js CHANGED
@@ -9,11 +9,17 @@ function summarize(sessions) {
9
9
  const cost = sumKnownCost(sessions);
10
10
  const inputTokens = sum(sessions, "inputTokens");
11
11
  const cachedInputTokens = sum(sessions, "cachedInputTokens");
12
+ const cacheCreationInputTokens = optionalSum(sessions, "cacheCreationInputTokens");
13
+ const cacheCreationInputTokens5m = optionalSum(sessions, "cacheCreationInputTokens5m");
14
+ const cacheCreationInputTokens1h = optionalSum(sessions, "cacheCreationInputTokens1h");
12
15
  return {
13
16
  estimatedCostUsd: cost.knownCount > 0 ? cost.value : void 0,
14
17
  knownCostSessions: cost.knownCount,
15
18
  totalTokens: sum(sessions, "totalTokens"),
16
19
  cachedInputTokens,
20
+ cacheCreationInputTokens,
21
+ cacheCreationInputTokens5m,
22
+ cacheCreationInputTokens1h,
17
23
  inputTokens,
18
24
  outputTokens: sum(sessions, "outputTokens"),
19
25
  reasoningTokens: sum(sessions, "reasoningTokens"),
@@ -80,6 +86,9 @@ function groupBy(sessions, keyFn, labelFn) {
80
86
  unknownCostSessions: 0,
81
87
  inputTokens: 0,
82
88
  cachedInputTokens: 0,
89
+ cacheCreationInputTokens: 0,
90
+ cacheCreationInputTokens5m: 0,
91
+ cacheCreationInputTokens1h: 0,
83
92
  outputTokens: 0,
84
93
  reasoningTokens: 0,
85
94
  totalTokens: 0,
@@ -89,6 +98,9 @@ function groupBy(sessions, keyFn, labelFn) {
89
98
  };
90
99
  group.inputTokens += session.inputTokens;
91
100
  group.cachedInputTokens += session.cachedInputTokens;
101
+ group.cacheCreationInputTokens = (group.cacheCreationInputTokens ?? 0) + (session.cacheCreationInputTokens ?? 0);
102
+ group.cacheCreationInputTokens5m = (group.cacheCreationInputTokens5m ?? 0) + (session.cacheCreationInputTokens5m ?? 0);
103
+ group.cacheCreationInputTokens1h = (group.cacheCreationInputTokens1h ?? 0) + (session.cacheCreationInputTokens1h ?? 0);
92
104
  group.outputTokens += session.outputTokens;
93
105
  group.reasoningTokens += session.reasoningTokens;
94
106
  group.totalTokens += session.totalTokens;
@@ -128,6 +140,8 @@ function toCsv(sessions) {
128
140
  "inputTokens",
129
141
  "cachedInputTokens",
130
142
  "cacheCreationInputTokens",
143
+ "cacheCreationInputTokens5m",
144
+ "cacheCreationInputTokens1h",
131
145
  "outputTokens",
132
146
  "reasoningTokens",
133
147
  "reasoningOutputTokens",
@@ -219,11 +233,15 @@ function normalizePricingModelId(model) {
219
233
  }
220
234
  function claudePricingFamilyModel(model, isUsable = () => true) {
221
235
  const families = [
236
+ "claude-fable-5",
237
+ "claude-mythos-5",
238
+ "claude-opus-4-8-fast-mode",
222
239
  "claude-opus-4-7",
223
240
  "claude-opus-4-6",
224
241
  "claude-opus-4-5",
225
242
  "claude-opus-4-1",
226
243
  "claude-opus-4",
244
+ "claude-sonnet-5",
227
245
  "claude-sonnet-4-6",
228
246
  "claude-sonnet-4-5",
229
247
  "claude-sonnet-4",
@@ -246,11 +264,13 @@ function copilotPricingFamilyModel(model, isUsable = () => true) {
246
264
  if ((normalized === "raptor-mini" || normalized.startsWith("raptor-mini-") || normalized.startsWith("oswe-vscode")) && isUsable("raptor-mini")) return "raptor-mini";
247
265
  if ((normalized === "lark" || normalized.startsWith("lark-")) && isUsable("lark")) return "lark";
248
266
  if ((normalized === "goldeneye" || normalized.startsWith("goldeneye-")) && isUsable("goldeneye")) return "goldeneye";
267
+ if ((normalized === "mai-code-1-flash" || normalized.startsWith("mai-code-1-flash-")) && isUsable("mai-code-1-flash")) return "mai-code-1-flash";
268
+ if ((normalized === "kimi-k2.7-code" || normalized.startsWith("kimi-k2.7-code-")) && isUsable("kimi-k2.7-code")) return "kimi-k2.7-code";
249
269
  return void 0;
250
270
  }
251
271
  function parseVersionedModel(normalized) {
252
272
  if (normalized.includes("fast-mode")) return void 0;
253
- const claude = normalized.match(/^(claude-(?:opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
273
+ const claude = normalized.match(/^(claude-(?:fable|mythos|opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
254
274
  if (claude) {
255
275
  const minor = claude[3] ? Number(claude[3]) : 0;
256
276
  return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
@@ -276,7 +296,7 @@ function versionedFallbackModel(model, knownModels, isUsable = () => true) {
276
296
  }
277
297
  function isCopilotAliasModel(model) {
278
298
  const normalized = normalizePricingModelId(model);
279
- return normalized.startsWith("raptor-mini") || normalized.startsWith("oswe-vscode") || normalized === "lark" || normalized.startsWith("lark-") || normalized.startsWith("goldeneye");
299
+ return normalized.startsWith("raptor-mini") || normalized.startsWith("oswe-vscode") || normalized === "lark" || normalized.startsWith("lark-") || normalized.startsWith("goldeneye") || normalized.startsWith("mai-code-1-flash") || normalized.startsWith("kimi-k2.7-code");
280
300
  }
281
301
  function positiveRate(value) {
282
302
  return typeof value === "number" && Number.isFinite(value) && value > 0;
@@ -303,14 +323,19 @@ var pricingInfo = {
303
323
  sourceUrl: "https://developers.openai.com/api/docs/pricing",
304
324
  sourceUrls: [
305
325
  { label: "OpenAI pricing reference", url: "https://developers.openai.com/api/docs/pricing" },
326
+ { label: "OpenAI GPT-5.6 preview pricing", url: "https://openai.com/index/previewing-gpt-5-6-sol/" },
306
327
  { label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" },
307
328
  { label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
308
329
  ],
309
330
  unit: "USD per 1M tokens",
310
- updatedAt: "2026-05-18",
331
+ updatedAt: "2026-07-02",
311
332
  note: "RepoSpend estimates API-equivalent cost from local token counts and public API-style Standard pricing. This is not your actual bill; subscriptions, credits, provider terms, cache behavior, regional processing, or other billing factors can make your real cost different."
312
333
  };
313
334
  var defaultPricing = {
335
+ "gpt-5.6": { inputPerMillion: 5, cachedInputPerMillion: 0.5, cacheCreationInputPerMillion: 6.25, outputPerMillion: 30, reasoningOutputPerMillion: 30, note: "GPT-5.6 Sol flagship tier. OpenAI preview pricing lists Sol at $5 input / $30 output per 1M tokens, cache writes at 1.25x input, and cache reads at a 90% discount." },
336
+ "gpt-5.6-sol": { inputPerMillion: 5, cachedInputPerMillion: 0.5, cacheCreationInputPerMillion: 6.25, outputPerMillion: 30, reasoningOutputPerMillion: 30 },
337
+ "gpt-5.6-terra": { inputPerMillion: 2.5, cachedInputPerMillion: 0.25, cacheCreationInputPerMillion: 3.125, outputPerMillion: 15, reasoningOutputPerMillion: 15 },
338
+ "gpt-5.6-luna": { inputPerMillion: 1, cachedInputPerMillion: 0.1, cacheCreationInputPerMillion: 1.25, outputPerMillion: 6, reasoningOutputPerMillion: 6 },
314
339
  "gpt-5.5": { inputPerMillion: 5, cachedInputPerMillion: 0.5, outputPerMillion: 30, reasoningOutputPerMillion: 30 },
315
340
  "gpt-5.5-pro": { inputPerMillion: 30, outputPerMillion: 180, reasoningOutputPerMillion: 180 },
316
341
  "gpt-5.4": { inputPerMillion: 2.5, cachedInputPerMillion: 0.25, outputPerMillion: 15, reasoningOutputPerMillion: 15 },
@@ -342,18 +367,24 @@ var defaultPricing = {
342
367
  "oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
343
368
  "lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
344
369
  "goldeneye": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot fine-tuned model using GPT-5.1-Codex pricing." },
345
- "claude-opus-4-8": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
346
- "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
347
- "claude-opus-4-6": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
348
- "claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInputPerMillion: 0, cachedInputPerMillion: 0, outputPerMillion: 0, note: "GitHub Copilot supported model, but public per-token pricing is not listed separately. RepoSpend leaves API-equivalent pricing unset." },
349
- "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
350
- "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
351
- "claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
352
- "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
353
- "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
354
- "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
355
- "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
356
- "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInputPerMillion: 1, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
370
+ "mai-code-1-flash": { inputPerMillion: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, note: "GitHub Copilot supported Microsoft model; API-equivalent estimate, not a Copilot bill." },
371
+ "kimi-k2.7-code": { inputPerMillion: 0.95, cachedInputPerMillion: 0.19, outputPerMillion: 4, note: "GitHub Copilot supported Moonshot AI model; API-equivalent estimate, not a Copilot bill." },
372
+ "claude-fable-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50 },
373
+ "claude-mythos-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50, note: "Limited availability Anthropic model; same public API-equivalent pricing as Claude Fable 5." },
374
+ "claude-opus-4-8": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
375
+ "claude-opus-4-8-fast-mode": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 12.5, cachedInputPerMillion: 1, outputPerMillion: 50, note: "Claude Opus 4.8 fast mode research preview pricing. Prompt caching multipliers apply on top of fast mode pricing." },
376
+ "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
377
+ "claude-opus-4-6": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
378
+ "claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInput5mPerMillion: 0, cacheCreationInput1hPerMillion: 0, cacheCreationInputPerMillion: 0, cachedInputPerMillion: 0, outputPerMillion: 0, note: "GitHub Copilot supported model, but public per-token pricing is not listed separately. RepoSpend leaves API-equivalent pricing unset." },
379
+ "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
380
+ "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
381
+ "claude-opus-4": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
382
+ "claude-sonnet-5": { inputPerMillion: 2, cacheCreationInput5mPerMillion: 2.5, cacheCreationInput1hPerMillion: 4, cacheCreationInputPerMillion: 4, cachedInputPerMillion: 0.2, outputPerMillion: 10, note: "Anthropic introductory pricing through August 31, 2026. Standard pricing from September 1, 2026 is $3 input / $15 output per 1M tokens." },
383
+ "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
384
+ "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
385
+ "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
386
+ "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInput5mPerMillion: 1.25, cacheCreationInput1hPerMillion: 2, cacheCreationInputPerMillion: 2, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
387
+ "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInput5mPerMillion: 1, cacheCreationInput1hPerMillion: 1.6, cacheCreationInputPerMillion: 1.6, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
357
388
  };
358
389
  function loadPricingTable(pricingPath) {
359
390
  if (!pricingPath) {
@@ -385,14 +416,23 @@ function calculateCostUsd(usage, pricing) {
385
416
  if (!modelPricing) {
386
417
  return void 0;
387
418
  }
388
- const cacheCreationInput = usage.cacheCreationInputTokens ?? 0;
419
+ const cacheCreationInput5m = usage.cacheCreationInputTokens5m ?? 0;
420
+ const cacheCreationInput1h = usage.cacheCreationInputTokens1h ?? 0;
421
+ const splitCacheCreationInput = cacheCreationInput5m + cacheCreationInput1h;
422
+ const cacheCreationInput = usage.cacheCreationInputTokens ?? splitCacheCreationInput;
423
+ const unclassifiedCacheCreationInput = Math.max(cacheCreationInput - splitCacheCreationInput, 0);
389
424
  const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens - cacheCreationInput, 0);
390
425
  const cachedRate = modelPricing.cachedInputPerMillion ?? modelPricing.inputPerMillion;
391
- const cacheCreationRate = modelPricing.cacheCreationInputPerMillion ?? modelPricing.inputPerMillion;
426
+ const cacheCreationRate = firstPositiveRate(modelPricing.cacheCreationInputPerMillion, modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInput5mPerMillion, modelPricing.inputPerMillion);
427
+ const cacheCreation5mRate = firstPositiveRate(modelPricing.cacheCreationInput5mPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
428
+ const cacheCreation1hRate = firstPositiveRate(modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
392
429
  const reasoningRate = modelPricing.reasoningOutputPerMillion ?? modelPricing.outputPerMillion;
393
- const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
430
+ const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput5m / 1e6 * cacheCreation5mRate + cacheCreationInput1h / 1e6 * cacheCreation1hRate + unclassifiedCacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
394
431
  return Number(cost.toFixed(6));
395
432
  }
433
+ function firstPositiveRate(...rates) {
434
+ return rates.find((rate) => typeof rate === "number" && Number.isFinite(rate) && rate > 0) ?? 0;
435
+ }
396
436
 
397
437
  // packages/core/src/cache.ts
398
438
  var activeCacheVersion = "parse-v1";
@@ -2795,6 +2835,8 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
2795
2835
  inputTokens: breakdown.inputTokens,
2796
2836
  cachedInputTokens: breakdown.cachedInputTokens,
2797
2837
  cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
2838
+ cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
2839
+ cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h,
2798
2840
  outputTokens: breakdown.outputTokens,
2799
2841
  reasoningTokens: 0,
2800
2842
  reasoningOutputTokens: 0,
@@ -2817,12 +2859,14 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
2817
2859
  serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
2818
2860
  serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
2819
2861
  serviceTierDetail: serviceTierSummary.serviceTierDetail,
2820
- sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length ? {
2862
+ sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length || breakdown.cacheCreationInputTokens5m > 0 || breakdown.cacheCreationInputTokens1h > 0 ? {
2821
2863
  claude: {
2822
2864
  serviceTiers: breakdown.serviceTiers,
2823
2865
  speeds: breakdown.serviceSpeeds,
2824
2866
  serviceTier: serviceTierSummary.serviceTier,
2825
- speed: serviceSpeedSummary.serviceTier
2867
+ speed: serviceSpeedSummary.serviceTier,
2868
+ cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
2869
+ cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h
2826
2870
  }
2827
2871
  } : void 0,
2828
2872
  userPromptCount: breakdown.userPromptCount,
@@ -2951,19 +2995,30 @@ function occurrenceKey(filePath, recordIndex) {
2951
2995
  return `${path6.resolve(filePath)}:${recordIndex}`;
2952
2996
  }
2953
2997
  function cloneUsage(usage) {
2998
+ const cacheCreation = firstObject4(usage.cache_creation);
2954
2999
  return {
2955
3000
  input_tokens: numberValue(usage.input_tokens),
2956
3001
  cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
2957
3002
  cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
3003
+ cache_creation: cacheCreation ? {
3004
+ ephemeral_5m_input_tokens: numberValue(cacheCreation.ephemeral_5m_input_tokens),
3005
+ ephemeral_1h_input_tokens: numberValue(cacheCreation.ephemeral_1h_input_tokens)
3006
+ } : void 0,
2958
3007
  output_tokens: numberValue(usage.output_tokens),
2959
3008
  service_tier: stringValue3(usage.service_tier),
2960
3009
  speed: stringValue3(usage.speed)
2961
3010
  };
2962
3011
  }
2963
3012
  function mergeUsageMax(target, usage) {
3013
+ const targetCacheCreation = firstObject4(target.cache_creation) ?? {};
3014
+ const usageCacheCreation = firstObject4(usage.cache_creation) ?? {};
2964
3015
  target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
2965
3016
  target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
2966
3017
  target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
3018
+ target.cache_creation = {
3019
+ ephemeral_5m_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_5m_input_tokens), numberValue(usageCacheCreation.ephemeral_5m_input_tokens)),
3020
+ ephemeral_1h_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_1h_input_tokens), numberValue(usageCacheCreation.ephemeral_1h_input_tokens))
3021
+ };
2967
3022
  target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
2968
3023
  target.service_tier ??= stringValue3(usage.service_tier);
2969
3024
  target.speed ??= stringValue3(usage.speed);
@@ -2971,11 +3026,17 @@ function mergeUsageMax(target, usage) {
2971
3026
  function applyUsage(breakdown, usage) {
2972
3027
  const input = numberValue(usage.input_tokens);
2973
3028
  const cacheRead = numberValue(usage.cache_read_input_tokens);
2974
- const cacheCreation = numberValue(usage.cache_creation_input_tokens);
3029
+ const cacheCreationBreakdown = firstObject4(usage.cache_creation);
3030
+ const cacheCreation5m = numberValue(cacheCreationBreakdown?.ephemeral_5m_input_tokens);
3031
+ const cacheCreation1h = numberValue(cacheCreationBreakdown?.ephemeral_1h_input_tokens);
3032
+ const splitCacheCreation = cacheCreation5m + cacheCreation1h;
3033
+ const cacheCreation = Math.max(numberValue(usage.cache_creation_input_tokens), splitCacheCreation);
2975
3034
  const output = numberValue(usage.output_tokens);
2976
3035
  breakdown.inputTokens += input + cacheRead + cacheCreation;
2977
3036
  breakdown.cachedInputTokens += cacheRead;
2978
3037
  breakdown.cacheCreationInputTokens += cacheCreation;
3038
+ breakdown.cacheCreationInputTokens5m += cacheCreation5m;
3039
+ breakdown.cacheCreationInputTokens1h += cacheCreation1h;
2979
3040
  breakdown.outputTokens += output;
2980
3041
  breakdown.usageCount += 1;
2981
3042
  pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
@@ -3030,7 +3091,8 @@ function dateKey2(timestamp3) {
3030
3091
  return timestamp3?.slice(0, 10) ?? "unknown-date";
3031
3092
  }
3032
3093
  function usageTotal(usage) {
3033
- return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
3094
+ const cacheCreation = firstObject4(usage.cache_creation);
3095
+ return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + Math.max(numberValue(usage.cache_creation_input_tokens), numberValue(cacheCreation?.ephemeral_5m_input_tokens) + numberValue(cacheCreation?.ephemeral_1h_input_tokens)) + numberValue(usage.output_tokens);
3034
3096
  }
3035
3097
  function applyContentSignals(breakdown, content) {
3036
3098
  if (!Array.isArray(content)) return;
@@ -3073,6 +3135,8 @@ function emptyBreakdown2() {
3073
3135
  inputTokens: 0,
3074
3136
  cachedInputTokens: 0,
3075
3137
  cacheCreationInputTokens: 0,
3138
+ cacheCreationInputTokens5m: 0,
3139
+ cacheCreationInputTokens1h: 0,
3076
3140
  outputTokens: 0,
3077
3141
  totalTokens: 0,
3078
3142
  usageCount: 0,
@@ -5212,12 +5276,7 @@ import Fastify from "fastify";
5212
5276
 
5213
5277
  // apps/server/src/demo/fixtures.ts
5214
5278
  var baseDate = "2026-05-18";
5215
- var demoPricing = {
5216
- ...defaultPricing,
5217
- "gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
5218
- "gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
5219
- "claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
5220
- };
5279
+ var demoPricing = defaultPricing;
5221
5280
  function issue(command2, category, severity, impact, reason) {
5222
5281
  return {
5223
5282
  command: command2,
@@ -5375,11 +5434,11 @@ var demoInputs = [
5375
5434
  {
5376
5435
  id: "lotr-session-003",
5377
5436
  repo: "shire-mobile",
5378
- title: "Investigate token spike after second breakfast",
5437
+ title: "Investigate Opus 4.8 token spike after second breakfast",
5379
5438
  sourceClient: "claude",
5380
5439
  sourceApp: "Claude Code",
5381
5440
  sourceAppRaw: "claude-code",
5382
- model: "claude-opus-4-7",
5441
+ model: "claude-opus-4-8",
5383
5442
  provider: "anthropic",
5384
5443
  hour: 11,
5385
5444
  minute: 5,
@@ -5458,11 +5517,11 @@ var demoInputs = [
5458
5517
  {
5459
5518
  id: "lotr-session-006",
5460
5519
  repo: "rivendell-dashboard",
5461
- title: "Refactor the council-of-elrond planner",
5520
+ title: "Refactor the council-of-elrond planner with Fable 5",
5462
5521
  sourceClient: "claude",
5463
5522
  sourceApp: "Claude Desktop App",
5464
5523
  sourceAppRaw: "claude-desktop-local-agent",
5465
- model: "claude-sonnet-4-6",
5524
+ model: "claude-fable-5",
5466
5525
  provider: "anthropic",
5467
5526
  hour: 14,
5468
5527
  minute: 15,
@@ -5594,11 +5653,11 @@ var demoInputs = [
5594
5653
  {
5595
5654
  id: "lotr-session-009",
5596
5655
  repo: "minas-tirith-admin",
5597
- title: "Tighten city-gate roles before the beacons launch",
5656
+ title: "Tighten city-gate roles with Mythos 5 before the beacons launch",
5598
5657
  sourceClient: "claude",
5599
5658
  sourceApp: "Claude Code",
5600
5659
  sourceAppRaw: "claude-code",
5601
- model: "claude-haiku-4-5-20251001",
5660
+ model: "claude-mythos-5",
5602
5661
  provider: "anthropic",
5603
5662
  hour: 17,
5604
5663
  minute: 5,
@@ -5750,11 +5809,11 @@ var demoInputs = [
5750
5809
  {
5751
5810
  id: "lotr-session-014",
5752
5811
  repo: "shire-mobile",
5753
- title: "Make pantry sync work offline in Bag End",
5812
+ title: "Make pantry sync work offline in Bag End with Sonnet 4.6",
5754
5813
  sourceClient: "claude",
5755
5814
  sourceApp: "Claude Code",
5756
5815
  sourceAppRaw: "claude-code",
5757
- model: "claude-haiku-4-5-20251001",
5816
+ model: "claude-sonnet-4-6-20260601",
5758
5817
  provider: "anthropic",
5759
5818
  hour: 20,
5760
5819
  minute: 45,
@@ -6297,9 +6356,13 @@ function validatePricingBody(body) {
6297
6356
  outputPerMillion
6298
6357
  };
6299
6358
  const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
6359
+ const cacheCreationInput5mPerMillion = finiteNumber(row.cacheCreationInput5mPerMillion);
6360
+ const cacheCreationInput1hPerMillion = finiteNumber(row.cacheCreationInput1hPerMillion);
6300
6361
  const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
6301
6362
  const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
6302
6363
  if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
6364
+ if (cacheCreationInput5mPerMillion !== void 0) modelPricing.cacheCreationInput5mPerMillion = cacheCreationInput5mPerMillion;
6365
+ if (cacheCreationInput1hPerMillion !== void 0) modelPricing.cacheCreationInput1hPerMillion = cacheCreationInput1hPerMillion;
6303
6366
  if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
6304
6367
  if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
6305
6368
  if (typeof row.note === "string") modelPricing.note = row.note;
@@ -30,6 +30,13 @@ RepoSpend scans:
30
30
 
31
31
  Claude Code session transcripts are parsed from JSONL files. Useful fields include `sessionId`, `cwd`, `gitBranch`, `timestamp`, `entrypoint`, `message.model`, and `message.usage`. When `message.usage` is present, RepoSpend sums Claude assistant usage records directly. When token counts or model names are absent in a transcript, sessions remain visible with unknown cost.
32
32
 
33
+ RepoSpend intentionally includes Claude Desktop/local-agent session roots when
34
+ they exist. Tools such as `ccusage` and Tokscale commonly focus on
35
+ `~/.claude/projects`, so RepoSpend's Claude totals can be higher when local-agent
36
+ sessions are present. In one all-time local audit, RepoSpend's
37
+ `~/.claude/projects` slice matched `ccusage`, while the difference came from
38
+ `~/Library/Application Support/Claude/local-agent-mode-sessions`.
39
+
33
40
  RepoSpend checks `~/.claude/history.jsonl` only for source status/counting. History-only entries are not imported into usage analytics because they do not include reliable token, model, or transcript data.
34
41
 
35
42
  ## GitHub Copilot
package/docs/llms.txt CHANGED
@@ -53,6 +53,7 @@ Positioning:
53
53
  - RepoSpend complements CLI tools such as ccusage.
54
54
  - ccusage is useful for terminal-first Claude Code totals and daily breakdowns.
55
55
  - RepoSpend is useful for visual repo-level analytics, session inspection, source/tool comparison, model mix, token shape, cache reuse, exports, and command/agent friction.
56
+ - RepoSpend totals can intentionally differ from ccusage or Tokscale because it includes Claude Desktop/local-agent session roots when present, treats cache reads/writes as input sub-buckets, prices Claude cache writes by recorded 5-minute vs 1-hour TTL, and separates Codex visible output from reasoning output.
56
57
 
57
58
  Cost language:
58
59
  - RepoSpend shows estimated API-equivalent cost.
package/docs/pricing.md CHANGED
@@ -4,10 +4,24 @@ RepoSpend estimates API-equivalent cost from a local pricing table. The bundled
4
4
 
5
5
  The bundled table is seeded from public OpenAI, Anthropic, Google, and GitHub Copilot model references and is expressed as USD per 1M tokens. Pricing changes over time, so treat RepoSpend costs as API-equivalent estimates rather than invoice-grade accounting.
6
6
 
7
+ The bundled defaults use currently published standard API prices unless a provider has an active introductory API rate. For example, Claude Sonnet 5 uses Anthropic's $2 input and $10 output introductory rate through August 31, 2026, with a pricing-table note for the $3 input and $15 output standard rate that starts September 1, 2026.
8
+
9
+ Some GitHub Copilot model rows are hosted or fine-tuned vendor models such as Raptor mini, MAI-Code-1-Flash, and Kimi K2.7 Code. RepoSpend prices those rows from GitHub's public per-token table as API-equivalent estimates, not as a Copilot invoice.
10
+
11
+ For Claude models, RepoSpend uses Anthropic's cache-write TTL split when local
12
+ Claude Code usage reports expose it. Tokens recorded under
13
+ `cache_creation.ephemeral_5m_input_tokens` use the 5-minute cache-write rate, and
14
+ tokens recorded under `cache_creation.ephemeral_1h_input_tokens` use the 1-hour
15
+ cache-write rate. If a source only exposes the older aggregate
16
+ `cache_creation_input_tokens` field, RepoSpend falls back to the model's generic
17
+ cache-write rate.
18
+
7
19
  Each model can define:
8
20
 
9
21
  - input tokens
10
- - cache write input tokens
22
+ - 5-minute cache write input tokens
23
+ - 1-hour cache write input tokens
24
+ - generic cache write input tokens for sources without a TTL split
11
25
  - cached input tokens
12
26
  - output tokens
13
27
  - reasoning output tokens
@@ -25,8 +39,20 @@ from visible output before storing and pricing both buckets. If a comparison too
25
39
  shows output inclusive of reasoning and also prices reasoning separately, its
26
40
  cost will be higher because reasoning is counted twice.
27
41
 
42
+ This is a known reason RepoSpend Codex API-equivalent cost can be lower than a
43
+ tool whose output bucket remains inclusive of reasoning while also exposing a
44
+ separate reasoning bucket. Compare visible output and reasoning separately before
45
+ treating a cost delta as a pricing-table problem.
46
+
28
47
  See [token-accounting.md](token-accounting.md) for the detailed comparison model.
29
48
 
49
+ This can differ from tools or older `ccusage` versions that price all Claude
50
+ cache creation tokens with one cache-write rate. A single-rate calculation can
51
+ understate sessions that mostly used Anthropic's 1-hour cache writes or overstate
52
+ sessions that mostly used 5-minute cache writes. RepoSpend prices the buckets
53
+ recorded in the local Claude transcript instead of forcing every cache write into
54
+ one column.
55
+
30
56
  When an exact model id is not present in the pricing table, RepoSpend first tries conservative family matching for known provider naming patterns. For example, a nearby newer Claude Opus 4.x or GPT 5.x variant can inherit the closest older bundled rate so the dashboard stays useful while public rate cards catch up. Inherited rates are labeled in Settings.
31
57
 
32
58
  If RepoSpend cannot resolve a usable rate, it still displays token totals and marks cost as unknown. Unknown pricing does not stop scans, dashboard responses, CLI output, or exports.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -5,6 +5,7 @@ RepoSpend uses one normalized token shape across local clients:
5
5
  - `inputTokens` is total input for the request/session. When a source reports cache reads or cache writes separately, RepoSpend includes them in `inputTokens` and also stores them in sub-buckets.
6
6
  - `cachedInputTokens` is the cache-read portion of input.
7
7
  - `cacheCreationInputTokens` is the cache-write portion of input, when the source exposes it.
8
+ - `cacheCreationInputTokens5m` and `cacheCreationInputTokens1h` preserve Anthropic's Claude cache-write TTL split when local transcripts expose `usage.cache_creation`.
8
9
  - `outputTokens` is visible/non-reasoning output.
9
10
  - `reasoningTokens` is reasoning output, when the source exposes it.
10
11
  - `totalTokens = inputTokens + outputTokens + reasoningTokens`.
@@ -37,6 +38,39 @@ cache write input = cacheCreationInputTokens
37
38
 
38
39
  Adding cached tokens again would inflate token totals and double-charge cached input in API-equivalent cost.
39
40
 
41
+ ## Claude Source Scope
42
+
43
+ RepoSpend scans both Claude project transcripts and Claude Desktop/local-agent
44
+ session roots:
45
+
46
+ ```text
47
+ ~/.claude/projects
48
+ ~/.config/claude/projects
49
+ ~/Library/Application Support/Claude/local-agent-mode-sessions
50
+ ~/.config/Claude/local-agent-mode-sessions
51
+ ```
52
+
53
+ Many terminal-first usage tools focus on `~/.claude/projects`. If local-agent
54
+ session files exist, RepoSpend can show higher Claude totals than `ccusage` or
55
+ Tokscale without a parser bug. When comparing tools, first check whether the
56
+ same Claude roots are included.
57
+
58
+ For Claude, RepoSpend also preserves the cache-write TTL split when it is present
59
+ in local logs:
60
+
61
+ ```text
62
+ 5-minute cache write = cacheCreationInputTokens5m
63
+ 1-hour cache write = cacheCreationInputTokens1h
64
+ unclassified cache write = cacheCreationInputTokens - cacheCreationInputTokens5m - cacheCreationInputTokens1h
65
+ ```
66
+
67
+ This is one reason RepoSpend API-equivalent cost can differ from `ccusage`.
68
+ Anthropic prices 5-minute cache writes at a different rate than 1-hour cache
69
+ writes. RepoSpend applies the recorded TTL-specific rate for each bucket; tools
70
+ that collapse all Claude cache creation tokens into a single cache-write column
71
+ can drift when a session mixes TTLs or when the chosen single rate does not match
72
+ the session's actual cache usage.
73
+
40
74
  ## Reasoning Tokens
41
75
 
42
76
  RepoSpend keeps reasoning tokens separate when the local source exposes them. The accurate treatment depends on the source's raw usage shape:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repospend",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local-first dashboard for tracking AI coding token usage and API-equivalent spend by repository, session, model, and tool.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Mehmet Mustafa Demir",
@@ -45,7 +45,9 @@
45
45
  "typecheck": "pnpm --filter @repospend/types --filter @repospend/core build && pnpm --stream --filter @repospend/types --filter @repospend/core --filter @repospend/server --filter @repospend/web typecheck",
46
46
  "clean": "pnpm -r clean && rm -rf dist web-dist",
47
47
  "rebuild:native": "pnpm rebuild better-sqlite3",
48
- "doctor:native": "node scripts/check-native.mjs"
48
+ "doctor:native": "node scripts/check-native.mjs",
49
+ "media:capture": "node scripts/capture-release-media.mjs",
50
+ "media:producthunt": "node scripts/generate-producthunt-media.mjs"
49
51
  },
50
52
  "keywords": [
51
53
  "codex",
@@ -81,6 +83,9 @@
81
83
  "@types/node": "^22.19.1",
82
84
  "esbuild": "^0.28.0",
83
85
  "eslint": "^9.39.1",
86
+ "gifenc": "^1.0.3",
87
+ "playwright": "^1.60.0",
88
+ "sharp": "^0.35.1",
84
89
  "typescript": "^5.9.3",
85
90
  "typescript-eslint": "^8.46.4",
86
91
  "vitest": "^3.2.4"