From 14ce693e5846596c823941ce90add538713a25b1 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 7 Sep 2026 23:05:14 -0300 Subject: [PATCH 1/2] fix(google): guard model-tail histories with user continue nudge across Gemini and CCA Google Gemini (via Cloud Code Assist / Antigravity, Vertex AI, and AI Studio) rejects requests whose conversation history ends on a model turn with HTTP 400 INVALID_ARGUMENT ("Requests ending with a model turn are not supported."). Previously, the continue nudge was only attached for Claude-on-Antigravity under the assumption that Gemini natively accepts model-tail histories. In practice, autonomous multi-turn loops (such as subagent orchestration and continuation turns in Codex CLI) produce histories ending in an assistant message, triggering the 400. This patch: - Injects a synthetic user `(continue)` nudge in `messagesToGeminiFormat` whenever the reconstructed `contents` ends with `role: "model"` or is empty. - Broadens the post-replay Antigravity tail check to cover all Google models. - Updates unit tests to verify that Gemini 3.7 Flash, Gemini 3.8 Flash, and AI Studio endpoints all receive the continue nudge on model-tail contexts. Co-authored-by: CommandCodeBot (cherry picked from commit 1837b8f99ed6bf5bf99cac841335a3abf64f416f) --- src/adapters/google.ts | 32 ++++++++++++------ .../google-claude-prefill-guard.test.ts | 33 ++++++++++++++++--- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 9d828af0bd..5b4342ec30 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -429,6 +429,18 @@ function messagesToGeminiFormat( } } + // Gemini API and Claude-on-Antigravity reject assistant-tail (model-tail in Gemini terms) + // histories. Gemini fails upstream with "Requests ending with a model turn are not supported" + // (HTTP 400), while Claude fails with "This model does not support assistant message prefill. + // The conversation must end with a user message." Context compaction, previous_response_id + // expansion, subagent orchestration, and interrupted-turn replay can all produce a + // model-tail history. Append a user "(continue)" nudge, mirroring the anthropic adapter's + // tail guard (src/adapters/anthropic.ts). + const lastTurn = contents.length > 0 ? (contents[contents.length - 1] as { role?: string }) : undefined; + if (!lastTurn || lastTurn.role === "model") { + contents.push({ role: "user", parts: [{ text: "(continue)" }] }); + } + return { systemInstruction, contents, replayedCallIds }; } @@ -894,16 +906,16 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // fills a first functionCall that replay could not sign. Outside the cache branch too, // because the turn still needs a signature when no session was ever recorded. applyAntigravityThoughtSignatureFallback(wireModelId, contents); - // Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories - // as prefill: "This model does not support assistant message prefill. The conversation - // must end with a user message." Context compaction, previous_response_id expansion, - // and interrupted-turn replay can all produce a model-tail history. Append a user - // "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts). - if (/claude/i.test(wireModelId)) { - const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined; - if (!last || last.role === "model") { - contents.push({ role: "user", parts: [{ text: "(continue)" }] }); - } + // Gemini and Claude-on-Antigravity reject assistant-tail (model-tail in Gemini terms) + // histories. Gemini fails upstream with "Requests ending with a model turn are not supported" + // (HTTP 400), while Claude fails with "This model does not support assistant message prefill. + // The conversation must end with a user message." Context compaction, previous_response_id + // expansion, subagent orchestration, and interrupted-turn replay can all produce a + // model-tail history. Append a user "(continue)" nudge, mirroring the anthropic adapter's + // tail guard (src/adapters/anthropic.ts). + const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined; + if (!last || last.role === "model") { + contents.push({ role: "user", parts: [{ text: "(continue)" }] }); } } const envelope = { diff --git a/tests/adapters/google/google-claude-prefill-guard.test.ts b/tests/adapters/google/google-claude-prefill-guard.test.ts index 72dbd1d0e4..bbaf30bf73 100644 --- a/tests/adapters/google/google-claude-prefill-guard.test.ts +++ b/tests/adapters/google/google-claude-prefill-guard.test.ts @@ -78,14 +78,39 @@ describe("google claude prefill guard", () => { expect(JSON.stringify(contents.at(-1))).not.toContain("(continue)"); }); - test("does not append nudge for non-Claude models on Antigravity", async () => { + test("appends a user continue nudge when Gemini context ends with model turn", async () => { const contents = await envelopeContents(parsed([ { role: "user", content: "start", timestamp: 0 }, { role: "assistant", content: [{ type: "text", text: "answer" }], model: "gemini", timestamp: 0 }, ], "gemini-3.7-flash")); - // Gemini natively accepts model-tail; no nudge - expect(contents.at(-1)!.role).toBe("model"); - expect(JSON.stringify(contents)).not.toContain("(continue)"); + // Google Gemini strictly rejects requests ending with a model turn with HTTP 400 + // "Requests ending with a model turn are not supported." A user continue nudge is required. + expect(contents.at(-1)).toEqual({ role: "user", parts: [{ text: "(continue)" }] }); + }); + + test("appends a user continue nudge for Gemini 3.8 Flash on Antigravity", async () => { + const contents = await envelopeContents(parsed([ + { role: "user", content: "start", timestamp: 0 }, + { role: "assistant", content: [{ type: "text", text: "answer" }], model: "gemini", timestamp: 0 }, + ], "gemini-3.8-flash")); + + expect(contents.at(-1)).toEqual({ role: "user", parts: [{ text: "(continue)" }] }); + }); + + test("appends a user continue nudge in AI Studio mode", async () => { + const aiStudioProvider = { + adapter: "google", + baseUrl: "https://generativelanguage.googleapis.com", + apiKey: "key-123", + } as OcxProviderConfig; + + const { body } = await createGoogleAdapter(aiStudioProvider).buildRequest(parsed([ + { role: "user", content: "hello", timestamp: 0 }, + { role: "assistant", content: [{ type: "text", text: "assistant reply" }], model: "gemini", timestamp: 0 }, + ], "gemini-2.5-flash")); + + const payload = JSON.parse(body); + expect(payload.contents.at(-1)).toEqual({ role: "user", parts: [{ text: "(continue)" }] }); }); }); From 16d49ceab82f69d6d37ca47f331af1603f441aeb Mon Sep 17 00:00:00 2001 From: t Date: Tue, 8 Sep 2026 21:47:13 +0900 Subject: [PATCH 2/2] fix(google): keep a single owner for the model-tail continue nudge The carried change added the nudge to messagesToGeminiFormat for every Google mode and also widened the post-replay Antigravity check from Claude-only to all models, leaving two injection sites on one request. The second is only a no-op while nothing between them reshapes the tail; the signature sanitizer can. Drop the post-replay copy so the formatter owns the invariant. --- src/adapters/google.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 5b4342ec30..9e1b46307e 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -906,17 +906,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // fills a first functionCall that replay could not sign. Outside the cache branch too, // because the turn still needs a signature when no session was ever recorded. applyAntigravityThoughtSignatureFallback(wireModelId, contents); - // Gemini and Claude-on-Antigravity reject assistant-tail (model-tail in Gemini terms) - // histories. Gemini fails upstream with "Requests ending with a model turn are not supported" - // (HTTP 400), while Claude fails with "This model does not support assistant message prefill. - // The conversation must end with a user message." Context compaction, previous_response_id - // expansion, subagent orchestration, and interrupted-turn replay can all produce a - // model-tail history. Append a user "(continue)" nudge, mirroring the anthropic adapter's - // tail guard (src/adapters/anthropic.ts). - const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined; - if (!last || last.role === "model") { - contents.push({ role: "user", parts: [{ text: "(continue)" }] }); - } + // The model-tail "(continue)" guard runs once, in messagesToGeminiFormat, so CCA, + // Vertex and AI Studio share one decision. A second check here would append a + // duplicate nudge whenever signature sanitization reshapes the tail afterwards. } const envelope = { model: wireModelId,