repospend 0.1.0 → 0.1.1
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 +11 -0
- package/README.md +21 -7
- package/dist/cli.js +85 -35
- package/docs/data-sources.md +7 -0
- package/docs/llms.txt +1 -0
- package/docs/pricing.md +23 -1
- package/docs/screenshots/agent-friction.png +0 -0
- package/docs/screenshots/dashboard-overview.png +0 -0
- package/docs/screenshots/models-view.png +0 -0
- package/docs/screenshots/repo-detail.png +0 -0
- package/docs/screenshots/repos-view.png +0 -0
- package/docs/screenshots/session-detail.png +0 -0
- package/docs/screenshots/sessions-view.png +0 -0
- package/docs/token-accounting.md +34 -0
- package/package.json +6 -2
- package/web-dist/assets/index-BBw375PL.js +308 -0
- package/web-dist/assets/index-Bzdfbi5w.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-CdTTbCdO.css +0 -1
- package/web-dist/assets/index-DxlpLT8B.js +0 -308
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoSpend will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.1
|
|
6
|
+
|
|
7
|
+
- Add bundled Claude Fable 5 and Claude Mythos 5 API-equivalent pricing, including dated model ID family pricing and future Fable/Mythos version fallback.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- Replace Settings save and local-cache actions that forced full page reloads with React refetches that preserve page context.
|
|
12
|
+
- Improve chart readability with stable repo colors, positive-only minimum bar size, and a model distribution share panel for skewed model usage.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
5
16
|
## 0.1.0
|
|
6
17
|
|
|
7
18
|
- 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
|
-

