From 7a18f344b368e9da72b4587ada1aede8a5f9c630 Mon Sep 17 00:00:00 2001 From: Thiago Mafra Date: Fri, 18 Sep 2026 11:29:38 -0300 Subject: [PATCH] fix(translator): stop double-counting cached tokens in Gemini to Claude usage (#12863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(translator): stop double-counting cached tokens in Gemini→Claude usage * docs(changelog): add fragment for gemini-to-claude cached input tokens fix --- ...63-gemini-to-claude-cached-input-tokens.md | 1 + .../translator/response/gemini-to-claude.ts | 2 +- .../translator-resp-gemini-to-claude.test.ts | 51 ++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/12863-gemini-to-claude-cached-input-tokens.md diff --git a/changelog.d/fixes/12863-gemini-to-claude-cached-input-tokens.md b/changelog.d/fixes/12863-gemini-to-claude-cached-input-tokens.md new file mode 100644 index 0000000000..915d5ffa68 --- /dev/null +++ b/changelog.d/fixes/12863-gemini-to-claude-cached-input-tokens.md @@ -0,0 +1 @@ +- **fix(translator):** Gemini to Claude usage no longer double-counts the cached prompt prefix — `input_tokens` now excludes `cache_read_input_tokens`, matching the Anthropic Messages semantics ([#12863](https://github.com/diegosouzapw/OmniRoute/pull/12863)) diff --git a/open-sse/translator/response/gemini-to-claude.ts b/open-sse/translator/response/gemini-to-claude.ts index 8b06d8707a..523f55d6b1 100644 --- a/open-sse/translator/response/gemini-to-claude.ts +++ b/open-sse/translator/response/gemini-to-claude.ts @@ -397,7 +397,7 @@ export function geminiToClaudeResponse(chunk, state) { typeof usageMeta.cachedContentTokenCount === "number" ? usageMeta.cachedContentTokenCount : 0; state.usage = { - input_tokens: inputTokens, + input_tokens: Math.max(0, inputTokens - cachedTokens), output_tokens: candidatesTokens + thoughtsTokens, }; if (cachedTokens > 0) { diff --git a/tests/unit/translator-resp-gemini-to-claude.test.ts b/tests/unit/translator-resp-gemini-to-claude.test.ts index 4e63824842..85917db19b 100644 --- a/tests/unit/translator-resp-gemini-to-claude.test.ts +++ b/tests/unit/translator-resp-gemini-to-claude.test.ts @@ -107,7 +107,7 @@ test("Gemini -> Claude stream: functionCall becomes tool_use and MAX_TOKENS maps assert.equal(result[2].delta.partial_json, JSON.stringify({ path: "/tmp/a" })); assert.equal(result[3].type, "content_block_stop"); assert.equal(result[4].delta.stop_reason, "tool_use"); - assert.equal(result[4].usage.input_tokens, 5); + assert.equal(result[4].usage.input_tokens, 4); assert.equal(result[4].usage.output_tokens, 5); assert.equal(result[4].usage.cache_read_input_tokens, 1); assert.equal(result[5].type, "message_stop"); @@ -190,3 +190,52 @@ test("Gemini -> Claude stream: response wrapper is supported and promptFeedback- assert.equal(wrapped[2].delta.text, "wrapped"); assert.equal(geminiToClaudeResponse({ promptFeedback: { blockReason: "SAFETY" } }, {}), null); }); + +test("Gemini -> Claude stream: input_tokens excludes cached tokens", () => { + const result = geminiToClaudeResponse( + { + responseId: "resp-6", + modelVersion: "gemini-2.5-pro", + candidates: [ + { + content: { parts: [{ text: "done" }] }, + finishReason: "STOP", + }, + ], + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: 7, + cachedContentTokenCount: 90, + }, + }, + {} + ); + + const delta = result.find((event) => event.type === "message_delta"); + assert.equal(delta.usage.input_tokens, 10); + assert.equal(delta.usage.cache_read_input_tokens, 90); +}); + +test("Gemini -> Claude stream: input_tokens equals promptTokenCount when nothing is cached", () => { + const result = geminiToClaudeResponse( + { + responseId: "resp-7", + modelVersion: "gemini-2.5-pro", + candidates: [ + { + content: { parts: [{ text: "done" }] }, + finishReason: "STOP", + }, + ], + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: 7, + }, + }, + {} + ); + + const delta = result.find((event) => event.type === "message_delta"); + assert.equal(delta.usage.input_tokens, 100); + assert.equal(delta.usage.cache_read_input_tokens, undefined); +});