From e2b4c2b06e8204936d379571fb7e76b87ae29ed9 Mon Sep 17 00:00:00 2001 From: Anton <39598727+NomenAK@users.noreply.github.com> Date: Thu, 14 May 2026 05:15:40 +0200 Subject: [PATCH] fix(antigravity): strip generationConfig.thinkingConfig for Claude models (#2217) Integrated into release/v3.8.0 --- .../translator/request/openai-to-gemini.ts | 14 ++++++ .../unit/translator-openai-to-gemini.test.ts | 45 ++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index abdd49d133..1d4d016826 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -547,6 +547,10 @@ export function openaiToAntigravityRequest(model, body, stream, credentials = nu // Match real Antigravity client: don't send maxOutputTokens when the user // hasn't explicitly specified max_tokens / max_completion_tokens. // The Cloud Code server decides the output limit on its own. + // Note: read hasThinking BEFORE stripping thinkingConfig below — for Claude + // models the Cloud Code envelope still carries a thinkingBudget set upstream + // by applyAntigravityGenerationDefaults, which we must consult here so we + // do not accidentally drop the maxOutputTokens it bumped for us. const clientRequestedMaxTokens = body.max_tokens ?? body.max_completion_tokens; const hasThinking = !!envelope.request?.generationConfig?.thinkingConfig?.thinkingBudget; if ( @@ -557,6 +561,16 @@ export function openaiToAntigravityRequest(model, body, stream, credentials = nu delete envelope.request.generationConfig.maxOutputTokens; } + // Claude models on Antigravity use their own native thinking — Gemini's thinkingConfig + // is not understood by the Cloud Code Claude endpoint and must be stripped. + // applyAntigravityGenerationDefaults (inside wrapInCloudCodeEnvelope) already bumped + // maxOutputTokens to thinkingBudget+1 before we get here, so the budget is preserved. + // Must run AFTER the hasThinking-derived maxOutputTokens decision above so the + // budget is accounted for before the field is removed. + if (isClaude && envelope.request?.generationConfig) { + delete envelope.request.generationConfig.thinkingConfig; + } + return envelope; } diff --git a/tests/unit/translator-openai-to-gemini.test.ts b/tests/unit/translator-openai-to-gemini.test.ts index 8bf929d858..69f68d3783 100644 --- a/tests/unit/translator-openai-to-gemini.test.ts +++ b/tests/unit/translator-openai-to-gemini.test.ts @@ -756,7 +756,11 @@ test("OpenAI -> Antigravity Claude path sanitizes tool names for Gemini schema", assert.deepEqual(toolResultBlock.response, { result: { ok: true } }); }); -test("OpenAI -> Antigravity Claude path applies output cap in generationConfig", () => { +test("OpenAI -> Antigravity Claude path applies output cap and strips thinkingConfig", () => { + // For Claude on Antigravity, applyAntigravityGenerationDefaults must bump + // maxOutputTokens to thinkingBudget+1 BEFORE the envelope strips thinkingConfig + // (because Claude on Cloud Code does not understand Gemini's thinkingConfig + // shape but still benefits from the larger output cap derived from it). const result = openaiToAntigravityRequest( "claude-3-7-sonnet", { @@ -769,15 +773,14 @@ test("OpenAI -> Antigravity Claude path applies output cap in generationConfig", ); assert.equal((result as any).request?.generationConfig.maxOutputTokens, 32769); - assert.deepEqual((result as any).request?.generationConfig.thinkingConfig, { - thinkingBudget: 32768, - includeThoughts: true, - }); + // thinkingConfig must be stripped for Claude — Cloud Code Claude endpoint + // does not understand the Gemini-shape thinkingConfig field. + assert.equal((result as any).request?.generationConfig.thinkingConfig, undefined); assert.equal((result as any).request?.max_tokens, undefined); assert.equal((result as any).request?.thinking, undefined); }); -test("OpenAI -> Antigravity Claude path preserves lower requested output", () => { +test("OpenAI -> Antigravity Claude path preserves lower requested output and strips thinkingConfig", () => { const result = openaiToAntigravityRequest( "claude-3-7-sonnet", { @@ -790,10 +793,32 @@ test("OpenAI -> Antigravity Claude path preserves lower requested output", () => ); assert.equal((result as any).request?.generationConfig.maxOutputTokens, 32769); - assert.deepEqual((result as any).request?.generationConfig.thinkingConfig, { - thinkingBudget: 32768, - includeThoughts: true, - }); + assert.equal((result as any).request?.generationConfig.thinkingConfig, undefined); assert.equal((result as any).request?.max_tokens, undefined); assert.equal((result as any).request?.thinking, undefined); }); + +test("OpenAI -> Antigravity Gemini path preserves thinkingConfig (only Claude is stripped)", () => { + // Negative-control for the Claude-thinkingConfig-strip behavior. Gemini models + // on Antigravity must still receive thinkingConfig — only Claude needs it removed + // (Cloud Code Claude endpoint does not understand the Gemini-shape field). + const result = openaiToAntigravityRequest( + "gemini-2.5-pro", + { + messages: [{ role: "user", content: "Summarize this" }], + max_completion_tokens: 32000, + reasoning_effort: "high", + }, + false, + { projectId: "proj-gemini-thinking" } as any + ); + + // For Gemini, thinkingConfig must remain in place because the Cloud Code + // Gemini endpoint understands and uses it. + assert.ok( + (result as any).request?.generationConfig.thinkingConfig, + "thinkingConfig must be preserved for Gemini models on Antigravity" + ); + assert.equal((result as any).request?.generationConfig.thinkingConfig.thinkingBudget > 0, true); + assert.equal((result as any).request?.generationConfig.thinkingConfig.includeThoughts, true); +});