Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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 = {
Expand Down
33 changes: 29 additions & 4 deletions tests/adapters/google/google-claude-prefill-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)" }] });
});
});
Loading