fix(antigravity): align provider fingerprint with IDE Desktop 2.1.1 (#2389)

Match captured official Antigravity IDE traffic: cloudcode-pa host,
antigravity/ide/2.1.1 User-Agent, IDE-shaped agent requestId, and drop
router-only stream/usage headers plus the legacy double system prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
nguyenha935
2026-07-08 10:17:47 +07:00
committed by decolua
co-authored by Cursor
parent b10b807063
commit 71cd5b2f23
9 changed files with 147 additions and 62 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
import { platform, arch } from "os"; import { platform, arch } from "os";
import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js"; import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js";
import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js";
// === Gemini CLI === derive từ registry gemini-cli.transport // === Gemini CLI === derive từ registry gemini-cli.transport
export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion; export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion;
@@ -59,7 +60,7 @@ export function getPlatformEnum() {
} }
export function getPlatformUserAgent() { export function getPlatformUserAgent() {
return `antigravity/1.104.0 ${platform()}/${arch()}`; return ANTIGRAVITY_IDE_USER_AGENT;
} }
export const CLIENT_METADATA = { export const CLIENT_METADATA = {
@@ -129,7 +130,7 @@ export const AG_DEFAULT_TOOLS = new Set([
// Antigravity chat/stream headers // Antigravity chat/stream headers
export const ANTIGRAVITY_HEADERS = { export const ANTIGRAVITY_HEADERS = {
"User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` "User-Agent": ANTIGRAVITY_IDE_USER_AGENT
}; };
// Cloud Code Assist API // Cloud Code Assist API
+39 -20
View File
@@ -1,7 +1,7 @@
import crypto from "crypto"; import crypto from "crypto";
import { BaseExecutor } from "./base.js"; import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js"; import { PROVIDERS } from "../config/providers.js";
import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js"; import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { resolveSessionId } from "../utils/sessionManager.js"; import { resolveSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js";
@@ -18,7 +18,8 @@ function sanitizeFunctionName(name) {
const MAX_RETRY_AFTER_MS = 10000; const MAX_RETRY_AFTER_MS = 10000;
const ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS = 15000; const ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS = 15000;
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384; const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 64000;
const ANTIGRAVITY_IDE_REQUEST_ID_RE = /^agent\/[^/]+\/\d+\/[^/]+\/\d+$/;
const ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS = [ const ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS = [
/high\s+traffic/i, /high\s+traffic/i,
@@ -87,6 +88,27 @@ function parseImageConfig(model) {
return config; return config;
} }
function uuidFromSeed(seed) {
const bytes = crypto.createHash("sha256").update(String(seed || "antigravity")).digest().subarray(0, 16);
bytes[6] = (bytes[6] & 0x0f) | 0x50;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = bytes.toString("hex");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function buildIdeRequestId({ body, request, credentials, model, requestType }) {
if (ANTIGRAVITY_IDE_REQUEST_ID_RE.test(body?.requestId || "")) {
return body.requestId;
}
const sessionId = request?.sessionId || body?.request?.sessionId || credentials?._clientSessionId || credentials?.connectionId || credentials?.email || "anonymous";
const conversationId = uuidFromSeed(`antigravity:conversation:${sessionId}`);
const trajectoryId = uuidFromSeed(`antigravity:trajectory:${sessionId}:${model}:${requestType}`);
const contentCount = Array.isArray(request?.contents) ? request.contents.length : 1;
const step = Math.max(1, contentCount * 2 - 1);
return `agent/${conversationId}/${Date.now()}/${trajectoryId}/${step}`;
}
export class AntigravityExecutor extends BaseExecutor { export class AntigravityExecutor extends BaseExecutor {
constructor() { constructor() {
super("antigravity", PROVIDERS.antigravity); super("antigravity", PROVIDERS.antigravity);
@@ -104,14 +126,10 @@ export class AntigravityExecutor extends BaseExecutor {
// sessionId comes from transformRequest output; base.execute runs transformRequest before // sessionId comes from transformRequest output; base.execute runs transformRequest before
// buildHeaders, so we read it from instance state cached there (fallback: explicit arg). // buildHeaders, so we read it from instance state cached there (fallback: explicit arg).
buildHeaders(credentials, stream = true, sessionId = null) { buildHeaders(credentials, stream = true, sessionId = null) {
const sid = sessionId || this._lastSessionId;
return { return {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${credentials.accessToken}`, "Authorization": `Bearer ${credentials.accessToken}`,
"User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"], "User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"],
[INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value,
...(sid && { "X-Machine-Session-Id": sid }),
"Accept": stream ? "text/event-stream" : "application/json"
}; };
} }
@@ -142,25 +160,26 @@ export class AntigravityExecutor extends BaseExecutor {
}); });
this._lastSessionId = sessionId; this._lastSessionId = sessionId;
const request = {
contents,
generationConfig: {
temperature: 1.0,
topP: 0.95,
topK: 40,
maxOutputTokens: 8192,
imageConfig,
},
sessionId,
// No tools, no systemInstruction, no safetySettings for image gen
};
return { return {
project: projectId, project: projectId,
model: cleanModel, model: cleanModel,
userAgent: "antigravity", userAgent: "antigravity",
requestType: "image_gen", requestType: "image_gen",
requestId: `agent-${crypto.randomUUID()}`, requestId: buildIdeRequestId({ body, request, credentials, model: cleanModel, requestType: "image_gen" }),
request: { request,
contents,
generationConfig: {
temperature: 1.0,
topP: 0.95,
topK: 40,
maxOutputTokens: 8192,
imageConfig,
},
sessionId,
// No tools, no systemInstruction, no safetySettings for image gen
},
}; };
} }
@@ -248,7 +267,7 @@ export class AntigravityExecutor extends BaseExecutor {
model: model, model: model,
userAgent: "antigravity", userAgent: "antigravity",
requestType: "agent", requestType: "agent",
requestId: `agent-${crypto.randomUUID()}`, requestId: buildIdeRequestId({ body, request: transformedRequest, credentials, model, requestType: "agent" }),
request: transformedRequest request: transformedRequest
}; };
} }
+3 -7
View File
@@ -1,5 +1,4 @@
import { platform, arch } from "os"; import { ANTIGRAVITY_IDE_BASE_URL, ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js";
import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js";
export default { export default {
id: "antigravity", id: "antigravity",
@@ -20,13 +19,10 @@ export default {
category: "oauth", category: "oauth",
serviceKinds: ["llm", "image"], serviceKinds: ["llm", "image"],
transport: { transport: {
baseUrls: [ baseUrls: [ANTIGRAVITY_IDE_BASE_URL],
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
],
format: "antigravity", format: "antigravity",
headers: { headers: {
"User-Agent": "antigravity/1.107.0 darwin/arm64", "User-Agent": ANTIGRAVITY_IDE_USER_AGENT,
}, },
retry: { retry: {
"429": { "429": {
+7
View File
@@ -54,6 +54,13 @@ export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages";
export const OPENAI_COMPAT_BASE = "https://api.openai.com/v1"; export const OPENAI_COMPAT_BASE = "https://api.openai.com/v1";
export const ANTHROPIC_COMPAT_BASE = "https://api.anthropic.com/v1"; export const ANTHROPIC_COMPAT_BASE = "https://api.anthropic.com/v1";
// Official Antigravity IDE Desktop 2.1.1 fingerprint captured from macOS arm64.
// Keep this static even when 9router runs on Linux: the provider profile is
// intentionally matching the IDE client, not the server host.
export const ANTIGRAVITY_IDE_VERSION = "2.1.1";
export const ANTIGRAVITY_IDE_BASE_URL = "https://cloudcode-pa.googleapis.com";
export const ANTIGRAVITY_IDE_USER_AGENT = `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} darwin/arm64`;
// Antigravity OAuth client credentials (public CLI client — duplicated in usage.js + src/lib/oauth) // Antigravity OAuth client credentials (public CLI client — duplicated in usage.js + src/lib/oauth)
export const ANTIGRAVITY_OAUTH_CLIENT = { export const ANTIGRAVITY_OAUTH_CLIENT = {
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
+4 -6
View File
@@ -2,15 +2,15 @@
* Google usage handlers (Gemini CLI + Antigravity) * Google usage handlers (Gemini CLI + Antigravity)
*/ */
import { CLIENT_METADATA, getPlatformUserAgent } from "../../config/appConstants.js"; import { CLIENT_METADATA } from "../../config/appConstants.js";
import { ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; import { ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_IDE_VERSION, ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js";
import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js"; import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js";
// Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here // Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here
const ANTIGRAVITY_CONFIG = { const ANTIGRAVITY_CONFIG = {
...U("antigravity"), ...U("antigravity"),
...ANTIGRAVITY_OAUTH_CLIENT, ...ANTIGRAVITY_OAUTH_CLIENT,
userAgent: getPlatformUserAgent(), userAgent: ANTIGRAVITY_IDE_USER_AGENT,
}; };
/** /**
@@ -129,8 +129,7 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
"User-Agent": ANTIGRAVITY_CONFIG.userAgent, "User-Agent": ANTIGRAVITY_CONFIG.userAgent,
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Name": "antigravity", "X-Client-Name": "antigravity",
"X-Client-Version": "1.107.0", "X-Client-Version": ANTIGRAVITY_IDE_VERSION,
"x-request-source": "local", // MITM bypass
}, },
body: JSON.stringify({ body: JSON.stringify({
...(projectId ? { project: projectId } : {}) ...(projectId ? { project: projectId } : {})
@@ -229,7 +228,6 @@ async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null)
"Authorization": `Bearer ${accessToken}`, "Authorization": `Bearer ${accessToken}`,
"User-Agent": ANTIGRAVITY_CONFIG.userAgent, "User-Agent": ANTIGRAVITY_CONFIG.userAgent,
"Content-Type": "application/json", "Content-Type": "application/json",
"x-request-source": "local", // MITM bypass
}, },
body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }),
}, 10000, proxyOptions); }, 10000, proxyOptions);
@@ -1,7 +1,6 @@
import { register } from "../index.js"; import { register } from "../index.js";
import { FORMATS } from "../formats.js"; import { FORMATS } from "../formats.js";
import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js"; import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
function generateUUID() { function generateUUID() {
return crypto.randomUUID(); return crypto.randomUUID();
@@ -283,19 +282,6 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
if (isAntigravity) { if (isAntigravity) {
envelope.requestType = "agent"; envelope.requestType = "agent";
// Inject required default system prompt for Antigravity
// Inject required default system prompt for Antigravity (double injection)
const systemParts = [
{ text: ANTIGRAVITY_DEFAULT_SYSTEM },
{ text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` }
];
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
}
// Add toolConfig for Antigravity // Add toolConfig for Antigravity
if (geminiCLI.tools?.length > 0) { if (geminiCLI.tools?.length > 0) {
envelope.request.toolConfig = { envelope.request.toolConfig = {
@@ -414,12 +400,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
} }
} }
// Add system instruction (Antigravity default - double injection + user system prompt) const systemParts = [];
const systemParts = [
{ text: ANTIGRAVITY_DEFAULT_SYSTEM },
{ text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` }
];
// Merge user system prompt from claudeRequest // Merge user system prompt from claudeRequest
if (claudeRequest.system) { if (claudeRequest.system) {
if (Array.isArray(claudeRequest.system)) { if (Array.isArray(claudeRequest.system)) {
@@ -431,10 +412,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
} }
} }
// Merge existing systemInstruction parts (from contents conversion) if (systemParts.length > 0) {
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts }; envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
} }
@@ -463,4 +441,3 @@ export function openaiToAntigravityRequest(model, body, stream, credentials = nu
register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGeminiRequest, null); register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGeminiRequest, null);
register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null); register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null);
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null); register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null);
+30
View File
@@ -4,6 +4,8 @@ import "./registerAll.js";
import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js"; import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js"; import { FORMATS } from "../../open-sse/translator/formats.js";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js"; import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js";
import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.js";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../open-sse/config/appConstants.js";
const AG2O = (req) => const AG2O = (req) =>
translateRequest(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, "m", { request: req }, true, null, null); translateRequest(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, "m", { request: req }, true, null, null);
@@ -106,4 +108,32 @@ describe("Antigravity executor", () => {
const query = out.request.tools[0].functionDeclarations[0].parameters.properties.query; const query = out.request.tools[0].functionDeclarations[0].parameters.properties.query;
expect(query).toEqual({ type: "string", description: "Search query" }); expect(query).toEqual({ type: "string", description: "Search query" });
}); });
it("does not inject the legacy Antigravity default system prompt for Gemini-backed models", () => {
const out = openaiToAntigravityRequest("gemini-3.5-flash-low", {
messages: [
{ role: "system", content: "USER_SYSTEM_PROMPT" },
{ role: "user", content: "hello" },
],
}, true, { projectId: "project-1", connectionId: "conn-1" });
const system = JSON.stringify(out.request.systemInstruction);
expect(system).toContain("USER_SYSTEM_PROMPT");
expect(system).not.toContain(ANTIGRAVITY_DEFAULT_SYSTEM);
expect(system).not.toContain("Please ignore the following [ignore]");
});
it("does not inject the legacy Antigravity default system prompt for Claude-backed models", () => {
const out = openaiToAntigravityRequest("claude-opus-4-6-thinking", {
messages: [
{ role: "system", content: "USER_SYSTEM_PROMPT" },
{ role: "user", content: "hello" },
],
}, true, { projectId: "project-1", connectionId: "conn-1" });
const system = JSON.stringify(out.request.systemInstruction);
expect(system).toContain("USER_SYSTEM_PROMPT");
expect(system).not.toContain(ANTIGRAVITY_DEFAULT_SYSTEM);
expect(system).not.toContain("Please ignore the following [ignore]");
});
}); });
+29 -2
View File
@@ -1,6 +1,7 @@
// Guards D3: antigravity 429/503 retry merged into base via computeRetryDelay hook. // Guards D3: antigravity 429/503 retry merged into base via computeRetryDelay hook.
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js"; import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js";
import antigravity from "../../open-sse/providers/registry/antigravity.js";
const MAX = 10000; const MAX = 10000;
function res(status, headers = {}, body = null) { function res(status, headers = {}, body = null) {
@@ -66,9 +67,35 @@ describe("antigravity computeRetryDelay hook (D3)", () => {
expect(out.request.tools[0].functionDeclarations.map(fn => fn.name)).toEqual(["read_file"]); expect(out.request.tools[0].functionDeclarations.map(fn => fn.name)).toEqual(["read_file"]);
}); });
it("buildHeaders includes cached session id after transformRequest", () => { it("registry uses the official IDE cloudcode host and user agent", () => {
expect(antigravity.transport.baseUrls).toEqual(["https://cloudcode-pa.googleapis.com"]);
expect(antigravity.transport.headers["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
});
it("buildHeaders matches official IDE stream headers", () => {
ag._lastSessionId = "sess-123"; ag._lastSessionId = "sess-123";
const h = ag.buildHeaders({ accessToken: "tok" }, true); const h = ag.buildHeaders({ accessToken: "tok" }, true);
expect(h["X-Machine-Session-Id"]).toBe("sess-123"); expect(h["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
expect(h["Content-Type"]).toBe("application/json");
expect(h["Authorization"]).toBe("Bearer tok");
expect(h).not.toHaveProperty("X-Machine-Session-Id");
expect(h).not.toHaveProperty("x-request-source");
expect(h).not.toHaveProperty("Accept");
});
it("transforms chat requests with official IDE requestId shape and 64000 token cap", () => {
const out = ag.transformRequest("claude-opus-4-6-thinking", {
request: {
contents: [
{ role: "user", parts: [{ text: "hi" }] },
{ role: "model", parts: [{ text: "hello" }] },
],
generationConfig: { maxOutputTokens: 90000 },
sessionId: "-3750763034362895579",
},
}, true, { projectId: "project-1", connectionId: "conn-1" });
expect(out.requestId).toMatch(/^agent\/[0-9a-f-]{36}\/\d{13}\/[0-9a-f-]{36}\/\d+$/);
expect(out.request.generationConfig.maxOutputTokens).toBe(64000);
}); });
}); });
@@ -0,0 +1,30 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
: { models: {} },
text: async () => "{}",
}));
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch,
}));
describe("Antigravity usage headers", () => {
beforeEach(() => proxyAwareFetch.mockClear());
it("uses the official IDE user agent and omits router-only source headers", async () => {
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
await getAntigravityUsage("access-token", {});
expect(proxyAwareFetch).toHaveBeenCalledTimes(2);
for (const [, options] of proxyAwareFetch.mock.calls) {
expect(options.headers["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
expect(options.headers).not.toHaveProperty("x-request-source");
}
});
});