From d8e9c16d83a53d6ce8f58a715289d26ab454ae2e Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 13 Apr 2026 11:05:18 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Antigravity/Gemini=20parity=20?= =?UTF-8?q?=E2=80=94=20header=20scrubbing,=20429=20engine,=20credits=20ret?= =?UTF-8?q?ry,=20dynamic=20UA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the Antigravity executor to parity with CLIProxyAPI and ZeroGravity for Gemini/Google traffic handling. New modules: - antigravityHeaderScrub.ts: Removes 28 proxy/fingerprint/Chromium headers that reveal non-native traffic. Sets Accept-Encoding to 'gzip, deflate, br' (Node.js default). - antigravity429Engine.ts: 4-tier 429 classification (unknown, rate_limited, quota_exhausted, soft_rate_limit) with nuanced retry decisions (soft_retry, instant_retry, short_cooldown, full_quota_exhausted). Per-auth credits failure tracking with auto-disable after 3 failures (5h cooldown). - antigravityCredits.ts: Google One AI credits injection — retries quota_exhausted 429s with enabledCreditTypes: ['GOOGLE_ONE_AI']. Enabled via ANTIGRAVITY_CREDITS=1 env var. - antigravityHeaders.ts: Dynamic User-Agent from OS/arch (antigravity/1.21.9 darwin/arm64), Gemini CLI UA per-model (GeminiCLI/0.31.0/MODEL (OS; ARCH)), X-Goog-Api-Client header (google-genai-sdk/1.41.0 gl-node/v22.19.0). - antigravityObfuscation.ts: Sensitive word obfuscation using zero-width joiners for 17 client names (matching ZeroGravity). Updated antigravity.ts: - Dynamic UA replaces hardcoded 'antigravity/1.104.0 darwin/arm64' - X-Goog-Api-Client header added (was missing entirely) - Header scrubbing on all outbound requests - 4-tier 429 engine replaces 2-category classification - Google One AI credits retry on quota_exhausted - Sensitive word obfuscation in user message content --- open-sse/executors/antigravity.ts | 79 ++++++--- open-sse/services/antigravity429Engine.ts | 178 ++++++++++++++++++++ open-sse/services/antigravityCredits.ts | 47 ++++++ open-sse/services/antigravityHeaderScrub.ts | 62 +++++++ open-sse/services/antigravityHeaders.ts | 83 +++++++++ open-sse/services/antigravityObfuscation.ts | 37 ++++ 6 files changed, 467 insertions(+), 19 deletions(-) create mode 100644 open-sse/services/antigravity429Engine.ts create mode 100644 open-sse/services/antigravityCredits.ts create mode 100644 open-sse/services/antigravityHeaderScrub.ts create mode 100644 open-sse/services/antigravityHeaders.ts create mode 100644 open-sse/services/antigravityObfuscation.ts diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index e88599a3a6..3b72817f7e 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -1,6 +1,11 @@ import crypto, { randomUUID } from "crypto"; import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts"; +import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts"; +import { antigravityUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts"; +import { classify429, decide429, type Decision } from "../services/antigravity429Engine.ts"; +import { injectCreditsField, shouldRetryWithCredits, handleCreditsFailure } from "../services/antigravityCredits.ts"; +import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts"; const MAX_RETRY_AFTER_MS = 60_000; const LONG_RETRY_THRESHOLD_MS = 60_000; @@ -39,13 +44,15 @@ export class AntigravityExecutor extends BaseExecutor { } buildHeaders(credentials, stream = true) { - return { + const raw = { "Content-Type": "application/json", Authorization: `Bearer ${credentials.accessToken}`, - "User-Agent": this.config.headers?.["User-Agent"] || "antigravity/1.104.0 darwin/arm64", - "X-OmniRoute-Source": "omniroute", + "User-Agent": antigravityUserAgent(), + "X-Goog-Api-Client": googApiClientHeader(), Accept: "text/event-stream", }; + // Scrub proxy/fingerprint headers that reveal non-native traffic + return scrubProxyAndFingerprintHeaders(raw); } transformRequest(model, body, stream, credentials) { @@ -119,6 +126,20 @@ export class AntigravityExecutor extends BaseExecutor { const upstreamModel = cleanModelName(model); + // Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor") + const requestContents = transformedRequest.contents; + if (Array.isArray(requestContents)) { + for (const msg of requestContents) { + if (Array.isArray(msg.parts)) { + for (const part of msg.parts) { + if (typeof part.text === "string") { + part.text = obfuscateSensitiveWords(part.text); + } + } + } + } + } + return { ...body, project: projectId, @@ -372,25 +393,45 @@ export class AntigravityExecutor extends BaseExecutor { retryMs = this.parseRetryFromErrorMessage(errorMessage); if (!retryMs) { - // Dynamic quota interpretation logic for Free vs Pro accounts - const lowerMsg = errorMessage.toLowerCase(); + // 4-tier 429 classification engine (matching CLIProxyAPI) + const category = classify429(errorMessage); + const decision: Decision = decide429(category, retryMs); + retryMs = decision.retryAfterMs; + log?.debug?.( + "AG_429", + `Category: ${category}, Decision: ${decision.kind} — ${decision.reason}` + ); + // For quota_exhausted, attempt Google One AI credits retry if ( - lowerMsg.includes("free tier") || - lowerMsg.includes("exhausted your capacity") || - lowerMsg.includes("daily limit") || - lowerMsg.includes("quota exceeded") + category === "quota_exhausted" && + shouldRetryWithCredits( + credentials?.accessToken || "", + process.env.ANTIGRAVITY_CREDITS === "1" || process.env.ANTIGRAVITY_CREDITS === "true" + ) ) { - // Hard limit hit for Free accounts (or exhausting general capacity), fallback immediately. - // Setting a massive retryMs forces an instant fallback. - retryMs = 24 * 60 * 60 * 1000; // 24 hours - } else if ( - lowerMsg.includes("pro") || - lowerMsg.includes("per minute") || - lowerMsg.includes("rpm") - ) { - // RPM limit for Pro counts, backoff up to 1 minute, then fallback - retryMs = 60 * 1000; // 60s + log?.info?.("AG_CREDITS", "Retrying with Google One AI credits"); + const creditsBody = injectCreditsField(transformedBody); + try { + const creditsResp = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(creditsBody), + signal, + }); + if (creditsResp.ok || creditsResp.status !== HTTP_STATUS.RATE_LIMITED) { + log?.info?.("AG_CREDITS", `Credits retry succeeded: ${creditsResp.status}`); + if (!stream) { + return this.collectStreamToResponse(creditsResp, model, url, headers, creditsBody, log, signal); + } + return { response: creditsResp, url, headers, transformedBody: creditsBody }; + } + handleCreditsFailure(credentials?.accessToken || ""); + log?.warn?.("AG_CREDITS", "Credits retry also 429'd"); + } catch (creditsErr) { + handleCreditsFailure(credentials?.accessToken || ""); + log?.warn?.("AG_CREDITS", `Credits retry failed: ${creditsErr}`); + } } } } catch (e) { diff --git a/open-sse/services/antigravity429Engine.ts b/open-sse/services/antigravity429Engine.ts new file mode 100644 index 0000000000..2442007baa --- /dev/null +++ b/open-sse/services/antigravity429Engine.ts @@ -0,0 +1,178 @@ +/** + * Antigravity 429 classification and retry decision engine. + * + * CLIProxyAPI classifies 429 responses into 4 categories and makes nuanced + * retry decisions for each. OmniRoute previously had only 2 categories + * (free tier vs RPM). This module brings full parity. + * + * Categories: + * - unknown: Generic 429, exponential backoff + * - rate_limited: Per-minute rate limit, short backoff + same auth retry + * - quota_exhausted: Daily/plan quota gone, switch auth or long cooldown + * - soft_rate_limit: Temporary burst limit, instant retry + * + * Decisions: + * - soft_retry: Wait briefly, retry same auth + * - instant_retry_same_auth: Retry immediately on same auth + * - short_cooldown_switch_auth: 5min cooldown, try next account + * - full_quota_exhausted: 24h cooldown, skip this account + */ + +export type Category = + | "unknown" + | "rate_limited" + | "quota_exhausted" + | "soft_rate_limit"; + +export type DecisionKind = + | "soft_retry" + | "instant_retry_same_auth" + | "short_cooldown_switch_auth" + | "full_quota_exhausted"; + +export interface Decision { + kind: DecisionKind; + retryAfterMs: number | null; + reason: string; +} + +const QUOTA_EXHAUSTED_KEYWORDS = [ + "quota_exhausted", + "quota exhausted", +]; + +const CREDITS_EXHAUSTED_KEYWORDS = [ + "google_one_ai", + "insufficient credit", + "insufficient credits", + "not enough credit", + "not enough credits", + "credit exhausted", + "credits exhausted", + "credit balance", + "minimumcreditamountforusage", + "minimum credit amount for usage", + "minimum credit", + "resource has been exhausted", +]; + +const SHORT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes +const INSTANT_RETRY_THRESHOLD_MS = 3 * 1000; // 3 seconds +const FULL_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours + +export function classify429(errorMessage: string): Category { + const lower = (errorMessage || "").toLowerCase(); + + // Check for quota exhaustion first (most specific) + for (const kw of QUOTA_EXHAUSTED_KEYWORDS) { + if (lower.includes(kw)) return "quota_exhausted"; + } + + // Check for credits exhaustion (also quota-related) + for (const kw of CREDITS_EXHAUSTED_KEYWORDS) { + if (lower.includes(kw)) return "quota_exhausted"; + } + + // Check for RPM/rate limit indicators + if ( + lower.includes("per minute") || + lower.includes("rpm") || + lower.includes("rate limit") || + lower.includes("rate_limit") || + lower.includes("too many requests") + ) { + return "rate_limited"; + } + + // Check for free tier exhaustion + if ( + lower.includes("free tier") || + lower.includes("daily limit") || + lower.includes("exhausted your capacity") + ) { + return "quota_exhausted"; + } + + // Check for soft/burst limits + if (lower.includes("try again") || lower.includes("temporarily")) { + return "soft_rate_limit"; + } + + return "unknown"; +} + +export function decide429( + category: Category, + retryAfterMs: number | null +): Decision { + switch (category) { + case "soft_rate_limit": + return { + kind: retryAfterMs && retryAfterMs <= INSTANT_RETRY_THRESHOLD_MS + ? "instant_retry_same_auth" + : "soft_retry", + retryAfterMs: retryAfterMs ?? 2000, + reason: "Soft rate limit — brief backoff", + }; + + case "rate_limited": + return { + kind: retryAfterMs && retryAfterMs <= SHORT_COOLDOWN_MS + ? "soft_retry" + : "short_cooldown_switch_auth", + retryAfterMs: retryAfterMs ?? 60_000, + reason: "RPM rate limit — switch auth if cooldown is long", + }; + + case "quota_exhausted": + return { + kind: "full_quota_exhausted", + retryAfterMs: retryAfterMs ?? FULL_QUOTA_COOLDOWN_MS, + reason: "Quota exhausted — skip this account", + }; + + default: + return { + kind: "soft_retry", + retryAfterMs: retryAfterMs ?? 5000, + reason: "Unknown 429 — generic backoff", + }; + } +} + +/** + * Track credits failure state per auth key. + * Auto-disables after repeated failures with 5h cooldown. + */ +const creditsFailureMap = new Map(); + +const CREDITS_DISABLE_THRESHOLD = 3; +const CREDITS_COOLDOWN_MS = 5 * 60 * 60 * 1000; // 5 hours + +export function recordCreditsFailure(authKey: string): boolean { + const state = creditsFailureMap.get(authKey) ?? { count: 0, disabledUntil: 0 }; + state.count++; + + if (state.count >= CREDITS_DISABLE_THRESHOLD) { + state.disabledUntil = Date.now() + CREDITS_COOLDOWN_MS; + creditsFailureMap.set(authKey, state); + return true; // disabled + } + + creditsFailureMap.set(authKey, state); + return false; +} + +export function isCreditsDisabled(authKey: string): boolean { + const state = creditsFailureMap.get(authKey); + if (!state) return false; + if (state.disabledUntil > Date.now()) return true; + // Cooldown expired, reset + creditsFailureMap.delete(authKey); + return false; +} + +export { SHORT_COOLDOWN_MS, FULL_QUOTA_COOLDOWN_MS }; diff --git a/open-sse/services/antigravityCredits.ts b/open-sse/services/antigravityCredits.ts new file mode 100644 index 0000000000..671f6f8f55 --- /dev/null +++ b/open-sse/services/antigravityCredits.ts @@ -0,0 +1,47 @@ +/** + * Google One AI credits injection for Antigravity. + * + * When Antigravity returns a quota_exhausted 429, CLIProxyAPI retries the + * request with `enabledCreditTypes: ["GOOGLE_ONE_AI"]` injected into the + * body. This uses the user's Google One AI credit balance for the retry, + * which is often available on Pro accounts. + * + * Based on CLIProxyAPI's antigravity_executor.go line 268. + */ + +import { isCreditsDisabled, recordCreditsFailure } from "./antigravity429Engine.ts"; + +/** + * Inject enabledCreditTypes into the request body for a credits retry. + * Returns a new body object with the field added. + */ +export function injectCreditsField( + body: Record +): Record { + return { + ...body, + enabledCreditTypes: ["GOOGLE_ONE_AI"], + }; +} + +/** + * Determine if a credits retry should be attempted for this auth key. + * Returns false if credits are disabled (too many failures) or if the + * config flag is off. + */ +export function shouldRetryWithCredits( + authKey: string, + creditsEnabled: boolean +): boolean { + if (!creditsEnabled) return false; + if (isCreditsDisabled(authKey)) return false; + return true; +} + +/** + * Handle a credits retry failure. Tracks the failure and returns + * true if credits are now disabled for this auth key. + */ +export function handleCreditsFailure(authKey: string): boolean { + return recordCreditsFailure(authKey); +} diff --git a/open-sse/services/antigravityHeaderScrub.ts b/open-sse/services/antigravityHeaderScrub.ts new file mode 100644 index 0000000000..5ecdd91627 --- /dev/null +++ b/open-sse/services/antigravityHeaderScrub.ts @@ -0,0 +1,62 @@ +/** + * Antigravity header scrubbing. + * + * Real Antigravity is a Node.js app. Its outbound HTTP requests never include + * proxy tracing headers, Stainless SDK headers, or Chromium Sec-Ch-* headers. + * Sending any of these reveals the request came through a third-party proxy. + * + * Based on CLIProxyAPI's ScrubProxyAndFingerprintHeaders (misc/header_utils.go). + */ + +const HEADERS_TO_REMOVE = [ + // Proxy tracing + "x-forwarded-for", + "x-forwarded-host", + "x-forwarded-proto", + "x-forwarded-port", + "x-real-ip", + "forwarded", + "via", + // Client identity (Stainless SDK — Claude Code specific, not Antigravity) + "x-title", + "x-stainless-lang", + "x-stainless-package-version", + "x-stainless-os", + "x-stainless-arch", + "x-stainless-runtime", + "x-stainless-runtime-version", + "x-stainless-timeout", + "x-stainless-retry-count", + "x-stainless-helper-method", + "http-referer", + "referer", + // Browser / Chromium fingerprint (Electron clients, NOT Node.js) + "sec-ch-ua", + "sec-ch-ua-mobile", + "sec-ch-ua-platform", + "sec-fetch-mode", + "sec-fetch-site", + "sec-fetch-dest", + "priority", + // Encoding: Antigravity (Node.js) sends "gzip, deflate, br" by default; + // Electron clients add "zstd" which is a fingerprint mismatch. + "accept-encoding", +]; + +/** + * Remove headers that reveal proxy infrastructure or non-native client identity + * from an outgoing request to Antigravity's upstream API. + */ +export function scrubProxyAndFingerprintHeaders( + headers: Record +): Record { + const cleaned: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (!HEADERS_TO_REMOVE.includes(key.toLowerCase())) { + cleaned[key] = value; + } + } + // Set the standard Node.js accept-encoding + cleaned["Accept-Encoding"] = "gzip, deflate, br"; + return cleaned; +} diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts new file mode 100644 index 0000000000..bb549f79d1 --- /dev/null +++ b/open-sse/services/antigravityHeaders.ts @@ -0,0 +1,83 @@ +/** + * Antigravity and Gemini CLI header utilities. + * + * Generates User-Agent strings and API client headers that match + * the real Antigravity and Gemini CLI binaries. + * + * Based on CLIProxyAPI's misc/header_utils.go. + */ + +import os from "node:os"; + +const ANTIGRAVITY_VERSION = "1.21.9"; +const GEMINI_CLI_VERSION = "0.31.0"; +const GEMINI_SDK_VERSION = "1.41.0"; +const NODE_VERSION = "v22.19.0"; + +function getPlatform(): string { + const p = os.platform(); + switch (p) { + case "win32": return "win32"; + case "darwin": return "darwin"; + default: return p; // "linux", etc. + } +} + +function getArch(): string { + const a = os.arch(); + switch (a) { + case "x64": return "x64"; + case "ia32": return "x86"; + case "arm64": return "arm64"; + default: return a; + } +} + +function getAntigravityOS(): string { + const p = os.platform(); + switch (p) { + case "darwin": return "darwin"; + case "win32": return "windows"; + default: return p; + } +} + +function getAntigravityArch(): string { + const a = os.arch(); + switch (a) { + case "x64": return "amd64"; + case "ia32": return "386"; + case "arm64": return "arm64"; + default: return a; + } +} + +/** + * Antigravity User-Agent: "antigravity/VERSION OS/ARCH" + * Example: "antigravity/1.21.9 darwin/arm64" + */ +export function antigravityUserAgent(): string { + return `antigravity/${ANTIGRAVITY_VERSION} ${getAntigravityOS()}/${getAntigravityArch()}`; +} + +/** + * Gemini CLI User-Agent: "GeminiCLI/VERSION/MODEL (OS; ARCH)" + * Example: "GeminiCLI/0.31.0/gemini-3-flash (darwin; arm64)" + */ +export function geminiCLIUserAgent(model: string): string { + return `GeminiCLI/${GEMINI_CLI_VERSION}/${model || "unknown"} (${getPlatform()}; ${getArch()})`; +} + +/** + * X-Goog-Api-Client header value matching the real Gemini SDK. + * Example: "google-genai-sdk/1.41.0 gl-node/v22.19.0" + */ +export function googApiClientHeader(): string { + return `google-genai-sdk/${GEMINI_SDK_VERSION} gl-node/${NODE_VERSION}`; +} + +export { + ANTIGRAVITY_VERSION, + GEMINI_CLI_VERSION, + GEMINI_SDK_VERSION, +}; diff --git a/open-sse/services/antigravityObfuscation.ts b/open-sse/services/antigravityObfuscation.ts new file mode 100644 index 0000000000..1602efec45 --- /dev/null +++ b/open-sse/services/antigravityObfuscation.ts @@ -0,0 +1,37 @@ +/** + * Sensitive word obfuscation for Antigravity requests. + * + * Obfuscates client tool names (OpenCode, Cursor, Claude Code, etc.) using + * zero-width joiners so Google's backend can't grep for them in request logs. + * Matching ZeroGravity's ZEROGRAVITY_SENSITIVE_WORDS and CLIProxyAPI's cloak system. + */ + +const ZWJ = "\u200d"; + +const DEFAULT_WORDS = [ + "opencode", "open-code", "cline", "roo-cline", "roo_cline", + "cursor", "windsurf", "aider", "continue.dev", "copilot", + "avante", "codecompanion", "claude code", "claude-code", + "kilo code", "kilocode", "omniroute", +]; + +let words = [...DEFAULT_WORDS]; + +export function setAntigravitySensitiveWords(w: string[]): void { + words = w.length > 0 ? w : [...DEFAULT_WORDS]; +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function obfuscateSensitiveWords(text: string): string { + if (!text || words.length === 0) return text; + let result = text; + for (const word of words) { + if (!word) continue; + const regex = new RegExp(escapeRegex(word), "gi"); + result = result.replace(regex, (m) => m.length <= 1 ? m : m[0] + ZWJ + m.slice(1)); + } + return result; +} From 7eb305685668ced07377860caa71c505701713ea Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 13 Apr 2026 11:41:22 +0200 Subject: [PATCH 2/3] feat: update Gemini CLI executor with dynamic UA, header scrubbing, and obfuscation Matches the Antigravity executor treatment: - Dynamic User-Agent: GeminiCLI/0.31.0/MODEL (OS; ARCH) per-model - X-Goog-Api-Client: maintained (was already correct) - Header scrubbing: removes proxy/fingerprint headers - Sensitive word obfuscation in user message content - Tracks current model for per-request UA generation --- open-sse/executors/gemini-cli.ts | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/open-sse/executors/gemini-cli.ts b/open-sse/executors/gemini-cli.ts index 3c8298a0b6..228b00b38c 100644 --- a/open-sse/executors/gemini-cli.ts +++ b/open-sse/executors/gemini-cli.ts @@ -1,5 +1,8 @@ import { BaseExecutor } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; +import { geminiCLIUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts"; +import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts"; +import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts"; const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"; const PROJECT_TTL_MS = 30_000; // 30 seconds — matches native Gemini CLI @@ -22,19 +25,20 @@ export class GeminiCLIExecutor extends BaseExecutor { } buildHeaders(credentials, stream = true) { - return { + const raw = { "Content-Type": "application/json", Authorization: `Bearer ${credentials.accessToken}`, - // Fingerprint headers matching native GeminiCLI client (prevents upstream rejection) - "User-Agent": "GeminiCLI/0.31.0/unknown (linux; x64)", - "X-Goog-Api-Client": "google-genai-sdk/1.41.0 gl-node/v22.19.0", + // Dynamic headers matching native GeminiCLI client + "User-Agent": geminiCLIUserAgent(this._currentModel || "unknown"), + "X-Goog-Api-Client": googApiClientHeader(), ...(stream && { Accept: "text/event-stream" }), - // NOTE: x-goog-user-project removed — the stored projectId can become stale for - // free-tier accounts, causing 403 "Cloud Code Private API has not been used in - // project X". The API resolves the correct project from the OAuth token alone. }; + return scrubProxyAndFingerprintHeaders(raw); } + // Track current model for dynamic UA (set by transformRequest) + private _currentModel = "unknown"; + /** * Fetch the current cloudaicompanionProject via loadCodeAssist API. * Native Gemini CLI refreshes this every 30 seconds — OmniRoute stores it once @@ -134,15 +138,29 @@ export class GeminiCLIExecutor extends BaseExecutor { } async transformRequest(model, body, stream, credentials) { + // Track model for dynamic User-Agent + this._currentModel = model || "unknown"; + // Refresh the project ID via loadCodeAssist (cached for 30s). - // The translator builds the envelope with the stale stored projectId — - // we replace it here with the fresh one before sending to the API. if (body && typeof body === "object" && body.request && credentials.accessToken) { const freshProject = await this.refreshProject(credentials.accessToken); if (freshProject) { body.project = freshProject; } - // If refresh failed, keep the stale projectId as a best-effort fallback + + // Obfuscate sensitive client names in user content + const contents = body.request?.contents; + if (Array.isArray(contents)) { + for (const msg of contents) { + if (Array.isArray(msg.parts)) { + for (const part of msg.parts) { + if (typeof part.text === "string") { + part.text = obfuscateSensitiveWords(part.text); + } + } + } + } + } } return body; } From b142145a4e70c211e81a9df55547150eb40ecbaf Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 13 Apr 2026 11:45:23 +0200 Subject: [PATCH 3/3] fix: hardcode Antigravity UA to darwin/arm64 to match CLIProxyAPI production behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLIProxyAPI works without bans using hardcoded darwin/arm64 in the Antigravity User-Agent. Real Antigravity is a macOS desktop tool — reporting the actual server OS (linux/amd64 on a VPS) is MORE suspicious than always claiming darwin/arm64. Matches proven production fingerprint. --- open-sse/services/antigravityHeaders.ts | 33 +++++++------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts index bb549f79d1..135125b8c4 100644 --- a/open-sse/services/antigravityHeaders.ts +++ b/open-sse/services/antigravityHeaders.ts @@ -1,3 +1,5 @@ +import os from "node:os"; + /** * Antigravity and Gemini CLI header utilities. * @@ -7,8 +9,6 @@ * Based on CLIProxyAPI's misc/header_utils.go. */ -import os from "node:os"; - const ANTIGRAVITY_VERSION = "1.21.9"; const GEMINI_CLI_VERSION = "0.31.0"; const GEMINI_SDK_VERSION = "1.41.0"; @@ -33,31 +33,16 @@ function getArch(): string { } } -function getAntigravityOS(): string { - const p = os.platform(); - switch (p) { - case "darwin": return "darwin"; - case "win32": return "windows"; - default: return p; - } -} - -function getAntigravityArch(): string { - const a = os.arch(); - switch (a) { - case "x64": return "amd64"; - case "ia32": return "386"; - case "arm64": return "arm64"; - default: return a; - } -} - /** - * Antigravity User-Agent: "antigravity/VERSION OS/ARCH" - * Example: "antigravity/1.21.9 darwin/arm64" + * Antigravity User-Agent: "antigravity/VERSION darwin/arm64" + * + * Always claims darwin/arm64 regardless of actual server OS. + * Real Antigravity is a macOS desktop tool — most users are on macOS. + * Claiming linux/amd64 from a datacenter IP is MORE suspicious than + * darwin/arm64. Matches CLIProxyAPI's proven production behavior. */ export function antigravityUserAgent(): string { - return `antigravity/${ANTIGRAVITY_VERSION} ${getAntigravityOS()}/${getAntigravityArch()}`; + return `antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`; } /**