fix(antigravity): bypass gemini mapping for claude models on vertex ai

This commit is contained in:
diegosouzapw
2026-05-04 23:11:03 -03:00
parent d07313333f
commit da089b09f6
3 changed files with 96 additions and 163 deletions

View File

@@ -295,66 +295,72 @@ export class AntigravityExecutor extends BaseExecutor {
}
const upstreamModel = cleanModelName(model);
const isClaude = upstreamModel.toLowerCase().includes("claude");
const baseBody = body && typeof body === "object" ? body : {};
const normalizedBody = shouldStripCloudCodeThinking(this.provider, upstreamModel)
? stripCloudCodeThinkingConfig(baseBody)
: baseBody;
// Fix contents for Claude models via Antigravity
const normalizedContents =
normalizedBody.request?.contents?.map((c) => {
let role = c.role;
// functionResponse must be role "user" for Claude models
if (c.parts?.some((p) => p.functionResponse)) {
role = "user";
let transformedRequest;
if (isClaude) {
// Claude models on Vertex AI Cloud Code expect the native Anthropic payload
// exactly as generated by openaiToClaudeRequestForAntigravity, without Gemini mappings.
transformedRequest = {
...normalizedBody.request,
sessionId: normalizedBody.request?.sessionId || this.generateSessionId(),
};
} else {
// Fix contents for Gemini models via Antigravity
const normalizedContents =
normalizedBody.request?.contents?.map((c) => {
let role = c.role;
if (c.parts?.some((p) => p.functionResponse)) {
role = "user";
}
const hasFunctionCall = c.parts?.some((p) => p.functionCall) || false;
const parts =
c.parts?.filter((p) => {
if (typeof p.text === "string" && p.text === "") return false;
if (p.functionCall && !p.functionCall.name) return false;
return !p.thought && (hasFunctionCall || !p.thoughtSignature);
}) || [];
return { ...c, role, parts };
}) || [];
const contents = [];
for (const c of normalizedContents) {
if (!Array.isArray(c.parts) || c.parts.length === 0) continue;
if (contents.length > 0 && contents[contents.length - 1].role === c.role) {
contents[contents.length - 1].parts.push(...c.parts);
} else {
contents.push(c);
}
const hasFunctionCall = c.parts?.some((p) => p.functionCall) || false;
// Antigravity rejects synthetic thought text, but Gemini 3+ requires any
// returned thoughtSignature metadata to survive model tool-call turns.
const parts =
c.parts?.filter((p) => {
// Drop empty text parts
if (typeof p.text === "string" && p.text === "") return false;
// Drop empty functionCalls
if (p.functionCall && !p.functionCall.name) return false;
return !p.thought && (hasFunctionCall || !p.thoughtSignature);
}) || [];
return { ...c, role, parts };
}) || [];
// Merge consecutive same-role entries and filter out empty sequences
const contents = [];
for (const c of normalizedContents) {
if (!Array.isArray(c.parts) || c.parts.length === 0) continue;
if (contents.length > 0 && contents[contents.length - 1].role === c.role) {
contents[contents.length - 1].parts.push(...c.parts);
} else {
contents.push(c);
}
}
const transformedRequest = {
...normalizedBody.request,
...(contents.length > 0 && { contents }),
sessionId: normalizedBody.request?.sessionId || this.generateSessionId(),
safetySettings: undefined,
toolConfig:
normalizedBody.request?.tools?.length > 0
? { functionCallingConfig: { mode: "VALIDATED" } }
: normalizedBody.request?.toolConfig,
};
transformedRequest = {
...normalizedBody.request,
...(contents.length > 0 && { contents }),
sessionId: normalizedBody.request?.sessionId || this.generateSessionId(),
safetySettings: undefined,
toolConfig:
normalizedBody.request?.tools?.length > 0
? { functionCallingConfig: { mode: "VALIDATED" } }
: normalizedBody.request?.toolConfig,
};
// Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor")
const requestContents = transformedRequest.contents;
if (Array.isArray(requestContents)) {
for (const msg of requestContents) {
if (Array.isArray(msg.parts)) {
for (const part of msg.parts) {
if (typeof part.text === "string") {
part.text = obfuscateSensitiveWords(part.text);
// Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor")
const requestContents = transformedRequest.contents;
if (Array.isArray(requestContents)) {
for (const msg of requestContents) {
if (Array.isArray(msg.parts)) {
for (const part of msg.parts) {
if (typeof part.text === "string") {
part.text = obfuscateSensitiveWords(part.text);
}
}
}
}

View File

@@ -57,7 +57,8 @@ type GeminiFunctionDeclaration = {
type GeminiRequest = {
model: string;
contents: GeminiContent[];
contents?: GeminiContent[];
[key: string]: any;
generationConfig: GeminiGenerationConfig;
safetySettings: unknown;
systemInstruction?: GeminiContent;
@@ -79,7 +80,8 @@ type CloudCodeEnvelope = {
request: {
session_id?: string;
sessionId?: string;
contents: GeminiContent[];
contents?: GeminiContent[];
[key: string]: any;
systemInstruction?: GeminiContent;
generationConfig: GeminiGenerationConfig;
tools?: Array<{
@@ -475,12 +477,6 @@ function wrapInCloudCodeEnvelopeForClaude(
credentials = null,
sourceBody = {}
) {
const toolNameMap = new Map<string, string>();
const sanitizeToolName = (name: string) =>
sanitizeGeminiToolName(name, {
stripNamespace: true,
toolNameMap,
});
let projectId = credentials?.projectId;
if (!projectId) {
@@ -493,11 +489,6 @@ function wrapInCloudCodeEnvelopeForClaude(
const cleanModel = model.includes("/") ? model.split("/").pop()! : model;
const generationConfig: GeminiGenerationConfig = {
temperature: claudeRequest.temperature || 1,
maxOutputTokens: getAntigravityClaudeOutputTokens(sourceBody),
};
const envelope: CloudCodeEnvelope = {
project: projectId,
model: cleanModel,
@@ -505,109 +496,11 @@ function wrapInCloudCodeEnvelopeForClaude(
requestId: `agent-${generateUUID()}`,
requestType: "agent",
request: {
...claudeRequest,
sessionId: generateSessionId(),
contents: [],
generationConfig,
},
};
const toolUseNames: Record<string, string> = {};
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
for (const msg of claudeRequest.messages) {
if (!Array.isArray(msg.content)) continue;
for (const block of msg.content) {
if (block.type === "tool_use" && block.id && typeof block.name === "string") {
toolUseNames[block.id] = sanitizeToolName(block.name);
}
}
}
}
// Convert Claude messages to Gemini contents
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
for (const msg of claudeRequest.messages) {
const parts = [];
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "text") {
parts.push({ text: block.text });
} else if (block.type === "image" && block.source) {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data,
},
});
} else if (block.type === "tool_use") {
parts.push({
functionCall: {
id: block.id,
name: sanitizeToolName(block.name),
args: block.input || {},
},
});
} else if (block.type === "tool_result") {
let content = block.content;
if (Array.isArray(content)) {
content = content
.map((c) => (c.type === "text" ? c.text : JSON.stringify(c)))
.join("\n");
}
parts.push({
functionResponse: {
id: block.tool_use_id,
name: toolUseNames[block.tool_use_id] || "unknown",
response: { result: tryParseJSON(content) || content },
},
});
}
}
} else if (typeof msg.content === "string") {
parts.push({ text: msg.content });
}
if (parts.length > 0) {
envelope.request.contents.push({
role: msg.role === "assistant" ? "model" : "user",
parts,
});
}
}
}
// Convert Claude tools to Gemini functionDeclarations
if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
const geminiTools = buildGeminiTools(claudeRequest.tools, {
stripNamespace: true,
toolNameMap,
});
if (geminiTools) {
envelope.request.tools = geminiTools;
}
}
// Keep Antigravity's default and caller-provided system rules as distinct parts,
// matching the Gemini bridge and avoiding accidental prompt concatenation.
const systemParts: GeminiPart[] = [{ text: ANTIGRAVITY_DEFAULT_SYSTEM }];
if (claudeRequest.system) {
if (Array.isArray(claudeRequest.system)) {
for (const block of claudeRequest.system) {
if (block.text) systemParts.push({ text: block.text });
}
} else if (typeof claudeRequest.system === "string") {
systemParts.push({ text: claudeRequest.system });
}
}
envelope.request.systemInstruction = { role: "system", parts: systemParts };
const changedToolNameMap = buildChangedToolNameMap(toolNameMap);
if (changedToolNameMap) {
envelope._toolNameMap = changedToolNameMap;
}
return envelope;
}

View File

@@ -503,3 +503,37 @@ test("AntigravityExecutor.execute applies CLI fingerprint when enabled", async (
globalThis.fetch = originalFetch;
}
});
test("AntigravityExecutor.transformRequest bypasses Gemini contents mapping for claude models", async () => {
const executor = new AntigravityExecutor();
const body = {
project: "project-1",
model: "claude-sonnet-4-6",
userAgent: "antigravity",
requestId: "agent-123",
requestType: "agent",
request: {
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
system: [{ type: "text", text: "System prompt" }],
generationConfig: {
temperature: 1,
maxOutputTokens: 16384,
},
},
};
const result = (await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
})) as any;
assert.equal(result.project, "project-1");
assert.equal(result.model, "claude-sonnet-4-6");
assert.equal(result.requestType, "agent");
assert.ok(result.request.sessionId);
assert.deepEqual(result.request.messages, [
{ role: "user", content: [{ type: "text", text: "Hello" }] },
]);
assert.deepEqual(result.request.system, [{ type: "text", text: "System prompt" }]);
assert.equal(result.request.contents, undefined);
assert.equal(result.request.toolConfig, undefined);
});