diff --git a/changelog.d/fixes/10465-gemini-cached-tokens.md b/changelog.d/fixes/10465-gemini-cached-tokens.md new file mode 100644 index 0000000000..0acd31720a --- /dev/null +++ b/changelog.d/fixes/10465-gemini-cached-tokens.md @@ -0,0 +1 @@ +- **fix(usage):** surface Gemini `cachedContentTokenCount` into `cached_tokens` for non-streaming requests so cache-hit accounting matches the OpenAI/Claude/Responses branches and the streaming path (follow-up to the #10430 envelope fix) ([#10465](https://github.com/diegosouzapw/OmniRoute/pull/10465)) — thanks @rqzbeh diff --git a/open-sse/handlers/usageExtractor.ts b/open-sse/handlers/usageExtractor.ts index 313d1ff0e4..f424996ca4 100644 --- a/open-sse/handlers/usageExtractor.ts +++ b/open-sse/handlers/usageExtractor.ts @@ -104,6 +104,7 @@ export function extractUsageFromResponse(responseBody, provider) { return { prompt_tokens: usageMetadata.promptTokenCount || 0, completion_tokens: (usageMetadata.candidatesTokenCount || 0) + thoughts, + cached_tokens: usageMetadata.cachedContentTokenCount || 0, reasoning_tokens: thoughts, }; } diff --git a/tests/unit/usage-extractor.test.ts b/tests/unit/usage-extractor.test.ts index 851032690c..af328e52b6 100644 --- a/tests/unit/usage-extractor.test.ts +++ b/tests/unit/usage-extractor.test.ts @@ -228,6 +228,7 @@ test("extractUsageFromResponse reads Gemini usageMetadata and thinking tokens", assert.deepEqual(usage, { prompt_tokens: 11, completion_tokens: 7, + cached_tokens: 0, reasoning_tokens: 2, }); }); @@ -252,6 +253,7 @@ test("extractUsageFromResponse reads Gemini usageMetadata from the antigravity r assert.deepEqual(usage, { prompt_tokens: 42, completion_tokens: 17, + cached_tokens: 7, reasoning_tokens: 4, }); }); @@ -268,10 +270,34 @@ test("extractUsageFromResponse prefers top-level usageMetadata over the envelope assert.deepEqual(usage, { prompt_tokens: 1, completion_tokens: 2, + cached_tokens: 0, reasoning_tokens: 0, }); }); +test("extractUsageFromResponse surfaces Gemini cachedContentTokenCount as cached_tokens", () => { + // Review follow-up on #10430: match the OpenAI/Claude/Responses branches and + // the streaming path (usageTracking.ts) by surfacing Gemini cache-hit tokens. + const usage = extractUsageFromResponse( + { + usageMetadata: { + promptTokenCount: 30, + candidatesTokenCount: 10, + thoughtsTokenCount: 3, + cachedContentTokenCount: 12, + }, + }, + "gemini" + ); + + assert.deepEqual(usage, { + prompt_tokens: 30, + completion_tokens: 13, + cached_tokens: 12, + reasoning_tokens: 3, + }); +}); + test("extractUsageFromResponse returns null when usage is missing", () => { const usage = extractUsageFromResponse( {