From b2780f521460dd97d7a3d26a2b2a2a893864c7e6 Mon Sep 17 00:00:00 2001 From: Vadym O Date: Mon, 19 Oct 2026 02:40:54 +0800 Subject: [PATCH] fix(usage): sum cache reads across attempts that report different fields aggregateAttemptUsage summed `cachedInputTokens` and `cacheReadInputTokens` independently. Anthropic usage sets both; an OpenAI-shaped attempt sets only `cachedInputTokens`. Row-level readers take `cacheReadInputTokens ?? cachedInputTokens` (request-log, management/shared.ts), so a row aggregating an Anthropic leg (60 read) and an OpenAI-shaped leg (40 cached) reported 60 cache reads instead of 100. Cost is priced per attempt and is not affected. Once any attempt reports `cacheReadInputTokens`, the aggregate's read field now counts each attempt's `cacheReadInputTokens ?? cachedInputTokens`. Rows whose attempts never report the read field are unchanged. Co-Authored-By: Claude Code --- src/server/request-log.ts | 5 ++++- tests/usage/request-log-attempt-identity.test.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/server/request-log.ts b/src/server/request-log.ts index 096a028bff0..16e7ee9f434 100644 --- a/src/server/request-log.ts +++ b/src/server/request-log.ts @@ -1873,7 +1873,10 @@ export function aggregateAttemptUsage( return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined; }; const cachedInputTokens = sumOptional("cachedInputTokens"); - const cacheReadInputTokens = sumOptional("cacheReadInputTokens"); + // Readers take `cacheReadInputTokens ?? cachedInputTokens` (usage/cost.ts), so once any attempt + // reports the read field, an attempt that only reports `cachedInputTokens` must count in it too. + const cacheReadInputTokens = sumOptional("cacheReadInputTokens") === undefined ? undefined + : usages.reduce((sum, usage) => sum + (usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0), 0); const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens"); const reasoningOutputTokens = sumOptional("reasoningOutputTokens"); const providerCredits = sumOptional("providerCredits"); diff --git a/tests/usage/request-log-attempt-identity.test.ts b/tests/usage/request-log-attempt-identity.test.ts index e6ca57f4c1c..36b13a5ddf4 100644 --- a/tests/usage/request-log-attempt-identity.test.ts +++ b/tests/usage/request-log-attempt-identity.test.ts @@ -56,4 +56,19 @@ describe("request log attempt identity", () => { }, }); }); + + test("cache reads sum across attempts that report them under different fields", () => { + // An Anthropic leg reports both fields; an OpenAI-shaped leg only cachedInputTokens. + const anthropic = beginRequestAttempt(1, "anthropic", "model-a", "anthropic"); + noteAttemptSend(anthropic, undefined); + finishRequestAttempt(anthropic, 502, 1, { inputTokens: 100, outputTokens: 0, cachedInputTokens: 60, cacheReadInputTokens: 60 }); + const openai = beginRequestAttempt(2, "openai", "model-b", "openai-chat"); + noteAttemptSend(openai, undefined); + finishRequestAttempt(openai, 200, 1, { inputTokens: 100, outputTokens: 5, cachedInputTokens: 40 }); + + expect(aggregateAttemptUsage([anthropic, openai]).usage).toMatchObject({ + cachedInputTokens: 100, + cacheReadInputTokens: 100, + }); + }); });