fix(antigravity): preserve thought token usage (#13055)

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.
This commit is contained in:
Paco Cartones
2026-09-11 22:41:29 +02:00
committed by GitHub
parent abe234e094
commit 4309a2fd56
3 changed files with 35 additions and 3 deletions

View File

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

View File

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

View File

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