diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 6abe29c8bb..f1e538d172 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -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); + } } } } diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 7142f32b50..1947c702d6 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -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(); - 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 = {}; - 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; } diff --git a/tests/unit/executor-antigravity.test.ts b/tests/unit/executor-antigravity.test.ts index b5bbcd0587..993ca818af 100644 --- a/tests/unit/executor-antigravity.test.ts +++ b/tests/unit/executor-antigravity.test.ts @@ -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); +});