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)" }] }); }); });