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:
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user