fix(translator): stop double-counting cached tokens in Gemini to Claude usage (#12863)

* fix(translator): stop double-counting cached tokens in Gemini→Claude usage

* docs(changelog): add fragment for gemini-to-claude cached input tokens fix
This commit is contained in:
Thiago Mafra
2026-09-18 11:29:38 -03:00
committed by GitHub
parent e20f5f34ea
commit 7a18f344b3
3 changed files with 52 additions and 2 deletions

View File

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

View File

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

View File

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