Compare commits

...

1 Commits

Author SHA1 Message Date
adevwithpurpose
c4ee73dee0 fix(antigravity): strip trailing model turn for native Gemini requests too
Newer Gemini endpoints reject a request ending on a model turn with HTTP
400 'Requests ending with a model turn are not supported' — the same
rejection class Claude hits via Vertex. transformRequest() previously
wired stripTrailingAntigravityAssistantTurn() only into the isClaude
branch, so native Gemini models routed through Antigravity kept a
trailing role:model entry and hit the 400.

Extend the guarded strip (never empties contents) to native Gemini
models too, gated by upstreamModel including "gemini". The Claude
path is untouched (byte-identical), preserving PR #6114's live
validation against Vertex Claude.

Flips tests/unit/antigravity-claude-prefill-strip.test.ts test (b),
which previously asserted the buggy pass-through, and adds (b2) for
the gemini-3-flash-agent tier.

Closes #10104
2026-08-14 21:54:06 -03:00
3 changed files with 36 additions and 7 deletions

View File

@@ -0,0 +1 @@
- fix(antigravity): strip trailing model turn for native Gemini requests too, not just Claude (#10104)

View File

@@ -397,9 +397,10 @@ function sanitizeAntigravityGeminiRequest(
* `"assistant"`). Mirrors the trailing-strip pop-loop already used for Mistral
* (#3396), Copilot (#5802), and the CC-bridge in `claudeCodeCompatible.ts`.
*
* Scoped strictly to the Claude path by the caller (`isClaude` branch only) — native
* Gemini models via Antigravity must be unaffected, since Vertex-Claude is the only
* documented rejection surface.
* Wired in by the caller for both the Claude path (`isClaude`) and native Gemini
* models (`isGemini`, #10104) — newer Gemini endpoints reject a trailing `model` turn
* with the same "ending with a model turn" class of 400 that Claude hits via Vertex.
* Other model families routed through Antigravity are left untouched.
*
* Guard: never strip `contents` down to empty — an empty `contents` array is itself
* an invalid request, so at least one entry (even a lone trailing "model" turn) is
@@ -597,6 +598,14 @@ export class AntigravityExecutor extends BaseExecutor {
const upstreamModel = await cleanModelName(model, modelIdOverride);
const isClaude = upstreamModel.toLowerCase().includes("claude");
// #10104: newer Gemini endpoints reject a request ending on a `model` turn with
// HTTP 400 "Requests ending with a model turn are not supported" — the same
// rejection surface Claude hits via Vertex (see stripTrailingAntigravityAssistantTurn's
// doc comment above). Native Gemini models routed through Antigravity (`agy/gemini-*`,
// e.g. the Gemini 3.x Flash/Pro tiers from PR #8013's catalog) need the same guarded
// strip. Scoped to models whose id names Gemini so unrelated model families are
// untouched; the strip itself never empties `contents` (see the guard above).
const isGemini = upstreamModel.toLowerCase().includes("gemini");
const baseBody = bodyRecord;
const normalizedBody = shouldStripCloudCodeThinking(this.provider, upstreamModel)
? stripCloudCodeThinkingConfig(baseBody)
@@ -660,11 +669,16 @@ export class AntigravityExecutor extends BaseExecutor {
: normalizedRequest?.toolConfig,
};
// Note: sanitizeAntigravityGeminiRequest() applies a Claude-only field whitelist
// (dropping fields native Gemini requests may legitimately carry), so the Gemini
// branch only runs the trailing-turn strip — never the sanitize/whitelist step.
const transformedRequest = isClaude
? stripTrailingAntigravityAssistantTurn(
sanitizeAntigravityGeminiRequest(rawTransformedRequest)
)
: rawTransformedRequest;
: isGemini
? stripTrailingAntigravityAssistantTurn(rawTransformedRequest)
: rawTransformedRequest;
applyAntigravityGenerationDefaults(transformedRequest, upstreamModel);

View File

@@ -44,14 +44,28 @@ test("(a) strips a single trailing assistant (model) turn for Claude models", as
assert.equal(contents.at(-1)?.role, "user");
});
test("(b) does NOT strip a trailing model turn for non-Claude (native Gemini) models", async () => {
test("(b) strips a trailing model turn for native Gemini models too (#10104)", async () => {
// Newer Gemini endpoints reject a request ending on a `model` turn with the same
// class of 400 Claude hits via Vertex ("Requests ending with a model turn are not
// supported"), so native Gemini models routed through Antigravity get the same
// guarded strip as the Claude path.
const request = await transform("antigravity/gemini-3.1-pro", [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "Hi there" }] },
]);
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 2);
assert.equal(contents.at(-1)?.role, "model", "native Gemini requests via Antigravity are untouched");
assert.equal(contents.length, 1);
assert.equal(contents.at(-1)?.role, "user", "transformed native Gemini request must end on user");
});
test("(b2) native Gemini agent tier (gemini-3-flash-agent, #10104) also gets the strip", async () => {
const request = await transform("antigravity/gemini-3-flash-agent", [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "Hi there" }] }, // trailing model turn -> 400 source
]);
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 1, "trailing model turn should be stripped for native Gemini");
assert.equal(contents.at(-1)?.role, "user", "transformed native Gemini request must end on user");
});
test("(c) a Claude conversation already ending on user is unchanged", async () => {