diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index 6ac9d324..cc46697c 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -1,5 +1,6 @@ import { platform, arch } from "os"; import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js"; +import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js"; // === Gemini CLI === derive từ registry gemini-cli.transport export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion; @@ -59,7 +60,7 @@ export function getPlatformEnum() { } export function getPlatformUserAgent() { - return `antigravity/1.104.0 ${platform()}/${arch()}`; + return ANTIGRAVITY_IDE_USER_AGENT; } export const CLIENT_METADATA = { @@ -129,7 +130,7 @@ export const AG_DEFAULT_TOOLS = new Set([ // Antigravity chat/stream headers export const ANTIGRAVITY_HEADERS = { - "User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` + "User-Agent": ANTIGRAVITY_IDE_USER_AGENT }; // Cloud Code Assist API diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 5249a3ec..f669e3a7 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -1,7 +1,7 @@ import crypto from "crypto"; import { BaseExecutor } from "./base.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 { resolveSessionId } from "../utils/sessionManager.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; @@ -18,7 +18,8 @@ function sanitizeFunctionName(name) { const MAX_RETRY_AFTER_MS = 10000; 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 = [ /high\s+traffic/i, @@ -87,6 +88,27 @@ function parseImageConfig(model) { 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 { constructor() { super("antigravity", PROVIDERS.antigravity); @@ -104,14 +126,10 @@ export class AntigravityExecutor extends BaseExecutor { // sessionId comes from transformRequest output; base.execute runs transformRequest before // buildHeaders, so we read it from instance state cached there (fallback: explicit arg). buildHeaders(credentials, stream = true, sessionId = null) { - const sid = sessionId || this._lastSessionId; return { "Content-Type": "application/json", "Authorization": `Bearer ${credentials.accessToken}`, "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; + 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 { project: projectId, model: cleanModel, userAgent: "antigravity", requestType: "image_gen", - requestId: `agent-${crypto.randomUUID()}`, - request: { - contents, - generationConfig: { - temperature: 1.0, - topP: 0.95, - topK: 40, - maxOutputTokens: 8192, - imageConfig, - }, - sessionId, - // No tools, no systemInstruction, no safetySettings for image gen - }, + requestId: buildIdeRequestId({ body, request, credentials, model: cleanModel, requestType: "image_gen" }), + request, }; } @@ -248,7 +267,7 @@ export class AntigravityExecutor extends BaseExecutor { model: model, userAgent: "antigravity", requestType: "agent", - requestId: `agent-${crypto.randomUUID()}`, + requestId: buildIdeRequestId({ body, request: transformedRequest, credentials, model, requestType: "agent" }), request: transformedRequest }; } diff --git a/open-sse/providers/registry/antigravity.js b/open-sse/providers/registry/antigravity.js index 29003527..dd7fbc02 100644 --- a/open-sse/providers/registry/antigravity.js +++ b/open-sse/providers/registry/antigravity.js @@ -1,5 +1,4 @@ -import { platform, arch } from "os"; -import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js"; +import { ANTIGRAVITY_IDE_BASE_URL, ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js"; export default { id: "antigravity", @@ -20,13 +19,10 @@ export default { category: "oauth", serviceKinds: ["llm", "image"], transport: { - baseUrls: [ - "https://daily-cloudcode-pa.googleapis.com", - "https://daily-cloudcode-pa.sandbox.googleapis.com", - ], + baseUrls: [ANTIGRAVITY_IDE_BASE_URL], format: "antigravity", headers: { - "User-Agent": "antigravity/1.107.0 darwin/arm64", + "User-Agent": ANTIGRAVITY_IDE_USER_AGENT, }, retry: { "429": { diff --git a/open-sse/providers/shared.js b/open-sse/providers/shared.js index 32388584..6f6a2c20 100644 --- a/open-sse/providers/shared.js +++ b/open-sse/providers/shared.js @@ -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 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) export const ANTIGRAVITY_OAUTH_CLIENT = { clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", diff --git a/open-sse/services/usage/google.js b/open-sse/services/usage/google.js index 71c53e89..f0e4d23e 100644 --- a/open-sse/services/usage/google.js +++ b/open-sse/services/usage/google.js @@ -2,15 +2,15 @@ * Google usage handlers (Gemini CLI + Antigravity) */ -import { CLIENT_METADATA, getPlatformUserAgent } from "../../config/appConstants.js"; -import { ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; +import { CLIENT_METADATA } from "../../config/appConstants.js"; +import { ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_IDE_VERSION, ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js"; // Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here const ANTIGRAVITY_CONFIG = { ...U("antigravity"), ...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, "Content-Type": "application/json", "X-Client-Name": "antigravity", - "X-Client-Version": "1.107.0", - "x-request-source": "local", // MITM bypass + "X-Client-Version": ANTIGRAVITY_IDE_VERSION, }, body: JSON.stringify({ ...(projectId ? { project: projectId } : {}) @@ -229,7 +228,6 @@ async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null) "Authorization": `Bearer ${accessToken}`, "User-Agent": ANTIGRAVITY_CONFIG.userAgent, "Content-Type": "application/json", - "x-request-source": "local", // MITM bypass }, body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), }, 10000, proxyOptions); diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index afb3effc..9423b293 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -1,7 +1,6 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.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"; function generateUUID() { return crypto.randomUUID(); @@ -283,19 +282,6 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra if (isAntigravity) { 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 if (geminiCLI.tools?.length > 0) { 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 = [ - { text: ANTIGRAVITY_DEFAULT_SYSTEM }, - { text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` } - ]; - + const systemParts = []; // Merge user system prompt from claudeRequest if (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 (envelope.request.systemInstruction?.parts) { - envelope.request.systemInstruction.parts.unshift(...systemParts); - } else { + if (systemParts.length > 0) { 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_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null); register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null); - diff --git a/tests/translator/bugs-antigravity.test.js b/tests/translator/bugs-antigravity.test.js index 47c36942..6ab37efe 100644 --- a/tests/translator/bugs-antigravity.test.js +++ b/tests/translator/bugs-antigravity.test.js @@ -4,6 +4,8 @@ import "./registerAll.js"; import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js"; import { FORMATS } from "../../open-sse/translator/formats.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) => 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; 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]"); + }); }); diff --git a/tests/unit/antigravity-retry-hook.test.js b/tests/unit/antigravity-retry-hook.test.js index 1602c736..989dd88f 100644 --- a/tests/unit/antigravity-retry-hook.test.js +++ b/tests/unit/antigravity-retry-hook.test.js @@ -1,6 +1,7 @@ // Guards D3: antigravity 429/503 retry merged into base via computeRetryDelay hook. import { describe, it, expect } from "vitest"; import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js"; +import antigravity from "../../open-sse/providers/registry/antigravity.js"; const MAX = 10000; 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"]); }); - 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"; 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); }); }); diff --git a/tests/unit/antigravity-usage-headers.test.js b/tests/unit/antigravity-usage-headers.test.js new file mode 100644 index 00000000..96087552 --- /dev/null +++ b/tests/unit/antigravity-usage-headers.test.js @@ -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"); + } + }); +});