diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index d3f41784..0fcbab81 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -5,8 +5,18 @@ import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAU import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { deriveSessionId } from "../utils/sessionManager.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_ANTIGRAVITY_OUTPUT_TOKENS = 16384; export class AntigravityExecutor extends BaseExecutor { constructor() { @@ -53,14 +63,44 @@ export class AntigravityExecutor extends BaseExecutor { 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 = { - ...body.request, + ...requestWithoutTools, + generationConfig, ...(contents && { contents }), + ...(tools && { tools }), sessionId: body.request?.sessionId || deriveSessionId(credentials?.email || credentials?.connectionId), safetySettings: undefined, - toolConfig: body.request?.tools?.length > 0 - ? { functionCallingConfig: { mode: "VALIDATED" } } - : body.request?.toolConfig + ...(tools?.length > 0 && { toolConfig: { functionCallingConfig: { mode: "VALIDATED" } } }) }; return { diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index 442693a3..a018dc59 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -140,14 +140,11 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream } } - // Antigravity cloaking: rename client tools + inject decoys (anti-ban) - // Only apply for GitHub Copilot requests so other clients are unaffected. + // Antigravity cloaking/tool stripping is intentionally disabled for GitHub Copilot. + // Keep the translated request intact; final provider-specific sanitization happens + // in the Antigravity executor. if (provider === FORMATS.ANTIGRAVITY && clientTool === "github-copilot") { - const { cloakedBody, toolNameMap } = AntigravityExecutor.cloakTools(result, clientTool); - result = cloakedBody; - if (toolNameMap?.size > 0) { - result._toolNameMap = toolNameMap; - } + // No-op } return result; diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index 27f96dd1..b2043065 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -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 if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { for (const msg of claudeRequest.messages) { @@ -349,7 +363,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu parts.push({ functionCall: { id: block.id, - name: block.name, + name: sanitizeGeminiFunctionName(block.name), args: block.input || {} } }); @@ -358,10 +372,14 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu if (Array.isArray(content)) { 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({ functionResponse: { id: block.tool_use_id, - name: "unknown", + name: resolvedName, response: { result: tryParseJSON(content) || content } } });