Merge branch 'pr-779-review'
This commit is contained in:
commit
a3032f7a3e
6 changed files with 113 additions and 30 deletions
|
|
@ -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 {
|
||||||
|
|
@ -263,21 +303,34 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||||
* - Inject AG default decoy tools after client tools
|
* - Inject AG default decoy tools after client tools
|
||||||
* Returns { cloakedBody, toolNameMap } where toolNameMap maps suffixed → original
|
* Returns { cloakedBody, toolNameMap } where toolNameMap maps suffixed → original
|
||||||
*/
|
*/
|
||||||
static cloakTools(body) {
|
static cloakTools(body, clientTool = null) {
|
||||||
const tools = body.request?.tools;
|
const tools = body.request?.tools;
|
||||||
if (!tools || tools.length === 0) {
|
if (!tools || tools.length === 0) {
|
||||||
return { cloakedBody: body, toolNameMap: null };
|
return { cloakedBody: body, toolNameMap: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCopilot = clientTool === "github-copilot";
|
||||||
const toolNameMap = new Map();
|
const toolNameMap = new Map();
|
||||||
const clientDeclarations = [];
|
const clientDeclarations = [];
|
||||||
|
const decoyNames = new Set(AG_DECOY_TOOLS.map(tool => tool.name));
|
||||||
|
|
||||||
// First: collect renamed client tools
|
// First: collect renamed client tools
|
||||||
for (const toolGroup of tools) {
|
for (const toolGroup of tools) {
|
||||||
if (!toolGroup.functionDeclarations) continue;
|
if (!toolGroup.functionDeclarations) continue;
|
||||||
|
|
||||||
for (const func of toolGroup.functionDeclarations) {
|
for (const func of toolGroup.functionDeclarations) {
|
||||||
// Skip if already an AG default tool name
|
// For GitHub Copilot, avoid emitting duplicate native Antigravity tool names.
|
||||||
|
// Keep the decoys only once in the final declaration list.
|
||||||
|
if (isCopilot && AG_DEFAULT_TOOLS.has(func.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if already covered by decoys for Copilot
|
||||||
|
if (isCopilot && decoyNames.has(func.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve native AG names for non-Copilot clients
|
||||||
if (AG_DEFAULT_TOOLS.has(func.name)) {
|
if (AG_DEFAULT_TOOLS.has(func.name)) {
|
||||||
clientDeclarations.push(func);
|
clientDeclarations.push(func);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -290,7 +343,13 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client tools first, then AG decoy tools
|
// Client tools first, then AG decoy tools
|
||||||
const allDeclarations = [...clientDeclarations, ...AG_DECOY_TOOLS];
|
const allDeclarations = [];
|
||||||
|
const seenNames = new Set();
|
||||||
|
for (const decl of [...clientDeclarations, ...AG_DECOY_TOOLS]) {
|
||||||
|
if (!decl?.name || seenNames.has(decl.name)) continue;
|
||||||
|
seenNames.add(decl.name);
|
||||||
|
allDeclarations.push(decl);
|
||||||
|
}
|
||||||
|
|
||||||
// Rename tool names in conversation history (contents)
|
// Rename tool names in conversation history (contents)
|
||||||
const cloakedContents = body.request?.contents?.map(msg => {
|
const cloakedContents = body.request?.contents?.map(msg => {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
||||||
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
|
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
|
||||||
translatedBody = { ...body, model };
|
translatedBody = { ...body, model };
|
||||||
} else {
|
} else {
|
||||||
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, stripList, connectionId, rtkEnabled);
|
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, stripList, connectionId, rtkEnabled, clientTool);
|
||||||
if (!translatedBody) {
|
if (!translatedBody) {
|
||||||
trackPendingRequest(model, provider, connectionId, false, true);
|
trackPendingRequest(model, provider, connectionId, false, true);
|
||||||
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`);
|
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||||
// Claude rejects these in VALIDATED mode
|
// Claude rejects these in VALIDATED mode
|
||||||
"default", "examples",
|
"default", "examples",
|
||||||
// JSON Schema meta keywords
|
// JSON Schema meta keywords
|
||||||
"$schema", "$defs", "definitions", "const", "$ref",
|
"$schema", "$defs", "definitions", "const", "$ref", "$comment",
|
||||||
// Object validation keywords (not supported)
|
// Object validation keywords (not supported)
|
||||||
"additionalProperties", "propertyNames", "patternProperties",
|
"additionalProperties", "propertyNames", "patternProperties", "enumDescriptions",
|
||||||
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
|
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
|
||||||
"anyOf", "oneOf", "allOf", "not",
|
"anyOf", "oneOf", "allOf", "not",
|
||||||
// Dependency keywords (not supported)
|
// Dependency keywords (not supported)
|
||||||
|
|
@ -111,16 +111,18 @@ function removeUnsupportedKeywords(obj, keywords) {
|
||||||
for (const item of obj) {
|
for (const item of obj) {
|
||||||
removeUnsupportedKeywords(item, keywords);
|
removeUnsupportedKeywords(item, keywords);
|
||||||
}
|
}
|
||||||
} else {
|
return;
|
||||||
for (const key of Object.keys(obj)) {
|
}
|
||||||
if (keywords.includes(key) || key.startsWith("x-")) {
|
|
||||||
delete obj[key];
|
for (const key of Object.keys(obj)) {
|
||||||
}
|
if (keywords.includes(key) || key.startsWith("x-")) {
|
||||||
|
delete obj[key];
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
for (const value of Object.values(obj)) {
|
|
||||||
if (value && typeof value === "object") {
|
const value = obj[key];
|
||||||
removeUnsupportedKeywords(value, keywords);
|
if (value && typeof value === "object") {
|
||||||
}
|
removeUnsupportedKeywords(value, keywords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ function stripContentTypes(body, stripList = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate request: source -> openai -> target
|
// Translate request: source -> openai -> target
|
||||||
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null, rtkEnabled = false) {
|
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null, rtkEnabled = false, clientTool = null) {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
let result = body;
|
let result = body;
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
// Skip if client is native AG (userAgent = antigravity)
|
// Keep the translated request intact; final provider-specific sanitization happens
|
||||||
if (provider === FORMATS.ANTIGRAVITY && body.userAgent !== FORMATS.ANTIGRAVITY) {
|
// in the Antigravity executor.
|
||||||
const { cloakedBody, toolNameMap } = AntigravityExecutor.cloakTools(result);
|
if (provider === FORMATS.ANTIGRAVITY && clientTool === "github-copilot") {
|
||||||
result = cloakedBody;
|
// No-op
|
||||||
if (toolNameMap?.size > 0) {
|
|
||||||
result._toolNameMap = toolNameMap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,17 @@ const NATIVE_PAIRS = {
|
||||||
export function detectClientTool(headers = {}, body = {}) {
|
export function detectClientTool(headers = {}, body = {}) {
|
||||||
const ua = (headers["user-agent"] || "").toLowerCase();
|
const ua = (headers["user-agent"] || "").toLowerCase();
|
||||||
const xApp = (headers["x-app"] || "").toLowerCase();
|
const xApp = (headers["x-app"] || "").toLowerCase();
|
||||||
|
const openaiIntent = (headers["openai-intent"] || "").toLowerCase();
|
||||||
|
const initiator = (headers["x-initiator"] || headers["X-Initiator"] || "").toLowerCase();
|
||||||
|
|
||||||
// Antigravity: detected via body field (not header)
|
// Antigravity: detected via body field (not header)
|
||||||
if (body.userAgent === "antigravity") return "antigravity";
|
if (body.userAgent === "antigravity") return "antigravity";
|
||||||
|
|
||||||
|
// GitHub Copilot / OAI compatible extension using Copilot chat headers
|
||||||
|
if (ua.includes("githubcopilotchat") || openaiIntent === "conversation-panel" || initiator === "user") {
|
||||||
|
return "github-copilot";
|
||||||
|
}
|
||||||
|
|
||||||
// Claude Code / Claude CLI
|
// Claude Code / Claude CLI
|
||||||
if (ua.includes("claude-cli") || ua.includes("claude-code") || xApp === "cli") return "claude";
|
if (ua.includes("claude-cli") || ua.includes("claude-code") || xApp === "cli") return "claude";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue