mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(antigravity): bypass gemini mapping for claude models on vertex ai
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user