diff --git a/open-sse/utils/usageTracking.js b/open-sse/utils/usageTracking.js index 24518ef3..0d66f6f3 100644 --- a/open-sse/utils/usageTracking.js +++ b/open-sse/utils/usageTracking.js @@ -190,7 +190,10 @@ export function canonicalizeUsage(usage) { prompt = prompt + cached + cacheCreation; } else { // OpenAI/Gemini path (or already-canonical input): prompt already includes cached_tokens. - cached = num(usage.cached_tokens); + // Mirror the cacheCreation fallback above: buildUsage() only ever emits the + // nested prompt_tokens_details.cached_tokens shape, so without this the + // cache-read count is silently dropped on every buildUsage()-derived usage. + cached = num(usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens); } const result = { diff --git a/tests/unit/cached-token-usage.test.js b/tests/unit/cached-token-usage.test.js index 878110d0..9b3ebd97 100644 --- a/tests/unit/cached-token-usage.test.js +++ b/tests/unit/cached-token-usage.test.js @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { canonicalizeUsage, extractUsage, mergeUsage } from "../../open-sse/utils/usageTracking.js"; import { calculateCostFromTokens } from "../../open-sse/providers/pricing.js"; -import { toOpenAIUsage } from "../../open-sse/translator/concerns/usage.js"; +import { buildUsage, toOpenAIUsage } from "../../open-sse/translator/concerns/usage.js"; // Canonical convention (single source of truth for storage + cost): // prompt_tokens = total input INCLUDING cache read + cache creation @@ -49,6 +49,18 @@ describe("canonicalizeUsage", () => { expect(out.reasoning_tokens).toBe(40); }); + it("reads cached_tokens from the nested buildUsage() shape", () => { + // buildUsage() only emits cache reads under prompt_tokens_details. The + // Responses translator overwrites state.usage with that shape on + // response.completed, so a top-level-only read silently drops the cache + // count for every Responses provider (codex, grok-cli, ...). + const out = canonicalizeUsage( + buildUsage({ promptTokens: 330, completionTokens: 50, totalTokens: 380, cachedTokens: 200 }) + ); + expect(out.prompt_tokens).toBe(330); + expect(out.cached_tokens).toBe(200); + }); + it("handles no-cache usage", () => { const out = canonicalizeUsage({ prompt_tokens: 100, completion_tokens: 50 }); expect(out.prompt_tokens).toBe(100);