Skip to content

Commit fead0b5

Browse files
committed
fix(usage): cache and reasoning cost estimation
1 parent 5d3f804 commit fead0b5

4 files changed

Lines changed: 188 additions & 21 deletions

File tree

cli.js

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3647,32 +3647,46 @@ function readTotalTokensFromUsage(usage) {
36473647
return explicitTotal;
36483648
}
36493649
const inputTokens = readNonNegativeInteger(usage.input_tokens ?? usage.inputTokens);
3650+
const cachedInputTokens = readNonNegativeInteger(
3651+
usage.cached_input_tokens ?? usage.cachedInputTokens
3652+
?? usage.cache_read_input_tokens ?? usage.cacheReadInputTokens
3653+
);
3654+
const cacheCreationInputTokens = readNonNegativeInteger(
3655+
usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens
3656+
);
36503657
const outputTokens = readNonNegativeInteger(usage.output_tokens ?? usage.outputTokens);
36513658
const reasoningOutputTokens = readNonNegativeInteger(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens);
3652-
if (inputTokens === null && outputTokens === null && reasoningOutputTokens === null) {
3659+
if (inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null) {
36533660
return null;
36543661
}
3655-
return (inputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0);
3662+
return (inputTokens || 0) + (cachedInputTokens || 0) + (cacheCreationInputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0);
36563663
}
36573664

