diff --git a/changelog.d/fixes/10430-antigravity-usage-envelope.md b/changelog.d/fixes/10430-antigravity-usage-envelope.md new file mode 100644 index 0000000000..645045e7ea --- /dev/null +++ b/changelog.d/fixes/10430-antigravity-usage-envelope.md @@ -0,0 +1 @@ +- **fix(usage):** read Gemini `usageMetadata` out of the antigravity `{ response: {...} }` envelope so non-streaming requests log real token usage instead of `IN 0 | OUT 0` (port of decolua/9router#59d858b) ([#10430](https://github.com/diegosouzapw/OmniRoute/pull/10430)) — thanks @rqzbeh diff --git a/open-sse/handlers/usageExtractor.ts b/open-sse/handlers/usageExtractor.ts index 8239c3feb9..313d1ff0e4 100644 --- a/open-sse/handlers/usageExtractor.ts +++ b/open-sse/handlers/usageExtractor.ts @@ -93,14 +93,17 @@ export function extractUsageFromResponse(responseBody, provider) { }; } - // Gemini format - if (responseBody.usageMetadata && typeof responseBody.usageMetadata === "object") { + // Gemini format. Antigravity / gemini-cli wrap the payload in + // { response: { ... } } — read the envelope so non-streaming requests do + // not silently log zero usage (port of decolua/9router#59d858b). + const usageMetadata = responseBody.usageMetadata || responseBody.response?.usageMetadata; + if (usageMetadata && typeof usageMetadata === "object") { // Gemini reports thoughts outside candidates. Fold them into completion so // every provider keeps reasoning as a subset of completion tokens. - const thoughts = responseBody.usageMetadata.thoughtsTokenCount || 0; + const thoughts = usageMetadata.thoughtsTokenCount || 0; return { - prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0, - completion_tokens: (responseBody.usageMetadata.candidatesTokenCount || 0) + thoughts, + prompt_tokens: usageMetadata.promptTokenCount || 0, + completion_tokens: (usageMetadata.candidatesTokenCount || 0) + thoughts, reasoning_tokens: thoughts, }; } diff --git a/tests/unit/usage-extractor.test.ts b/tests/unit/usage-extractor.test.ts index 6fa9c7b100..851032690c 100644 --- a/tests/unit/usage-extractor.test.ts +++ b/tests/unit/usage-extractor.test.ts @@ -232,6 +232,46 @@ test("extractUsageFromResponse reads Gemini usageMetadata and thinking tokens", }); }); +test("extractUsageFromResponse reads Gemini usageMetadata from the antigravity response envelope", () => { + // Antigravity / gemini-cli wrap non-streaming payloads in { response: {...} } + // (port of decolua/9router#59d858b — previously logged zero usage). + const usage = extractUsageFromResponse( + { + response: { + usageMetadata: { + promptTokenCount: 42, + candidatesTokenCount: 13, + thoughtsTokenCount: 4, + cachedContentTokenCount: 7, + }, + }, + }, + "antigravity" + ); + + assert.deepEqual(usage, { + prompt_tokens: 42, + completion_tokens: 17, + reasoning_tokens: 4, + }); +}); + +test("extractUsageFromResponse prefers top-level usageMetadata over the envelope", () => { + const usage = extractUsageFromResponse( + { + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 2 }, + response: { usageMetadata: { promptTokenCount: 99, candidatesTokenCount: 99 } }, + }, + "gemini" + ); + + assert.deepEqual(usage, { + prompt_tokens: 1, + completion_tokens: 2, + reasoning_tokens: 0, + }); +}); + test("extractUsageFromResponse returns null when usage is missing", () => { const usage = extractUsageFromResponse( {