fix: Antigravity INVALID_ARGUMENT errors and Copilot agent mode parity

This commit is contained in:
lukmanfauzie
2026-04-26 19:53:08 +08:00
parent 222e22fa53
commit c43f8c54d4
3 changed files with 68 additions and 13 deletions

View File

@@ -5,8 +5,18 @@ import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAU
import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { deriveSessionId } from "../utils/sessionManager.js"; import { deriveSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { cleanJSONSchemaForAntigravity } from "../translator/helpers/geminiHelper.js";
// Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63}
function sanitizeFunctionName(name) {
if (!name) return "_unknown";
let s = name.replace(/[^a-zA-Z0-9_.:\-]/g, "_");
if (!/^[a-zA-Z_]/.test(s)) s = "_" + s;
return s.substring(0, 64);
}
const MAX_RETRY_AFTER_MS = 10000; const MAX_RETRY_AFTER_MS = 10000;
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
export class AntigravityExecutor extends BaseExecutor { export class AntigravityExecutor extends BaseExecutor {
constructor() { constructor() {
@@ -53,14 +63,44 @@ export class AntigravityExecutor extends BaseExecutor {
return c; return c;
}); });
// Sanitize tool schemas and function names before sending to Antigravity.
let tools = body.request?.tools;
if (tools && tools.length > 0) {
tools = tools
.map(group => {
if (!group.functionDeclarations) return group;
const cleanedDeclarations = group.functionDeclarations.map(fn => ({
...fn,
name: sanitizeFunctionName(fn.name),
parameters: fn.parameters
? cleanJSONSchemaForAntigravity(structuredClone(fn.parameters))
: { type: "object", properties: { reason: { type: "string", description: "Brief explanation" } }, required: ["reason"] }
}));
return {
...group,
functionDeclarations: cleanedDeclarations
};
})
.filter(group => group.functionDeclarations?.length > 0)
.slice(0, 1);
}
const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {};
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
}
const transformedRequest = { const transformedRequest = {
...body.request, ...requestWithoutTools,
generationConfig,
...(contents && { contents }), ...(contents && { contents }),
...(tools && { tools }),
sessionId: body.request?.sessionId || deriveSessionId(credentials?.email || credentials?.connectionId), sessionId: body.request?.sessionId || deriveSessionId(credentials?.email || credentials?.connectionId),
safetySettings: undefined, safetySettings: undefined,
toolConfig: body.request?.tools?.length > 0 ...(tools?.length > 0 && { toolConfig: { functionCallingConfig: { mode: "VALIDATED" } } })
? { functionCallingConfig: { mode: "VALIDATED" } }
: body.request?.toolConfig
}; };
return { return {

View File

@@ -140,14 +140,11 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
} }
} }
// Antigravity cloaking: rename client tools + inject decoys (anti-ban) // Antigravity cloaking/tool stripping is intentionally disabled for GitHub Copilot.
// Only apply for GitHub Copilot requests so other clients are unaffected. // Keep the translated request intact; final provider-specific sanitization happens
// in the Antigravity executor.
if (provider === FORMATS.ANTIGRAVITY && clientTool === "github-copilot") { if (provider === FORMATS.ANTIGRAVITY && clientTool === "github-copilot") {
const { cloakedBody, toolNameMap } = AntigravityExecutor.cloakTools(result, clientTool); // No-op
result = cloakedBody;
if (toolNameMap?.size > 0) {
result._toolNameMap = toolNameMap;
}
} }
return result; return result;

View File

@@ -336,6 +336,20 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
} }
}; };
// Build tool_use id -> name map so functionResponse can use the correct name
const toolUseIdToName = {};
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
for (const msg of claudeRequest.messages) {
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_use" && block.id && block.name) {
toolUseIdToName[block.id] = block.name;
}
}
}
}
}
// Convert Claude messages to Gemini contents // Convert Claude messages to Gemini contents
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
for (const msg of claudeRequest.messages) { for (const msg of claudeRequest.messages) {
@@ -349,7 +363,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
parts.push({ parts.push({
functionCall: { functionCall: {
id: block.id, id: block.id,
name: block.name, name: sanitizeGeminiFunctionName(block.name),
args: block.input || {} args: block.input || {}
} }
}); });
@@ -358,10 +372,14 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
if (Array.isArray(content)) { if (Array.isArray(content)) {
content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n"); content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
} }
// Resolve the original tool name from the id — Gemini requires it to match the functionCall name
const resolvedName = toolUseIdToName[block.tool_use_id]
? sanitizeGeminiFunctionName(toolUseIdToName[block.tool_use_id])
: "tool";
parts.push({ parts.push({
functionResponse: { functionResponse: {
id: block.tool_use_id, id: block.tool_use_id,
name: "unknown", name: resolvedName,
response: { result: tryParseJSON(content) || content } response: { result: tryParseJSON(content) || content }
} }
}); });