From 7f5275ed6bf8e3278a6a4e5b3f2ae7eec63c9592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:58:03 +0330 Subject: [PATCH] fix(usage): surface Gemini cachedContentTokenCount as cached_tokens (#10465) * fix(usage): read Gemini usageMetadata out of the antigravity response envelope Port decolua/9router#59d858b: antigravity/gemini-cli wrap non-streaming payloads in { response: {...} }, so extractUsageFromResponse only saw the top-level usageMetadata and every non-streaming antigravity request logged zero usage (IN 0 | OUT 0) and zeroed usage-dashboard rows. Top-level metadata keeps priority; OpenAI/Claude branches untouched. * chore(changelog): fragment for #10430 antigravity usage envelope * fix(usage): surface Gemini cachedContentTokenCount as cached_tokens Review follow-up on #10430: the Gemini branch of extractUsageFromResponse ignored cachedContentTokenCount, so non-streaming cache-hit tokens never reached the cached_tokens field the OpenAI/Claude/Responses branches already populate (and the streaming path surfaces at usageTracking.ts:684). Adds cached_tokens: usageMetadata.cachedContentTokenCount || 0, updates the three Gemini assertions (envelope fixture already carried cachedContentTokenCount: 7), and adds a dedicated regression test. * chore(changelog): fragment for #10465 Gemini cached_tokens surfacing * test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base - alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod (2026-08-16 16:00 UTC) is in the past, making every quota entry classify as expired/not_capable; bump to 2028-01-01 UTC so the text/merge classification tests exercise the intended path again. - optional-transformers-dependency.test.ts: onnxruntime-node pin assertion updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403); the regular-not-optional intent is unchanged. --------- Co-authored-by: Rouzbeh --- .../fixes/10465-gemini-cached-tokens.md | 1 + open-sse/handlers/usageExtractor.ts | 1 + tests/unit/usage-extractor.test.ts | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 changelog.d/fixes/10465-gemini-cached-tokens.md 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( {