|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
repo names, sessions, prompts, token counts, and costs are intentional; no
|
|
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.
|
|
200
|
-
|
|
201
|
-
|
|
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,6 +233,8 @@ function normalizePricingModelId(model) {
|
|
|
219
233
|
}
|
|
220
234
|
function claudePricingFamilyModel(model, isUsable = () => true) {
|
|
221
235
|
const families = [
|
|
236
|
+
"claude-fable-5",
|
|
237
|
+
"claude-mythos-5",
|
|
222
238
|
"claude-opus-4-7",
|
|
223
239
|
"claude-opus-4-6",
|
|
224
240
|
"claude-opus-4-5",
|
|
@@ -250,7 +266,7 @@ function copilotPricingFamilyModel(model, isUsable = () => true) {
|
|
|
250
266
|
}
|
|
251
267
|
function parseVersionedModel(normalized) {
|
|
252
268
|
if (normalized.includes("fast-mode")) return void 0;
|
|
253
|
-
const claude = normalized.match(/^(claude-(?:opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
|
|
269
|
+
const claude = normalized.match(/^(claude-(?:fable|mythos|opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
|
|
254
270
|
if (claude) {
|
|
255
271
|
const minor = claude[3] ? Number(claude[3]) : 0;
|
|
256
272
|
return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
|
|
@@ -307,7 +323,7 @@ var pricingInfo = {
|
|
|
307
323
|
{ label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
|
|
308
324
|
],
|
|
309
325
|
unit: "USD per 1M tokens",
|
|
310
|
-
updatedAt: "2026-
|
|
326
|
+
updatedAt: "2026-06-10",
|
|
311
327
|
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
328
|
};
|
|
313
329
|
var defaultPricing = {
|
|
@@ -342,18 +358,20 @@ var defaultPricing = {
|
|
|
342
358
|
"oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
|
|
343
359
|
"lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
|
|
344
360
|
"goldeneye": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot fine-tuned model using GPT-5.1-Codex pricing." },
|
|
345
|
-
"claude-
|
|
346
|
-
"claude-
|
|
347
|
-
"claude-opus-4-
|
|
348
|
-
"claude-opus-4-
|
|
349
|
-
"claude-opus-4-
|
|
350
|
-
"claude-opus-4-
|
|
351
|
-
"claude-opus-4": { inputPerMillion:
|
|
352
|
-
"claude-
|
|
353
|
-
"claude-
|
|
354
|
-
"claude-sonnet-4": { inputPerMillion: 3,
|
|
355
|
-
"claude-
|
|
356
|
-
"claude-
|
|
361
|
+
"claude-fable-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50 },
|
|
362
|
+
"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." },
|
|
363
|
+
"claude-opus-4-8": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
364
|
+
"claude-opus-4-7": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
365
|
+
"claude-opus-4-6": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
366
|
+
"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." },
|
|
367
|
+
"claude-opus-4-5": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
368
|
+
"claude-opus-4-1": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
369
|
+
"claude-opus-4": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
370
|
+
"claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
371
|
+
"claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
372
|
+
"claude-sonnet-4": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
373
|
+
"claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInput5mPerMillion: 1.25, cacheCreationInput1hPerMillion: 2, cacheCreationInputPerMillion: 2, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
|
|
374
|
+
"claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInput5mPerMillion: 1, cacheCreationInput1hPerMillion: 1.6, cacheCreationInputPerMillion: 1.6, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
|
|
357
375
|
};
|
|
358
376
|
function loadPricingTable(pricingPath) {
|
|
359
377
|
if (!pricingPath) {
|
|
@@ -385,14 +403,23 @@ function calculateCostUsd(usage, pricing) {
|
|
|
385
403
|
if (!modelPricing) {
|
|
386
404
|
return void 0;
|
|
387
405
|
}
|
|
388
|
-
const
|
|
406
|
+
const cacheCreationInput5m = usage.cacheCreationInputTokens5m ?? 0;
|
|
407
|
+
const cacheCreationInput1h = usage.cacheCreationInputTokens1h ?? 0;
|
|
408
|
+
const splitCacheCreationInput = cacheCreationInput5m + cacheCreationInput1h;
|
|
409
|
+
const cacheCreationInput = usage.cacheCreationInputTokens ?? splitCacheCreationInput;
|
|
410
|
+
const unclassifiedCacheCreationInput = Math.max(cacheCreationInput - splitCacheCreationInput, 0);
|
|
389
411
|
const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens - cacheCreationInput, 0);
|
|
390
412
|
const cachedRate = modelPricing.cachedInputPerMillion ?? modelPricing.inputPerMillion;
|
|
391
|
-
const cacheCreationRate = modelPricing.cacheCreationInputPerMillion
|
|
413
|
+
const cacheCreationRate = firstPositiveRate(modelPricing.cacheCreationInputPerMillion, modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInput5mPerMillion, modelPricing.inputPerMillion);
|
|
414
|
+
const cacheCreation5mRate = firstPositiveRate(modelPricing.cacheCreationInput5mPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
|
|
415
|
+
const cacheCreation1hRate = firstPositiveRate(modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
|
|
392
416
|
const reasoningRate = modelPricing.reasoningOutputPerMillion ?? modelPricing.outputPerMillion;
|
|
393
|
-
const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate +
|
|
417
|
+
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
418
|
return Number(cost.toFixed(6));
|
|
395
419
|
}
|
|
420
|
+
function firstPositiveRate(...rates) {
|
|
421
|
+
return rates.find((rate) => typeof rate === "number" && Number.isFinite(rate) && rate > 0) ?? 0;
|
|
422
|
+
}
|
|
396
423
|
|
|
397
424
|
// packages/core/src/cache.ts
|
|
398
425
|
var activeCacheVersion = "parse-v1";
|
|
@@ -2795,6 +2822,8 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2795
2822
|
inputTokens: breakdown.inputTokens,
|
|
2796
2823
|
cachedInputTokens: breakdown.cachedInputTokens,
|
|
2797
2824
|
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
2825
|
+
cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
|
|
2826
|
+
cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h,
|
|
2798
2827
|
outputTokens: breakdown.outputTokens,
|
|
2799
2828
|
reasoningTokens: 0,
|
|
2800
2829
|
reasoningOutputTokens: 0,
|
|
@@ -2817,12 +2846,14 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2817
2846
|
serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
|
|
2818
2847
|
serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
|
|
2819
2848
|
serviceTierDetail: serviceTierSummary.serviceTierDetail,
|
|
2820
|
-
sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length ? {
|
|
2849
|
+
sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length || breakdown.cacheCreationInputTokens5m > 0 || breakdown.cacheCreationInputTokens1h > 0 ? {
|
|
2821
2850
|
claude: {
|
|
2822
2851
|
serviceTiers: breakdown.serviceTiers,
|
|
2823
2852
|
speeds: breakdown.serviceSpeeds,
|
|
2824
2853
|
serviceTier: serviceTierSummary.serviceTier,
|
|
2825
|
-
speed: serviceSpeedSummary.serviceTier
|
|
2854
|
+
speed: serviceSpeedSummary.serviceTier,
|
|
2855
|
+
cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
|
|
2856
|
+
cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h
|
|
2826
2857
|
}
|
|
2827
2858
|
} : void 0,
|
|
2828
2859
|
userPromptCount: breakdown.userPromptCount,
|
|
@@ -2951,19 +2982,30 @@ function occurrenceKey(filePath, recordIndex) {
|
|
|
2951
2982
|
return `${path6.resolve(filePath)}:${recordIndex}`;
|
|
2952
2983
|
}
|
|
2953
2984
|
function cloneUsage(usage) {
|
|
2985
|
+
const cacheCreation = firstObject4(usage.cache_creation);
|
|
2954
2986
|
return {
|
|
2955
2987
|
input_tokens: numberValue(usage.input_tokens),
|
|
2956
2988
|
cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
2957
2989
|
cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
|
|
2990
|
+
cache_creation: cacheCreation ? {
|
|
2991
|
+
ephemeral_5m_input_tokens: numberValue(cacheCreation.ephemeral_5m_input_tokens),
|
|
2992
|
+
ephemeral_1h_input_tokens: numberValue(cacheCreation.ephemeral_1h_input_tokens)
|
|
2993
|
+
} : void 0,
|
|
2958
2994
|
output_tokens: numberValue(usage.output_tokens),
|
|
2959
2995
|
service_tier: stringValue3(usage.service_tier),
|
|
2960
2996
|
speed: stringValue3(usage.speed)
|
|
2961
2997
|
};
|
|
2962
2998
|
}
|
|
2963
2999
|
function mergeUsageMax(target, usage) {
|
|
3000
|
+
const targetCacheCreation = firstObject4(target.cache_creation) ?? {};
|
|
3001
|
+
const usageCacheCreation = firstObject4(usage.cache_creation) ?? {};
|
|
2964
3002
|
target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
|
|
2965
3003
|
target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
|
|
2966
3004
|
target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
|
|
3005
|
+
target.cache_creation = {
|
|
3006
|
+
ephemeral_5m_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_5m_input_tokens), numberValue(usageCacheCreation.ephemeral_5m_input_tokens)),
|
|
3007
|
+
ephemeral_1h_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_1h_input_tokens), numberValue(usageCacheCreation.ephemeral_1h_input_tokens))
|
|
3008
|
+
};
|
|
2967
3009
|
target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
|
|
2968
3010
|
target.service_tier ??= stringValue3(usage.service_tier);
|
|
2969
3011
|
target.speed ??= stringValue3(usage.speed);
|
|
@@ -2971,11 +3013,17 @@ function mergeUsageMax(target, usage) {
|
|
|
2971
3013
|
function applyUsage(breakdown, usage) {
|
|
2972
3014
|
const input = numberValue(usage.input_tokens);
|
|
2973
3015
|
const cacheRead = numberValue(usage.cache_read_input_tokens);
|
|
2974
|
-
const
|
|
3016
|
+
const cacheCreationBreakdown = firstObject4(usage.cache_creation);
|
|
3017
|
+
const cacheCreation5m = numberValue(cacheCreationBreakdown?.ephemeral_5m_input_tokens);
|
|
3018
|
+
const cacheCreation1h = numberValue(cacheCreationBreakdown?.ephemeral_1h_input_tokens);
|
|
3019
|
+
const splitCacheCreation = cacheCreation5m + cacheCreation1h;
|
|
3020
|
+
const cacheCreation = Math.max(numberValue(usage.cache_creation_input_tokens), splitCacheCreation);
|
|
2975
3021
|
const output = numberValue(usage.output_tokens);
|
|
2976
3022
|
breakdown.inputTokens += input + cacheRead + cacheCreation;
|
|
2977
3023
|
breakdown.cachedInputTokens += cacheRead;
|
|
2978
3024
|
breakdown.cacheCreationInputTokens += cacheCreation;
|
|
3025
|
+
breakdown.cacheCreationInputTokens5m += cacheCreation5m;
|
|
3026
|
+
breakdown.cacheCreationInputTokens1h += cacheCreation1h;
|
|
2979
3027
|
breakdown.outputTokens += output;
|
|
2980
3028
|
breakdown.usageCount += 1;
|
|
2981
3029
|
pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
|
|
@@ -3030,7 +3078,8 @@ function dateKey2(timestamp3) {
|
|
|
3030
3078
|
return timestamp3?.slice(0, 10) ?? "unknown-date";
|
|
3031
3079
|
}
|
|
3032
3080
|
function usageTotal(usage) {
|
|
3033
|
-
|
|
3081
|
+
const cacheCreation = firstObject4(usage.cache_creation);
|
|
3082
|
+
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
3083
|
}
|
|
3035
3084
|
function applyContentSignals(breakdown, content) {
|
|
3036
3085
|
if (!Array.isArray(content)) return;
|
|
@@ -3073,6 +3122,8 @@ function emptyBreakdown2() {
|
|
|
3073
3122
|
inputTokens: 0,
|
|
3074
3123
|
cachedInputTokens: 0,
|
|
3075
3124
|
cacheCreationInputTokens: 0,
|
|
3125
|
+
cacheCreationInputTokens5m: 0,
|
|
3126
|
+
cacheCreationInputTokens1h: 0,
|
|
3076
3127
|
outputTokens: 0,
|
|
3077
3128
|
totalTokens: 0,
|
|
3078
3129
|
usageCount: 0,
|
|
@@ -5212,12 +5263,7 @@ import Fastify from "fastify";
|
|
|
5212
5263
|
|
|
5213
5264
|
// apps/server/src/demo/fixtures.ts
|
|
5214
5265
|
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
|
-
};
|
|
5266
|
+
var demoPricing = defaultPricing;
|
|
5221
5267
|
function issue(command2, category, severity, impact, reason) {
|
|
5222
5268
|
return {
|
|
5223
5269
|
command: command2,
|
|
@@ -5375,11 +5421,11 @@ var demoInputs = [
|
|
|
5375
5421
|
{
|
|
5376
5422
|
id: "lotr-session-003",
|
|
5377
5423
|
repo: "shire-mobile",
|
|
5378
|
-
title: "Investigate token spike after second breakfast",
|
|
5424
|
+
title: "Investigate Opus 4.8 token spike after second breakfast",
|
|
5379
5425
|
sourceClient: "claude",
|
|
5380
5426
|
sourceApp: "Claude Code",
|
|
5381
5427
|
sourceAppRaw: "claude-code",
|
|
5382
|
-
model: "claude-opus-4-
|
|
5428
|
+
model: "claude-opus-4-8",
|
|
5383
5429
|
provider: "anthropic",
|
|
5384
5430
|
hour: 11,
|
|
5385
5431
|
minute: 5,
|
|
@@ -5458,11 +5504,11 @@ var demoInputs = [
|
|
|
5458
5504
|
{
|
|
5459
5505
|
id: "lotr-session-006",
|
|
5460
5506
|
repo: "rivendell-dashboard",
|
|
5461
|
-
title: "Refactor the council-of-elrond planner",
|
|
5507
|
+
title: "Refactor the council-of-elrond planner with Fable 5",
|
|
5462
5508
|
sourceClient: "claude",
|
|
5463
5509
|
sourceApp: "Claude Desktop App",
|
|
5464
5510
|
sourceAppRaw: "claude-desktop-local-agent",
|
|
5465
|
-
model: "claude-
|
|
5511
|
+
model: "claude-fable-5",
|
|
5466
5512
|
provider: "anthropic",
|
|
5467
5513
|
hour: 14,
|
|
5468
5514
|
minute: 15,
|
|
@@ -5594,11 +5640,11 @@ var demoInputs = [
|
|
|
5594
5640
|
{
|
|
5595
5641
|
id: "lotr-session-009",
|
|
5596
5642
|
repo: "minas-tirith-admin",
|
|
5597
|
-
title: "Tighten city-gate roles before the beacons launch",
|
|
5643
|
+
title: "Tighten city-gate roles with Mythos 5 before the beacons launch",
|
|
5598
5644
|
sourceClient: "claude",
|
|
5599
5645
|
sourceApp: "Claude Code",
|
|
5600
5646
|
sourceAppRaw: "claude-code",
|
|
5601
|
-
model: "claude-
|
|
5647
|
+
model: "claude-mythos-5",
|
|
5602
5648
|
provider: "anthropic",
|
|
5603
5649
|
hour: 17,
|
|
5604
5650
|
minute: 5,
|
|
@@ -5750,11 +5796,11 @@ var demoInputs = [
|
|
|
5750
5796
|
{
|
|
5751
5797
|
id: "lotr-session-014",
|
|
5752
5798
|
repo: "shire-mobile",
|
|
5753
|
-
title: "Make pantry sync work offline in Bag End",
|
|
5799
|
+
title: "Make pantry sync work offline in Bag End with Sonnet 4.6",
|
|
5754
5800
|
sourceClient: "claude",
|
|
5755
5801
|
sourceApp: "Claude Code",
|
|
5756
5802
|
sourceAppRaw: "claude-code",
|
|
5757
|
-
model: "claude-
|
|
5803
|
+
model: "claude-sonnet-4-6-20260601",
|
|
5758
5804
|
provider: "anthropic",
|
|
5759
5805
|
hour: 20,
|
|
5760
5806
|
minute: 45,
|
|
@@ -6297,9 +6343,13 @@ function validatePricingBody(body) {
|
|
|
6297
6343
|
outputPerMillion
|
|
6298
6344
|
};
|
|
6299
6345
|
const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
|
|
6346
|
+
const cacheCreationInput5mPerMillion = finiteNumber(row.cacheCreationInput5mPerMillion);
|
|
6347
|
+
const cacheCreationInput1hPerMillion = finiteNumber(row.cacheCreationInput1hPerMillion);
|
|
6300
6348
|
const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
|
|
6301
6349
|
const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
|
|
6302
6350
|
if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
|
|
6351
|
+
if (cacheCreationInput5mPerMillion !== void 0) modelPricing.cacheCreationInput5mPerMillion = cacheCreationInput5mPerMillion;
|
|
6352
|
+
if (cacheCreationInput1hPerMillion !== void 0) modelPricing.cacheCreationInput1hPerMillion = cacheCreationInput1hPerMillion;
|
|
6303
6353
|
if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
|
|
6304
6354
|
if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
|
|
6305
6355
|
if (typeof row.note === "string") modelPricing.note = row.note;
|
package/docs/data-sources.md
CHANGED
|
@@ -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,20 @@ 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
|
+
For Claude models, RepoSpend uses Anthropic's cache-write TTL split when local
|
|
8
|
+
Claude Code usage reports expose it. Tokens recorded under
|
|
9
|
+
`cache_creation.ephemeral_5m_input_tokens` use the 5-minute cache-write rate, and
|
|
10
|
+
tokens recorded under `cache_creation.ephemeral_1h_input_tokens` use the 1-hour
|
|
11
|
+
cache-write rate. If a source only exposes the older aggregate
|
|
12
|
+
`cache_creation_input_tokens` field, RepoSpend falls back to the model's generic
|
|
13
|
+
cache-write rate.
|
|
14
|
+
|
|
7
15
|
Each model can define:
|
|
8
16
|
|
|
9
17
|
- input tokens
|
|
10
|
-
- cache write input tokens
|
|
18
|
+
- 5-minute cache write input tokens
|
|
19
|
+
- 1-hour cache write input tokens
|
|
20
|
+
- generic cache write input tokens for sources without a TTL split
|
|
11
21
|
- cached input tokens
|
|
12
22
|
- output tokens
|
|
13
23
|
- reasoning output tokens
|
|
@@ -25,8 +35,20 @@ from visible output before storing and pricing both buckets. If a comparison too
|
|
|
25
35
|
shows output inclusive of reasoning and also prices reasoning separately, its
|
|
26
36
|
cost will be higher because reasoning is counted twice.
|
|
27
37
|
|
|
38
|
+
This is a known reason RepoSpend Codex API-equivalent cost can be lower than a
|
|
39
|
+
tool whose output bucket remains inclusive of reasoning while also exposing a
|
|
40
|
+
separate reasoning bucket. Compare visible output and reasoning separately before
|
|
41
|
+
treating a cost delta as a pricing-table problem.
|
|
42
|
+
|
|
28
43
|
See [token-accounting.md](token-accounting.md) for the detailed comparison model.
|
|
29
44
|
|
|
45
|
+
This can differ from tools or older `ccusage` versions that price all Claude
|
|
46
|
+
cache creation tokens with one cache-write rate. A single-rate calculation can
|
|
47
|
+
understate sessions that mostly used Anthropic's 1-hour cache writes or overstate
|
|
48
|
+
sessions that mostly used 5-minute cache writes. RepoSpend prices the buckets
|
|
49
|
+
recorded in the local Claude transcript instead of forcing every cache write into
|
|
50
|
+
one column.
|
|
51
|
+
|
|
30
52
|
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
53
|
|
|
32
54
|
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
|
|
Binary file
|
package/docs/token-accounting.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.1",
|
|
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,8 @@
|
|
|
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"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|
|
51
52
|
"codex",
|
|
@@ -81,6 +82,9 @@
|
|
|
81
82
|
"@types/node": "^22.19.1",
|
|
82
83
|
"esbuild": "^0.28.0",
|
|
83
84
|
"eslint": "^9.39.1",
|
|
85
|
+
"gifenc": "^1.0.3",
|
|
86
|
+
"playwright": "^1.60.0",
|
|
87
|
+
"sharp": "^0.35.1",
|
|
84
88
|
"typescript": "^5.9.3",
|
|
85
89
|
"typescript-eslint": "^8.46.4",
|
|
86
90
|
"vitest": "^3.2.4"
|