36583665
function readUsageTotalsFromUsage(usage) {
36593666
if (!usage || typeof usage !== 'object' || Array.isArray(usage)) {
36603667
return null;
36613668
}
36623669
const inputTokens = readNonNegativeInteger(usage.input_tokens ?? usage.inputTokens);
3663-
const cachedInputTokens = readNonNegativeInteger(usage.cached_input_tokens ?? usage.cachedInputTokens);
3670+
const cachedInputTokens = readNonNegativeInteger(
3671+
usage.cached_input_tokens ?? usage.cachedInputTokens
3672+
?? usage.cache_read_input_tokens ?? usage.cacheReadInputTokens
3673+
);
3674+
const cacheCreationInputTokens = readNonNegativeInteger(
3675+
usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens
3676+
);
36643677
const outputTokens = readNonNegativeInteger(usage.output_tokens ?? usage.outputTokens);
36653678
const reasoningOutputTokens = readNonNegativeInteger(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens);
36663679
const totalTokens = readNonNegativeInteger(usage.total_tokens ?? usage.totalTokens)
3667-
?? ((inputTokens === null && cachedInputTokens === null && outputTokens === null && reasoningOutputTokens === null)
3680+
?? ((inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null)
36683681
? null
3669-
: ((inputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0)));
3670-
if (inputTokens === null && cachedInputTokens === null && outputTokens === null && reasoningOutputTokens === null && totalTokens === null) {
3682+
: ((inputTokens || 0) + (cachedInputTokens || 0) + (cacheCreationInputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0)));
3683+
if (inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null && totalTokens === null) {
36713684
return null;
36723685
}
36733686
return {
36743687
inputTokens,
36753688
cachedInputTokens,
3689+
cacheCreationInputTokens,
36763690
outputTokens,
36773691
reasoningOutputTokens,
36783692
totalTokens
@@ -3698,6 +3712,7 @@ function applyUsageTotalsToState(state, usageTotals) {
36983712
const pairs = [
36993713
['inputTokens', usageTotals.inputTokens],
37003714
['cachedInputTokens', usageTotals.cachedInputTokens],
3715+
['cacheCreationInputTokens', usageTotals.cacheCreationInputTokens],
37013716
['outputTokens', usageTotals.outputTokens],
37023717
['reasoningOutputTokens', usageTotals.reasoningOutputTokens],
37033718
['totalTokens', usageTotals.totalTokens]
@@ -3948,12 +3963,13 @@ function parseCodexSessionSummary(filePath, options = {}) {
39483963
let contextWindow = 0;
39493964
let inputTokens = 0;
39503965
let cachedInputTokens = 0;
3966+
let cacheCreationInputTokens = 0;
39513967
let outputTokens = 0;
39523968
let reasoningOutputTokens = 0;
39533969
let provider = 'codex';
39543970
let model = '';
39553971
const models = [];
3956-
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
3972+
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
39573973
const previewMessages = [];
39583974

39593975
for (const record of records) {
@@ -3966,6 +3982,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
39663982
contextWindow = usageState.contextWindow || 0;
39673983
inputTokens = usageState.inputTokens || 0;
39683984
cachedInputTokens = usageState.cachedInputTokens || 0;
3985+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
39693986
outputTokens = usageState.outputTokens || 0;
39703987
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
39713988

@@ -4000,6 +4017,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
40004017
contextWindow = usageState.contextWindow || 0;
40014018
inputTokens = usageState.inputTokens || 0;
40024019
cachedInputTokens = usageState.cachedInputTokens || 0;
4020+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
40034021
outputTokens = usageState.outputTokens || 0;
40044022
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
40054023
provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4059,6 +4077,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
40594077
contextWindow,
40604078
inputTokens,
40614079
cachedInputTokens,
4080+
cacheCreationInputTokens,
40624081
outputTokens,
40634082
reasoningOutputTokens,
40644083
__messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4095,12 +4114,13 @@ function parseClaudeSessionSummary(filePath, options = {}) {
40954114
let contextWindow = 0;
40964115
let inputTokens = 0;
40974116
let cachedInputTokens = 0;
4117+
let cacheCreationInputTokens = 0;
40984118
let outputTokens = 0;
40994119
let reasoningOutputTokens = 0;
41004120
let provider = 'claude';
41014121
let model = '';
41024122
const models = [];
4103-
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4123+
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
41044124
const previewMessages = [];
41054125
let createdAt = '';
41064126
let updatedAt = stat.mtime.toISOString();
@@ -4118,6 +4138,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
41184138
contextWindow = usageState.contextWindow || 0;
41194139
inputTokens = usageState.inputTokens || 0;
41204140
cachedInputTokens = usageState.cachedInputTokens || 0;
4141+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
41214142
outputTokens = usageState.outputTokens || 0;
41224143
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
41234144

@@ -4151,6 +4172,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
41514172
contextWindow = usageState.contextWindow || 0;
41524173
inputTokens = usageState.inputTokens || 0;
41534174
cachedInputTokens = usageState.cachedInputTokens || 0;
4175+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
41544176
outputTokens = usageState.outputTokens || 0;
41554177
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
41564178
provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4209,6 +4231,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
42094231
contextWindow,
42104232
inputTokens,
42114233
cachedInputTokens,
4234+
cacheCreationInputTokens,
42124235
outputTokens,
42134236
reasoningOutputTokens,
42144237
__messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4245,12 +4268,13 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
42454268
let contextWindow = 0;
42464269
let inputTokens = 0;
42474270
let cachedInputTokens = 0;
4271+
let cacheCreationInputTokens = 0;
42484272
let outputTokens = 0;
42494273
let reasoningOutputTokens = 0;
42504274
let provider = 'codebuddy';
42514275
let model = '';
42524276
const models = [];
4253-
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4277+
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
42544278
const previewMessages = [];
42554279
let createdAt = '';
42564280
let updatedAt = stat.mtime.toISOString();
@@ -4268,6 +4292,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
42684292
contextWindow = usageState.contextWindow || 0;
42694293
inputTokens = usageState.inputTokens || 0;
42704294
cachedInputTokens = usageState.cachedInputTokens || 0;
4295+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
42714296
outputTokens = usageState.outputTokens || 0;
42724297
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
42734298

@@ -4306,6 +4331,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
43064331
contextWindow = usageState.contextWindow || 0;
43074332
inputTokens = usageState.inputTokens || 0;
43084333
cachedInputTokens = usageState.cachedInputTokens || 0;
4334+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
43094335
outputTokens = usageState.outputTokens || 0;
43104336
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
43114337
provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4366,6 +4392,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
43664392
contextWindow,
43674393
inputTokens,
43684394
cachedInputTokens,
4395+
cacheCreationInputTokens,
43694396
outputTokens,
43704397
reasoningOutputTokens,
43714398
__messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4655,17 +4682,19 @@ function listClaudeSessions(limit, options = {}) {
46554682
let contextWindow = 0;
46564683
let inputTokens = 0;
46574684
let cachedInputTokens = 0;
4685+
let cacheCreationInputTokens = 0;
46584686
let outputTokens = 0;
46594687
let reasoningOutputTokens = 0;
46604688
let model = typeof entry.model === 'string' ? entry.model.trim() : '';
46614689
const models = model ? [model] : [];
46624690

4663-
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4691+
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
46644692
applySessionUsageSummaryFromIndexEntry(usageState, entry);
46654693
totalTokens = usageState.totalTokens || 0;
46664694
contextWindow = usageState.contextWindow || 0;
46674695
inputTokens = usageState.inputTokens || 0;
46684696
cachedInputTokens = usageState.cachedInputTokens || 0;
4697+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
46694698
outputTokens = usageState.outputTokens || 0;
46704699
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
46714700

@@ -4696,6 +4725,7 @@ function listClaudeSessions(limit, options = {}) {
46964725
contextWindow = usageState.contextWindow || 0;
46974726
inputTokens = usageState.inputTokens || 0;
46984727
cachedInputTokens = usageState.cachedInputTokens || 0;
4728+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
46994729
outputTokens = usageState.outputTokens || 0;
47004730
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
47014731
const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
@@ -4720,6 +4750,7 @@ function listClaudeSessions(limit, options = {}) {
47204750
contextWindow = usageState.contextWindow || 0;
47214751
inputTokens = usageState.inputTokens || 0;
47224752
cachedInputTokens = usageState.cachedInputTokens || 0;
4753+
cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
47234754
outputTokens = usageState.outputTokens || 0;
47244755
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
47254756

@@ -4743,6 +4774,7 @@ function listClaudeSessions(limit, options = {}) {
47434774
contextWindow,
47444775
inputTokens,
47454776
cachedInputTokens,
4777+
cacheCreationInputTokens,
47464778
outputTokens,
47474779
reasoningOutputTokens,
47484780
model,

tests/unit/session-usage-backend.test.mjs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,98 @@ test('parseClaudeSessionSummary reads model from session index and tail records'
666666
}
667667
});
668668

669+
test('parseClaudeSessionSummary exposes Anthropic cache_read and cache_creation token fields', () => {
670+
const parseClaudeSessionSummary = instantiateFunctionBundle(
671+
[
672+
getFileHeadTextSrc,
673+
getFileTailTextSrc,
674+
parseJsonlContentSrc,
675+
parseJsonlHeadRecordsSrc,
676+
parseJsonlTailRecordsSrc,
677+
isSessionSummaryMessageCountExactSrc,
678+
removeLeadingSystemMessageSrc,
679+
readNonNegativeIntegerSrc,
680+
readTotalTokensFromUsageSrc,
681+
readUsageTotalsFromUsageSrc,
682+
readContextWindowValueSrc,
683+
applyUsageTotalsToStateSrc,
684+
readSessionModelsFromRecordSrc,
685+
readSessionModelFromRecordSrc,
686+
readExplicitSessionProviderFromRecordSrc,
687+
readSessionProviderFromRecordSrc,
688+
applySessionUsageSummaryFromRecordSrc,
689+
parseClaudeSessionSummarySrc
690+
],
691+
'parseClaudeSessionSummary',
692+
{
693+
fs,
694+
path,
695+
Buffer,
696+
SESSION_SUMMARY_READ_BYTES: 512,
697+
SESSION_TITLE_READ_BYTES: 512,
698+
toIsoTime(value, fallback = '') {
699+
const date = new Date(value);
700+
if (!Number.isFinite(date.getTime())) return fallback;
701+
return date.toISOString();
702+
},
703+
updateLatestIso(current, next) {
704+
const currentMs = Date.parse(current || '');
705+
const nextMs = Date.parse(next || '');
706+
if (!Number.isFinite(nextMs)) return current || '';
707+
if (!Number.isFinite(currentMs) || nextMs > currentMs) return new Date(nextMs).toISOString();
708+
return current || '';
709+
},
710+
normalizeRole(role) {
711+
return typeof role === 'string' ? role.trim().toLowerCase() : '';
712+
},
713+
extractMessageText(content) {
714+
if (typeof content === 'string') return content;
715+
if (Array.isArray(content)) {
716+
return content.map((item) => item && item.text ? item.text : '').join(' ').trim();
717+
}
718+
return '';
719+
},
720+
truncateText(text) {
721+
return typeof text === 'string' ? text : '';
722+
},
723+
isBootstrapLikeText() {
724+
return false;
725+
}
726+
}
727+
);
728+
729+
const tempDir = fs.mkdtempSync(path.join(__dirname, 'tmp-session-usage-claude-cache-'));
730+
const filePath = path.join(tempDir, 'claude-cache.jsonl');
731+
fs.writeFileSync(filePath, [
732+
JSON.stringify({ type: 'user', message: { content: 'hello' }, timestamp: '2026-04-12T09:07:26.690Z' }),
733+
JSON.stringify({
734+
type: 'assistant',
735+
message: {
736+
model: 'claude-sonnet-4-6',
737+
usage: {
738+
input_tokens: 1000,
739+
cache_read_input_tokens: 5000,
740+
cache_creation_input_tokens: 2000,
741+
output_tokens: 500
742+
}
743+
},
744+
timestamp: '2026-04-12T09:11:35.588Z'
745+
})
746+
].join('\n'));
747+
748+
try {
749+
const result = parseClaudeSessionSummary(filePath, { summaryReadBytes: 512, titleReadBytes: 512 });
750+
assert(result, 'expected claude session summary');
751+
assert.strictEqual(result.cachedInputTokens, 5000, 'cache_read_input_tokens should map to cachedInputTokens');
752+
assert.strictEqual(result.cacheCreationInputTokens, 2000, 'cache_creation_input_tokens should be exposed');
753+
assert.strictEqual(result.inputTokens, 1000);
754+
assert.strictEqual(result.outputTokens, 500);
755+
assert.strictEqual(result.totalTokens, 8500, 'totalTokens fallback should include cache read and creation');
756+
} finally {
757+
fs.rmSync(tempDir, { recursive: true, force: true });
758+
}
759+
});
760+
669761
test('listSessionInventoryBySource reuses cached summaries and registers session lookups', () => {
670762
const codexCalls = [];
671763
const g_sessionInventoryCache = new Map();

tests/unit/web-ui-logic.test.mjs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ test('sessionUsageSummaryCards estimates usage cost from configured provider pri
10501050
assert(costCard, 'missing estimated cost summary card');
10511051
assert(activeDurationCard, 'missing active duration summary card');
10521052
assert(totalDurationCard, 'missing total duration summary card');
1053-
assert.strictEqual(costCard.value, '$1.25');
1053+
assert.strictEqual(costCard.value, '$0.8500');
10541054
assert.strictEqual(costCard.label, '预估费用 · 近 7 天');
10551055
assert.ok(!costCard.note);
10561056
assert.match(costCard.title, / 1\/2 /);
@@ -1100,6 +1100,46 @@ test('sessionUsageSummaryCards falls back to public catalog pricing when provide
11001100
assert.match(costCard.title, / 1\/1 /);
11011101
});
11021102

1103+
test('sessionUsageSummaryCards estimates Anthropic cache_creation cost via cacheWrite rate', () => {
1104+
const computed = createSessionComputed();
1105+
const cards = computed.sessionUsageSummaryCards.call({
1106+
sessionUsageCharts: {
1107+
summary: {
1108+
totalSessions: 1,
1109+
totalMessages: 4,
1110+
totalTokens: 400000,
1111+
totalContextWindow: 200000,
1112+
activeDurationMs: 30 * 60 * 1000,
1113+
totalDurationMs: 30 * 60 * 1000,
1114+
activeDays: 1,
1115+
avgMessagesPerSession: 4,
1116+
busiestDay: null,
1117+
busiestHour: null
1118+
}
1119+
},
1120+
sessionsUsageList: [
1121+
{
1122+
source: 'claude',
1123+
provider: 'claude',
1124+
model: 'claude-sonnet-4-6',
1125+
totalTokens: 400000,
1126+
inputTokens: 200000,
1127+
cachedInputTokens: 50000,
1128+
cacheCreationInputTokens: 30000,
1129+
outputTokens: 100000,
1130+
reasoningOutputTokens: 0
1131+
}
1132+
],
1133+
providersList: [],
1134+
currentProvider: 'claude'
1135+
});
1136+
1137+
const costCard = cards.find((card) => card.key === 'estimated-cost');
1138+
assert(costCard, 'missing estimated cost summary card');
1139+
assert.strictEqual(costCard.value, '$1.99');
1140+
assert.match(costCard.title, / 1\/1 /);
1141+
});
1142+
11031143
test('sessionUsageSummaryCards includes Claude sessions in cost estimation', () => {
11041144
const computed = createSessionComputed();
11051145
const cards = computed.sessionUsageSummaryCards.call({

0 commit comments

Comments
 (0)