From 1837b8f99ed6bf5bf99cac841335a3abf64f416f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 7 Sep 2026 23:05:14 -0300 Subject: [PATCH] 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 --- 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)" }] }); }); });