Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
} else {
sanitizeAntigravityClaudeSignatures(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)" }] });
}
}
}
const envelope = {
model: wireModelId,
Expand Down
91 changes: 91 additions & 0 deletions tests/google-claude-prefill-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, test } from "bun:test";
import { createGoogleAdapter as createGoogleAdapterProduction } from "../src/adapters/google";
import type { OcxMessage, OcxParsedRequest, OcxProviderConfig } from "../src/types";
import { withTestTranslatorBudget } from "./helpers/translator-budget";

const createGoogleAdapter = (...args: Parameters<typeof createGoogleAdapterProduction>) =>
withTestTranslatorBudget(createGoogleAdapterProduction(...args));

const provider = {
adapter: "google",
baseUrl: "https://daily-cloudcode-pa.googleapis.com",
googleMode: "cloud-code-assist",
project: "proj-123",
apiKey: "ya29.token",
} as OcxProviderConfig;

function parsed(messages: OcxMessage[], modelId = "claude-opus-4-6-thinking"): OcxParsedRequest {
return {
modelId,
stream: false,
options: {},
context: { messages, systemPrompt: [], tools: [] },
} as unknown as OcxParsedRequest;
}

async function envelopeContents(p: OcxParsedRequest): Promise<{ role: string; parts: unknown[] }[]> {
const { body } = await createGoogleAdapter(provider).buildRequest(p);
const envelope = JSON.parse(body);
return envelope.request.contents;
}

describe("google claude prefill guard", () => {
test("appends a user continue nudge when CCA Claude context ends with model turn", async () => {
const contents = await envelopeContents(parsed([
{ role: "user", content: "start", timestamp: 0 },
{ role: "assistant", content: [{ type: "text", text: "partial answer" }], model: "claude", timestamp: 0 },
]));

expect(contents.at(-1)).toEqual({ role: "user", parts: [{ text: "(continue)" }] });
});

test("leaves context ending with user unchanged", async () => {
const contents = await envelopeContents(parsed([
{ role: "assistant", content: [{ type: "text", text: "answer" }], model: "claude", timestamp: 0 },
{ role: "user", content: "follow up", timestamp: 0 },
]));

expect(contents.at(-1)!.role).toBe("user");
expect(JSON.stringify(contents.at(-1))).not.toContain("(continue)");
});

test("appends nudge when CCA Claude context is empty", async () => {
const contents = await envelopeContents(parsed([]));

expect(contents.at(-1)).toEqual({ role: "user", parts: [{ text: "(continue)" }] });
});

test("does not append nudge after tool result (tool result maps to user role)", async () => {
const contents = await envelopeContents(parsed([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read_file", arguments: { path: "README.md" } }],
model: "claude",
timestamp: 0,
},
{
role: "toolResult",
toolCallId: "call_1",
toolName: "read_file",
content: "contents",
isError: false,
timestamp: 0,
},
] as OcxMessage[]));

// toolResult maps to role:"user" in Gemini format, so no nudge needed
expect(contents.at(-1)!.role).toBe("user");
expect(JSON.stringify(contents.at(-1))).not.toContain("(continue)");
});

test("does not append nudge for non-Claude models 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.7-flash"));

// Gemini natively accepts model-tail; no nudge
expect(contents.at(-1)!.role).toBe("model");
expect(JSON.stringify(contents)).not.toContain("(continue)");
});
});
Loading