From 4309a2fd56540ee3d0f7b8a2255a1c63231bdc13 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:29 +0200 Subject: [PATCH] fix(antigravity): preserve thought token usage (#13055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right fix: `thoughtsTokenCount` is real output the caller paid for, so folding it into `completion_tokens` and surfacing it as `completion_tokens_details.reasoning_tokens` matches what every other reasoning-capable provider reports. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../13055-antigravity-thought-token-usage.md | 1 + open-sse/executors/antigravity/sseCollect.ts | 9 ++++-- tests/unit/executor-antigravity.test.ts | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/13055-antigravity-thought-token-usage.md diff --git a/changelog.d/fixes/13055-antigravity-thought-token-usage.md b/changelog.d/fixes/13055-antigravity-thought-token-usage.md new file mode 100644 index 0000000000..f19f31a0a5 --- /dev/null +++ b/changelog.d/fixes/13055-antigravity-thought-token-usage.md @@ -0,0 +1 @@ +- **fix(antigravity):** Preserve upstream thought-token usage in normalized completion and reasoning token counts ([#13055](https://github.com/diegosouzapw/OmniRoute/pull/13055)) — thanks @pacocartones diff --git a/open-sse/executors/antigravity/sseCollect.ts b/open-sse/executors/antigravity/sseCollect.ts index 5b7ef3ea85..ee4de7c11b 100644 --- a/open-sse/executors/antigravity/sseCollect.ts +++ b/open-sse/executors/antigravity/sseCollect.ts @@ -18,8 +18,7 @@ export type AntigravityCollectedStream = { // Both run once per SSE data line / per text part (processAntigravitySSEPayload), // so the literals are hoisted to module constants. -const TEXTUAL_TOOL_CALL_RE = - /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/; +const TEXTUAL_TOOL_CALL_RE = /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/; export function stripZeroWidth(value: unknown): unknown { if (typeof value === "string") { @@ -145,10 +144,14 @@ export function processAntigravitySSEPayload( } if (parsed?.response?.usageMetadata) { const um = parsed.response.usageMetadata; + const thoughtsTokens = typeof um.thoughtsTokenCount === "number" ? um.thoughtsTokenCount : 0; collected.usage = { prompt_tokens: um.promptTokenCount || 0, - completion_tokens: um.candidatesTokenCount || 0, + completion_tokens: (um.candidatesTokenCount || 0) + thoughtsTokens, total_tokens: um.totalTokenCount || 0, + ...(thoughtsTokens > 0 + ? { completion_tokens_details: { reasoning_tokens: thoughtsTokens } } + : {}), }; } if (Array.isArray(parsed?.remainingCredits)) { diff --git a/tests/unit/executor-antigravity.test.ts b/tests/unit/executor-antigravity.test.ts index facd6ba68b..befff78723 100644 --- a/tests/unit/executor-antigravity.test.ts +++ b/tests/unit/executor-antigravity.test.ts @@ -36,6 +36,7 @@ type ChatCompletionPayload = { prompt_tokens: number; completion_tokens: number; total_tokens: number; + completion_tokens_details?: { reasoning_tokens: number }; }; }; @@ -482,6 +483,33 @@ test("AntigravityExecutor.collectStreamToResponse turns SSE Gemini chunks into a }); }); +test("AntigravityExecutor.collectStreamToResponse preserves upstream thought token usage", async () => { + const executor = new AntigravityExecutor(); + const response = new Response( + 'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Done"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"thoughtsTokenCount":7,"totalTokenCount":15}}}\n\n', + { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + } + ); + + const result = await executor.collectStreamToResponse( + response, + "gemini-3.7-pro-high", + "https://example.com", + { Authorization: "Bearer ag-token" }, + { request: {} } + ); + const payload = (await result.response.json()) as ChatCompletionPayload; + + assert.deepEqual(payload.usage, { + prompt_tokens: 5, + completion_tokens: 10, + total_tokens: 15, + completion_tokens_details: { reasoning_tokens: 7 }, + }); +}); + test("AntigravityExecutor.collectStreamToResponse converts textual tool call SSE to structured tool_calls", async () => { const executor = new AntigravityExecutor(); const response = new Response(