fix(usage): preserve nested cached_tokens in canonicalizeUsage

buildUsage() only emits cache reads under prompt_tokens_details, so the
top-level-only read dropped the count for every Responses-format provider
(codex, grok-cli, ...), persisting cached_tokens: 0 and billing cache hits
at the full input rate. Mirror the cache_creation fallback already used
just above.
This commit is contained in:
Óscar Fonseca
2026-08-28 16:41:08 +07:00
parent d91e8b85e0
commit 4a371d1d9f
2 changed files with 17 additions and 2 deletions

View File

@@ -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 = {

View File

@@ -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);