feat: align Antigravity 2.0.1 support (#2443)

Integrated into release/v3.8.1
This commit is contained in:
Raxxoor
2026-05-20 20:35:30 +01:00
committed by GitHub
parent a0d2dee0c5
commit 18e27038e2
29 changed files with 966 additions and 268 deletions

View File

@@ -603,7 +603,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
CLAUDE_USER_AGENT="claude-cli/2.1.143 (external, cli)"
CODEX_USER_AGENT="codex-cli/0.131.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
ANTIGRAVITY_USER_AGENT="antigravity/1.23.2 darwin/arm64"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
QODER_USER_AGENT="Qoder-Cli"
QWEN_USER_AGENT="QwenCode/0.15.9 (linux; x64)"

View File

@@ -1,11 +1,132 @@
export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 Thinking" },
{ id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" },
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" },
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" },
{
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 250000,
maxOutputTokens: 64000,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Thinking)",
contextLength: 250000,
maxOutputTokens: 64000,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3-pro-preview",
name: "Gemini 3.1 Pro (High)",
contextLength: 1048576,
maxOutputTokens: 65535,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.1-pro-low",
name: "Gemini 3.1 Pro (Low)",
contextLength: 1048576,
maxOutputTokens: 65535,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-preview",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3-flash-agent",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-low",
name: "Gemini 3.5 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.1-flash-lite",
name: "Gemini 3.1 Flash Lite",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
contextLength: 1048576,
maxOutputTokens: 65535,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-2.5-flash-lite",
name: "Gemini 2.5 Flash Lite",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-2.5-flash-thinking",
name: "Gemini 2.5 Flash Thinking",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-pro-agent",
name: "Gemini 3.1 Pro (High)",
contextLength: 1048576,
maxOutputTokens: 65535,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B (Medium)",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: true,
toolCalling: true,
},
{ id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro Image" },
{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" },
{
id: "gemini-2.5-computer-use-preview-10-2025",
name: "Gemini 2.5 Computer Use Preview (10/2025)",
@@ -14,6 +135,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
"gemini-3-pro-preview": "gemini-3.1-pro-high",
"gemini-3.5-flash-preview": "gemini-3-flash-agent",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-2.5-computer-use-preview-10-2025": "rev19-uic3-1p",
@@ -26,6 +148,7 @@ type AntigravityModelAliasMap = Record<string, string>;
export const ANTIGRAVITY_REVERSE_MODEL_ALIASES: AntigravityModelAliasMap = Object.freeze({
"gemini-3.1-pro-high": "gemini-3-pro-preview",
"gemini-3-flash-agent": "gemini-3.5-flash-preview",
"gemini-3-flash": "gemini-3-flash-preview",
"gemini-3-pro-image": "gemini-3-pro-image-preview",
"rev19-uic3-1p": "gemini-2.5-computer-use-preview-10-2025",

View File

@@ -1,7 +1,7 @@
export const ANTIGRAVITY_BASE_URLS = Object.freeze([
"https://daily-cloudcode-pa.sandbox.googleapis.com",
"https://daily-cloudcode-pa.googleapis.com",
"https://cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
]);
const ANTIGRAVITY_MODELS_PATH = "/v1internal:models";

View File

@@ -174,17 +174,18 @@ export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
},
antigravity: {
headerOrder: [
"Host",
"Content-Type",
"Accept",
"Accept-Encoding",
"Authorization",
"Content-Type",
"User-Agent",
"x-goog-api-client",
"x-client-name",
"x-client-version",
"x-machine-id",
"x-vscode-sessionid",
"x-goog-user-project",
"Accept",
"Accept-Encoding",
"Host",
"Connection",
],
bodyFieldOrder: [
"project",

View File

@@ -2820,7 +2820,12 @@ export async function handleChatCore({
credentials,
provider,
reqLogger,
{ normalizeToolCallId, preserveDeveloperRole, preserveCacheControl }
{
normalizeToolCallId,
preserveDeveloperRole,
preserveCacheControl,
signatureNamespace: connectionId,
}
);
}
} catch (error) {
@@ -3071,7 +3076,6 @@ export async function handleChatCore({
// Create stream controller for disconnect detection
const streamController = createStreamController({
onDisconnect,
log,
provider,
model,
connectionId,

View File

@@ -18,13 +18,8 @@ import { randomUUID } from "crypto";
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
import { HTTP_STATUS } from "../config/constants.ts";
import { antigravityUserAgent } from "../services/antigravityHeaders.ts";
import {
deriveAntigravityMachineId,
getAntigravityEnvelopeUserAgent,
getAntigravityVscodeSessionId,
} from "../services/antigravityIdentity.ts";
import { getCachedAntigravityVersion } from "../services/antigravityVersion.ts";
import { applyAntigravityClientProfileHeaders } from "../services/antigravityClientProfile.ts";
import { getAntigravityEnvelopeUserAgent } from "../services/antigravityIdentity.ts";
import { kieExecutor } from "../executors/kie.ts";
import { mapImageSize } from "../translator/image/sizeMapper.ts";
import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts";
@@ -699,13 +694,9 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"User-Agent": antigravityUserAgent(),
"x-client-name": "antigravity",
"x-client-version": getCachedAntigravityVersion(),
"x-machine-id": deriveAntigravityMachineId(credentialRecord),
"x-vscode-sessionid": getAntigravityVscodeSessionId(),
"x-goog-user-project": projectId,
};
applyAntigravityClientProfileHeaders(headers, credentialRecord, antigravityBody);
delete headers["x-goog-user-project"];
if (log) {
const promptPreview = promptText.slice(0, 60);
@@ -716,25 +707,12 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden
}
try {
let response = await fetch(url, {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(antigravityBody),
});
if (response.status === HTTP_STATUS.FORBIDDEN && headers["x-goog-user-project"]) {
const retryHeaders = { ...headers };
delete retryHeaders["x-goog-user-project"];
if (log) {
log.info("IMAGE", "antigravity image 403 with x-goog-user-project; retrying without it");
}
response = await fetch(url, {
method: "POST",
headers: retryHeaders,
body: JSON.stringify(antigravityBody),
});
}
if (!response.ok) {
const errorText = await response.text();
const safeError = sanitizeImageProviderError(errorText);

View File

@@ -24,6 +24,7 @@ export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT = "";
export const ANTIGRAVITY_NODE_API_CLIENT = "google-api-nodejs-client/10.3.0";
// Harness/bootstrap X-Goog-Api-Client synced with CLIProxyAPI misc.AntigravityGoogAPIClientUA.
export const ANTIGRAVITY_CREDIT_PROBE_API_CLIENT = "gl-node/22.21.1";
export const ANTIGRAVITY_API_CLIENT = ANTIGRAVITY_CREDIT_PROBE_API_CLIENT;
type AntigravityLoadCodeAssistPlatform = "MACOS" | "WINDOWS" | "LINUX";
function getAntigravityLoadCodeAssistPlatformLabel(
@@ -131,4 +132,9 @@ export function getAntigravityCreditProbeApiClientHeader(): string {
return ANTIGRAVITY_CREDIT_PROBE_API_CLIENT;
}
/** X-Goog-Api-Client used by harness/native Node Antigravity paths. */
export function getAntigravityApiClientHeader(): string {
return ANTIGRAVITY_API_CLIENT;
}
export { ANTIGRAVITY_VERSION };

View File

@@ -116,18 +116,20 @@ export async function ensureAntigravityProjectAssigned(
accessToken: string,
fetchImpl: FetchLike = fetch,
clientProfile: AntigravityClientProfile = "ide"
): Promise<void> {
): Promise<string | undefined> {
const cacheKey = getProjectCacheKey(accessToken, clientProfile);
if (projectCache.has(cacheKey)) {
return; // already bootstrapped for this token
return projectCache.get(cacheKey); // already bootstrapped for this token
}
const projectId = await tryLoadCodeAssist(accessToken, fetchImpl, clientProfile);
if (projectId) {
projectCache.set(cacheKey, projectId);
return projectId;
}
// Non-fatal: if all endpoints failed, we proceed without caching.
return undefined;
}
/** Exported for tests. */

View File

@@ -1,5 +1,10 @@
import { getDbInstance } from "../../src/lib/db/core.ts";
const MAX_SIGNATURES = 1000;
const TTL_MS = 1000 * 60 * 60;
const MAX_PERSISTED_SIGNATURES = 2_000;
const MEMORY_TTL_MS = 1000 * 60 * 60;
const PERSISTED_TTL_MS = 1000 * 60 * 60 * 24 * 30;
const NAMESPACE = "gemini_thought_signatures";
export type SignatureCacheMode = "enabled" | "bypass" | "bypass-strict";
@@ -8,8 +13,34 @@ type Entry = {
expiresAt: number;
};
type PersistedEntry = Entry & {
createdAt: number;
};
const signatures = new Map<string, Entry>();
let signatureCacheMode: SignatureCacheMode = "enabled";
let persistedPruneCounter = 0;
const loggedPersistenceErrors = new Set<string>();
function warnPersistenceError(operation: string, error: unknown) {
if (process.env.NODE_ENV === "test") return;
if (loggedPersistenceErrors.has(operation)) return;
loggedPersistenceErrors.add(operation);
const message = error instanceof Error ? error.message : String(error);
console.warn(`[signature-cache] persisted ${operation} failed: ${message}`);
}
export function buildGeminiThoughtSignatureKey(namespace: unknown, toolCallId: unknown): unknown {
if (
typeof namespace === "string" &&
namespace.length > 0 &&
typeof toolCallId === "string" &&
toolCallId.length > 0
) {
return `${namespace}:${toolCallId}`;
}
return toolCallId;
}
function pruneExpired() {
const now = Date.now();
@@ -26,15 +57,93 @@ function pruneExpired() {
}
}
function serializePersistedEntry(entry: PersistedEntry): string {
return JSON.stringify(entry);
}
function parsePersistedEntry(value: string, now = Date.now()) {
try {
const parsed = JSON.parse(value) as Partial<PersistedEntry>;
if (typeof parsed.signature !== "string" || parsed.signature.length === 0) return null;
const createdAt = typeof parsed.createdAt === "number" ? parsed.createdAt : now;
const expiresAt =
typeof parsed.expiresAt === "number" ? parsed.expiresAt : now + PERSISTED_TTL_MS;
if (expiresAt <= now) return null;
return {
entry: { signature: parsed.signature, createdAt, expiresAt },
shouldRewrite: createdAt !== parsed.createdAt || expiresAt !== parsed.expiresAt,
};
} catch {
if (!value) return null;
return {
entry: {
signature: value,
createdAt: now,
expiresAt: now + PERSISTED_TTL_MS,
},
shouldRewrite: true,
};
}
}
function maybePrunePersistedSignatures(db: ReturnType<typeof getDbInstance>) {
persistedPruneCounter += 1;
if (persistedPruneCounter % 100 !== 0) return;
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = ?")
.all(NAMESPACE) as Array<{ key: string; value: string }>;
const now = Date.now();
const validRows: Array<{ key: string; createdAt: number }> = [];
const keysToDelete = new Set<string>();
for (const row of rows) {
const parsed = parsePersistedEntry(row.value, now);
if (!parsed) {
keysToDelete.add(row.key);
continue;
}
validRows.push({ key: row.key, createdAt: parsed.entry.createdAt });
}
if (rows.length <= MAX_PERSISTED_SIGNATURES && keysToDelete.size === 0) return;
validRows.sort((a, b) => b.createdAt - a.createdAt);
for (const row of validRows.slice(MAX_PERSISTED_SIGNATURES)) {
keysToDelete.add(row.key);
}
if (keysToDelete.size === 0) return;
const remove = db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?");
const tx = db.transaction((keys: string[]) => {
for (const key of keys) remove.run(NAMESPACE, key);
});
tx([...keysToDelete]);
}
export function storeGeminiThoughtSignature(toolCallId: unknown, signature: unknown) {
if (typeof toolCallId !== "string" || !toolCallId) return;
if (typeof signature !== "string" || !signature) return;
const now = Date.now();
pruneExpired();
signatures.set(toolCallId, {
signature,
expiresAt: Date.now() + TTL_MS,
expiresAt: now + MEMORY_TTL_MS,
});
try {
const db = getDbInstance();
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
NAMESPACE,
toolCallId,
serializePersistedEntry({ signature, createdAt: now, expiresAt: now + PERSISTED_TTL_MS })
);
maybePrunePersistedSignatures(db);
} catch (error) {
warnPersistenceError("store", error);
}
}
export function getGeminiThoughtSignature(toolCallId: unknown) {
@@ -42,8 +151,44 @@ export function getGeminiThoughtSignature(toolCallId: unknown) {
pruneExpired();
const entry = signatures.get(toolCallId);
if (!entry) return null;
return entry.signature;
if (entry) return entry.signature;
try {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get(NAMESPACE, toolCallId) as { value: string } | undefined;
if (row?.value) {
const persisted = parsePersistedEntry(row.value);
if (!persisted) {
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(
NAMESPACE,
toolCallId
);
return null;
}
signatures.set(toolCallId, {
signature: persisted.entry.signature,
expiresAt: Date.now() + MEMORY_TTL_MS,
});
if (persisted.shouldRewrite) {
db.prepare("UPDATE key_value SET value = ? WHERE namespace = ? AND key = ?").run(
serializePersistedEntry(persisted.entry),
NAMESPACE,
toolCallId
);
}
return persisted.entry.signature;
}
} catch (error) {
warnPersistenceError("read", error);
}
return null;
}
export function normalizeSignatureCacheMode(value: unknown): SignatureCacheMode {
@@ -161,4 +306,19 @@ export function resolveGeminiThoughtSignature(
export function clearGeminiThoughtSignatures() {
signatures.clear();
signatureCacheMode = "enabled";
try {
const db = getDbInstance();
db.prepare("DELETE FROM key_value WHERE namespace = ?").run(NAMESPACE);
} catch (error) {
warnPersistenceError("clear", error);
}
}
export function clearGeminiThoughtSignatureMemoryForTests() {
signatures.clear();
}
export function getGeminiThoughtSignatureMemorySizeForTests() {
pruneExpired();
return signatures.size;
}

View File

@@ -131,6 +131,7 @@ export function translateRequest(
normalizeToolCallId?: boolean;
preserveDeveloperRole?: boolean;
preserveCacheControl?: boolean;
signatureNamespace?: string | null;
}
) {
let result = body;
@@ -184,7 +185,13 @@ export function translateRequest(
if (targetFormat !== FORMATS.OPENAI) {
const fromOpenAI = getRequestTranslator(FORMATS.OPENAI, targetFormat);
if (fromOpenAI) {
result = fromOpenAI(model, result, stream, credentials);
const translationCredentials = options?.signatureNamespace
? {
...(credentials && typeof credentials === "object" ? credentials : {}),
_signatureNamespace: options.signatureNamespace,
}
: credentials;
result = fromOpenAI(model, result, stream, translationCredentials);
}
}
}

View File

@@ -2,7 +2,10 @@ import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/constants.ts";
import { resolveGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts";
import {
buildGeminiThoughtSignatureKey,
resolveGeminiThoughtSignature,
} from "../../services/geminiThoughtSignatureStore.ts";
import {
generateAntigravityRequestId,
getAntigravityEnvelopeUserAgent,
@@ -104,6 +107,8 @@ type CloudCodeEnvelope = {
type GeminiToolNameOptions = {
stripNamespace?: boolean;
functionResponseShape?: "result" | "output";
signatureNamespace?: string | null;
signaturelessToolCallMode?: "native" | "text";
};
type OpenAIToolCallLike = {
@@ -286,20 +291,36 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
const toolCallIds = [];
const firstPersistedSignature = msg.tool_calls
.map((tc) => resolveGeminiThoughtSignature(tc.id, extractClientThoughtSignature(tc)))
.find((signature) => typeof signature === "string" && signature.length > 0);
const resolvedSignatures = new Map<unknown, string>();
let firstPersistedSignature: string | undefined;
for (const tc of msg.tool_calls) {
const resolved = resolveGeminiThoughtSignature(
buildGeminiThoughtSignatureKey(toolNameOptions.signatureNamespace, tc.id),
extractClientThoughtSignature(tc)
);
if (typeof resolved === "string" && resolved.length > 0) {
resolvedSignatures.set(tc.id, resolved);
firstPersistedSignature ??= resolved;
}
}
let shouldUseEmbeddedSignature = !parts.some((p) => p.thoughtSignature);
const stringifySignaturelessToolCalls =
toolNameOptions.signaturelessToolCallMode === "text";
for (const tc of msg.tool_calls) {
if (tc.type !== "function") continue;
const signatureForToolCall = resolvedSignatures.get(tc.id);
if (!signatureForToolCall && stringifySignaturelessToolCalls) {
const args = tc.function?.arguments || "{}";
parts.push({
text: `[Tool call: ${tc.function?.name || "unknown"}]\nArguments: ${args}`,
});
continue;
}
const args = tryParseJSON(tc.function?.arguments || "{}");
const signatureForToolCall = resolveGeminiThoughtSignature(
tc.id,
extractClientThoughtSignature(tc)
);
const embeddedThoughtSignature = shouldUseEmbeddedSignature
? firstPersistedSignature || signatureForToolCall
: undefined;
@@ -326,7 +347,14 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
}
// Check if there are actual tool responses in the next messages
const hasActualResponses = toolCallIds.some((fid) => toolResponses[fid]);
const hasSignaturelessTextResponses =
stringifySignaturelessToolCalls &&
msg.tool_calls.some(
(tc) =>
tc.type === "function" && !resolvedSignatures.has(tc.id) && toolResponses[tc.id]
);
const hasActualResponses =
toolCallIds.some((fid) => toolResponses[fid]) || hasSignaturelessTextResponses;
if (hasActualResponses) {
const toolParts = [];
@@ -363,6 +391,23 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
},
});
}
if (stringifySignaturelessToolCalls) {
// Signature-less historical tool responses are represented as text
// so strict Gemini/Antigravity endpoints don't reject them as native
// functionResponse parts missing a matching thoughtSignature.
for (const tc of msg.tool_calls) {
if (tc.type !== "function" || !tc.id) continue;
if (!resolvedSignatures.has(tc.id) && toolResponses[tc.id]) {
const name = tcID2Name[tc.id] || tc.function?.name || "unknown";
const resp = toolResponses[tc.id];
toolParts.push({
text: `[Tool response: ${name}]\nResult: ${resp}`,
});
}
}
}
if (toolParts.length > 0) {
result.contents.push({ role: "user", parts: toolParts });
}
@@ -420,11 +465,17 @@ export function openaiToGeminiCLIRequest(
model,
body,
stream,
options: { functionResponseShape?: "result" | "output" } = {}
options: {
functionResponseShape?: "result" | "output";
signatureNamespace?: string | null;
signaturelessToolCallMode?: "native" | "text";
} = {}
) {
const gemini = openaiToGeminiBase(model, body, stream, {
stripNamespace: true,
functionResponseShape: options.functionResponseShape,
signatureNamespace: options.signatureNamespace,
signaturelessToolCallMode: options.signaturelessToolCallMode,
});
// Add thinking config for CLI
@@ -536,7 +587,16 @@ function getAntigravityClaudeOutputTokens(body: Record<string, unknown>): number
// OpenAI -> Antigravity (Sandbox Cloud Code with wrapper)
export function openaiToAntigravityRequest(model, body, stream, credentials = null) {
const isClaude = model.toLowerCase().includes("claude");
const geminiCLI = openaiToGeminiCLIRequest(model, body, stream);
const signatureNamespace =
credentials &&
typeof credentials === "object" &&
typeof (credentials as Record<string, unknown>)._signatureNamespace === "string"
? ((credentials as Record<string, unknown>)._signatureNamespace as string)
: null;
const geminiCLI = openaiToGeminiCLIRequest(model, body, stream, {
signatureNamespace,
signaturelessToolCallMode: isClaude ? "native" : "text",
});
if (isClaude) {
geminiCLI.generationConfig.maxOutputTokens = getAntigravityClaudeOutputTokens(body);

View File

@@ -1,13 +1,88 @@
import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
import { storeGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts";
import {
buildGeminiThoughtSignatureKey,
storeGeminiThoughtSignature,
} from "../../services/geminiThoughtSignatureStore.ts";
function buildToolCallId(functionCall, toolName, toolCallIndex) {
type GeminiToOpenAIState = {
functionIndex: number;
messageId: string;
model: string;
pendingThoughtSignature?: string | null;
signatureNamespace?: string | null;
toolCalls: Map<number, unknown>;
toolNameMap?: Map<string, string>;
};
type GeminiFunctionCallPart = {
functionCall: {
args?: unknown;
id?: string;
name: string;
};
};
function buildToolCallId(
functionCall: GeminiFunctionCallPart["functionCall"],
toolName: string,
toolCallIndex: number
) {
return typeof functionCall?.id === "string" && functionCall.id.length > 0
? functionCall.id
: `${toolName}-${Date.now()}-${toolCallIndex}`;
}
function getSignatureCacheKey(
state: Pick<GeminiToOpenAIState, "signatureNamespace">,
toolCallId: unknown
) {
return buildGeminiThoughtSignatureKey(state?.signatureNamespace, toolCallId);
}
function emitFunctionCallPart(
part: GeminiFunctionCallPart,
state: GeminiToOpenAIState,
results: Array<Record<string, unknown>>
) {
const rawToolName = part.functionCall.name;
const fcName = state.toolNameMap?.get(rawToolName) || rawToolName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: buildToolCallId(part.functionCall, fcName, toolCallIndex),
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs),
},
};
if (state.pendingThoughtSignature) {
storeGeminiThoughtSignature(
getSignatureCacheKey(state, toolCall.id),
state.pendingThoughtSignature
);
state.pendingThoughtSignature = null;
}
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [
{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null,
},
],
});
}
// Convert Gemini response chunk to OpenAI format
export function geminiToOpenAIResponse(chunk, state) {
if (!chunk) return null;
@@ -111,41 +186,7 @@ export function geminiToOpenAIResponse(chunk, state) {
}
if (hasFunctionCall) {
const rawToolName = part.functionCall.name;
const fcName = state.toolNameMap?.get(rawToolName) || rawToolName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: buildToolCallId(part.functionCall, fcName, toolCallIndex),
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs),
},
};
if (state.pendingThoughtSignature) {
storeGeminiThoughtSignature(toolCall.id, state.pendingThoughtSignature);
state.pendingThoughtSignature = null;
}
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [
{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null,
},
],
});
emitFunctionCallPart(part, state, results);
}
continue;
}
@@ -169,41 +210,7 @@ export function geminiToOpenAIResponse(chunk, state) {
// Function call
if (part.functionCall) {
const rawToolName = part.functionCall.name;
const fcName = state.toolNameMap?.get(rawToolName) || rawToolName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: buildToolCallId(part.functionCall, fcName, toolCallIndex),
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs),
},
};
if (state.pendingThoughtSignature) {
storeGeminiThoughtSignature(toolCall.id, state.pendingThoughtSignature);
state.pendingThoughtSignature = null;
}
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [
{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null,
},
],
});
emitFunctionCallPart(part, state, results);
}
// Inline data (images)

View File

@@ -58,7 +58,7 @@ export { COLORS, formatSSE };
type JsonRecord = Record<string, unknown>;
const PENDING_REQUEST_CLEARED_MARKER = "__omniroutePendingRequestCleared";
export const PENDING_REQUEST_CLEARED_MARKER = "__omniroutePendingRequestCleared";
function markPendingRequestCleared(error: Error): Error {
(error as Error & Record<string, unknown>)[PENDING_REQUEST_CLEARED_MARKER] = true;
@@ -149,6 +149,7 @@ type StreamOptions = {
type TranslateState = ReturnType<typeof initState> & {
provider?: string | null;
toolNameMap?: unknown;
signatureNamespace?: string | null;
usage?: unknown;
finishReason?: unknown;
copilotCompatibleReasoning?: boolean;
@@ -544,6 +545,7 @@ export function createSSEStream(options: StreamOptions = {}) {
onComplete = null,
onFailure = null,
} = options;
const signatureNamespace = connectionId;
const clientExpectsResponsesStream =
(mode === STREAM_MODE.PASSTHROUGH
@@ -583,6 +585,7 @@ export function createSSEStream(options: StreamOptions = {}) {
...(initState(sourceFormat) as TranslateState),
provider,
toolNameMap,
signatureNamespace,
copilotCompatibleReasoning,
accumulatedContent: "",
}

View File

@@ -1,9 +1,9 @@
import { trackPendingRequest } from "@/lib/usageDb";
import { FORMATS } from "../translator/formats.ts";
import { PENDING_REQUEST_CLEARED_MARKER } from "./stream.ts";
// Stream handler with disconnect detection - shared for all providers
const PENDING_REQUEST_CLEARED_MARKER = "__omniroutePendingRequestCleared";
const DISCONNECT_ABORT_DELAY_MS = 2_000;
type StreamDisconnectEvent = {
@@ -13,7 +13,6 @@ type StreamDisconnectEvent = {
type StreamControllerOptions = {
onDisconnect?: (event: StreamDisconnectEvent) => void;
log?: unknown;
provider?: string;
model?: string;
connectionId?: string | null;
@@ -22,6 +21,18 @@ type StreamControllerOptions = {
type StreamController = ReturnType<typeof createStreamController>;
type StreamErrorStatusKind = "rate_limit" | "authentication" | "permission" | "client" | "server";
type StreamErrorStatusMapping = {
responses: {
type: string;
code: string;
};
claude: {
type: string;
};
};
function isResponsesClientFormat(clientResponseFormat?: string | null): boolean {
return (
clientResponseFormat === FORMATS.OPENAI_RESPONSES ||
@@ -29,27 +40,70 @@ function isResponsesClientFormat(clientResponseFormat?: string | null): boolean
);
}
function responsesErrorType(statusCode: number): string {
if (statusCode === 429) return "rate_limit_error";
if (statusCode === 401 || statusCode === 403) return "authentication_error";
if (statusCode >= 400 && statusCode < 500) return "invalid_request_error";
return "server_error";
function getStreamErrorStatusKind(statusCode: number): StreamErrorStatusKind {
if (statusCode === 429) return "rate_limit";
if (statusCode === 401) return "authentication";
if (statusCode === 403) return "permission";
if (statusCode >= 400 && statusCode < 500) return "client";
return "server";
}
function responsesErrorCode(statusCode: number): string {
if (statusCode === 429) return "rate_limit_exceeded";
if (statusCode === 401) return "invalid_authentication";
if (statusCode === 403) return "permission_denied";
if (statusCode >= 400 && statusCode < 500) return "bad_request";
return "server_error";
function getStreamErrorStatusMapping(statusCode: number): StreamErrorStatusMapping {
switch (getStreamErrorStatusKind(statusCode)) {
case "rate_limit":
return {
responses: { type: "rate_limit_error", code: "rate_limit_exceeded" },
claude: { type: "rate_limit_error" },
};
case "authentication":
return {
responses: { type: "authentication_error", code: "invalid_authentication" },
claude: { type: "authentication_error" },
};
case "permission":
return {
responses: { type: "authentication_error", code: "permission_denied" },
claude: { type: "permission_error" },
};
case "client":
return {
responses: { type: "invalid_request_error", code: "bad_request" },
claude: { type: "invalid_request_error" },
};
case "server":
return {
responses: { type: "server_error", code: "server_error" },
claude: { type: "api_error" },
};
default:
return {
responses: { type: "server_error", code: "server_error" },
claude: { type: "api_error" },
};
}
}
function claudeErrorType(statusCode: number): string {
if (statusCode === 429) return "rate_limit_error";
if (statusCode === 401) return "authentication_error";
if (statusCode === 403) return "permission_error";
if (statusCode >= 400 && statusCode < 500) return "invalid_request_error";
return "api_error";
function encodeSseEvent(
data: unknown,
{
event,
includeDone = false,
}: {
event?: string;
includeDone?: boolean;
} = {}
) {
if (event && /[\r\n]/.test(event)) {
throw new Error("SSE event names must not contain newlines");
}
const encoder = new TextEncoder();
const prefix = event ? `event: ${event}\n` : "";
const chunks = [encoder.encode(`${prefix}data: ${JSON.stringify(data)}\n\n`)];
if (includeDone) {
chunks.push(encoder.encode("data: [DONE]\n\n"));
}
return chunks;
}
// Get HH:MM:SS timestamp
@@ -73,7 +127,6 @@ function getTimeString() {
/** @param {StreamControllerOptions} options */
export function createStreamController({
onDisconnect,
log,
provider,
model,
connectionId,
@@ -180,7 +233,8 @@ function buildStreamErrorChunks(
statusCode: number,
clientResponseFormat?: string | null
) {
const encoder = new TextEncoder();
const statusMapping = getStreamErrorStatusMapping(statusCode);
if (isResponsesClientFormat(clientResponseFormat)) {
const errorEvent = {
type: "response.failed",
@@ -189,25 +243,25 @@ function buildStreamErrorChunks(
status: "failed",
error: {
message: errorMsg,
type: responsesErrorType(statusCode),
code: responsesErrorCode(statusCode),
type: statusMapping.responses.type,
code: statusMapping.responses.code,
},
},
};
return [encoder.encode(`event: response.failed\ndata: ${JSON.stringify(errorEvent)}\n\n`)];
return encodeSseEvent(errorEvent, { event: "response.failed" });
}
if (clientResponseFormat === FORMATS.CLAUDE) {
const errorEvent = {
type: "error",
error: {
type: claudeErrorType(statusCode),
type: statusMapping.claude.type,
message: errorMsg,
},
};
return [encoder.encode(`event: error\ndata: ${JSON.stringify(errorEvent)}\n\n`)];
return encodeSseEvent(errorEvent, { event: "error" });
}
const errorEvent = {
@@ -226,10 +280,7 @@ function buildStreamErrorChunks(
},
};
return [
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`),
encoder.encode(`data: [DONE]\n\n`),
];
return encodeSseEvent(errorEvent, { includeDone: true });
}
/**

View File

@@ -61,7 +61,12 @@ export const SQLITE_FILE = isCloud ? null : path.join(DATA_DIR, "storage.sqlite"
const JSON_DB_FILE = isCloud ? null : path.join(DATA_DIR, "db.json");
export const DB_BACKUPS_DIR = isCloud ? null : path.join(DATA_DIR, "db_backups");
const DEFAULT_CRITICAL_TABLE_ROW_LIMIT = 10_000;
const SKIP_PRESERVE_NAMESPACES = new Set(["syncedAvailableModels", "providerLimitsCache", "lkgp"]);
const SKIP_PRESERVE_NAMESPACES = new Set([
"syncedAvailableModels",
"providerLimitsCache",
"lkgp",
"gemini_thought_signatures",
]);
const CRITICAL_DB_TABLES: CriticalTableSpec[] = [
{
table: "key_value",

View File

@@ -1,3 +1,7 @@
import {
ANTIGRAVITY_BASE_URLS,
getAntigravityFetchAvailableModelsUrls,
} from "@omniroute/open-sse/config/antigravityUpstream.ts";
import {
ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT,
ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT,
@@ -157,13 +161,16 @@ export const ANTIGRAVITY_CONFIG = {
"https://www.googleapis.com/auth/experimentsandconfigs",
],
// Antigravity specific
apiEndpoint: "https://daily-cloudcode-pa.sandbox.googleapis.com",
apiEndpoint: ANTIGRAVITY_BASE_URLS[0],
apiVersion: "v1internal",
loadCodeAssistEndpoint:
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist",
onboardUserEndpoint: "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:onboardUser",
fetchAvailableModelsEndpoint:
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels",
loadCodeAssistEndpoints: ANTIGRAVITY_BASE_URLS.map(
(baseUrl) => `${baseUrl}/v1internal:loadCodeAssist`
),
onboardUserEndpoints: ANTIGRAVITY_BASE_URLS.map((baseUrl) => `${baseUrl}/v1internal:onboardUser`),
fetchAvailableModelsEndpoints: getAntigravityFetchAvailableModelsUrls(),
loadCodeAssistEndpoint: `${ANTIGRAVITY_BASE_URLS[0]}/v1internal:loadCodeAssist`,
onboardUserEndpoint: `${ANTIGRAVITY_BASE_URLS[0]}/v1internal:onboardUser`,
fetchAvailableModelsEndpoint: getAntigravityFetchAvailableModelsUrls()[0],
loadCodeAssistUserAgent: ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT,
loadCodeAssistApiClient: ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT,
loadCodeAssistClientMetadata: getAntigravityLoadCodeAssistClientMetadata(),

View File

@@ -5,6 +5,20 @@ import {
getAntigravityLoadCodeAssistMetadata,
} from "@omniroute/open-sse/services/antigravityHeaders.ts";
async function fetchFirstOk(endpoints: string[], init: RequestInit) {
let lastError: unknown = null;
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint, init);
if (response.ok) return response;
lastError = new Error(`${response.status} ${await response.text()}`);
} catch (error) {
lastError = error;
}
}
throw lastError || new Error("No Antigravity endpoints configured");
}
export const antigravity = {
config: ANTIGRAVITY_CONFIG,
flowType: "authorization_code",
@@ -61,20 +75,18 @@ export const antigravity = {
let projectId = "";
let tierId = "legacy-tier";
try {
const loadRes = await fetch(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoint, {
const loadRes = await fetchFirstOk(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoints, {
method: "POST",
headers,
body: JSON.stringify({ metadata }),
});
if (loadRes.ok) {
const data = await loadRes.json();
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
if (Array.isArray(data.allowedTiers)) {
for (const tier of data.allowedTiers) {
if (tier.isDefault && tier.id) {
tierId = tier.id.trim();
break;
}
const data = await loadRes.json();
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
if (Array.isArray(data.allowedTiers)) {
for (const tier of data.allowedTiers) {
if (tier.isDefault && tier.id) {
tierId = tier.id.trim();
break;
}
}
}
@@ -85,23 +97,19 @@ export const antigravity = {
if (projectId) {
try {
for (let i = 0; i < 10; i++) {
const onboardRes = await fetch(ANTIGRAVITY_CONFIG.onboardUserEndpoint, {
const onboardRes = await fetchFirstOk(ANTIGRAVITY_CONFIG.onboardUserEndpoints, {
method: "POST",
headers,
body: JSON.stringify({ tierId, metadata, cloudaicompanionProject: projectId }),
body: JSON.stringify({ tier_id: tierId, metadata }),
});
if (onboardRes.ok) {
const result = await onboardRes.json();
if (result.done === true) {
if (result.response?.cloudaicompanionProject) {
const respProject = result.response.cloudaicompanionProject;
projectId =
typeof respProject === "string"
? respProject.trim()
: respProject.id || projectId;
}
break;
const result = await onboardRes.json();
if (result.done === true) {
if (result.response?.cloudaicompanionProject) {
const respProject = result.response.cloudaicompanionProject;
projectId =
typeof respProject === "string" ? respProject.trim() : respProject.id || projectId;
}
break;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}

View File

@@ -99,20 +99,43 @@ export class AntigravityService {
return getAntigravityLoadCodeAssistMetadata();
}
private getEndpointList(key: string, fallbackKey: string) {
const endpoints = this.config[key];
if (Array.isArray(endpoints) && endpoints.length > 0) return endpoints;
const fallback = this.config[fallbackKey];
return typeof fallback === "string" && fallback ? [fallback] : [];
}
private async fetchFirstOk(endpoints: string[], init: RequestInit, label: string) {
let lastError: unknown = null;
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint, init);
if (response.ok) return response;
lastError = new Error(`${response.status} ${await response.text()}`);
} catch (error) {
lastError = error;
}
}
const message =
lastError instanceof Error ? lastError.message : String(lastError || "no endpoints");
throw new Error(`Failed to ${label}: ${message}`);
}
/**
* Fetch Project ID and Tier from loadCodeAssist API
*/
async loadCodeAssist(accessToken: string) {
const response = await fetch(this.config.loadCodeAssistEndpoint, {
method: "POST",
headers: this.getApiHeaders(accessToken),
body: JSON.stringify({ metadata: this.getMetadata() }),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to load code assist: ${errorText}`);
}
const response = await this.fetchFirstOk(
this.getEndpointList("loadCodeAssistEndpoints", "loadCodeAssistEndpoint"),
{
method: "POST",
headers: this.getApiHeaders(accessToken),
body: JSON.stringify({ metadata: this.getMetadata() }),
},
"load code assist"
);
const data = await response.json();
@@ -139,21 +162,19 @@ export class AntigravityService {
/**
* Onboard user to enable Gemini Code Assist for the project
*/
async onboardUser(accessToken: string, projectId: string, tierId: string) {
const response = await fetch(this.config.onboardUserEndpoint, {
method: "POST",
headers: this.getApiHeaders(accessToken),
body: JSON.stringify({
tierId,
metadata: this.getMetadata(),
cloudaicompanionProject: projectId,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to onboard user: ${errorText}`);
}
async onboardUser(accessToken: string, tierId: string) {
const response = await this.fetchFirstOk(
this.getEndpointList("onboardUserEndpoints", "onboardUserEndpoint"),
{
method: "POST",
headers: this.getApiHeaders(accessToken),
body: JSON.stringify({
tier_id: tierId,
metadata: this.getMetadata(),
}),
},
"onboard user"
);
return await response.json();
}
@@ -168,7 +189,7 @@ export class AntigravityService {
maxRetries = 10
) {
for (let i = 0; i < maxRetries; i++) {
const result = await this.onboardUser(accessToken, projectId, tierId);
const result = await this.onboardUser(accessToken, tierId);
if (result.done === true) {
// Extract final project ID from response

View File

@@ -45,13 +45,16 @@ describe("ensureAntigravityProjectAssigned", () => {
return new Response("Not Found", { status: 404 });
};
await ensureAntigravityProjectAssigned("fake-token-1", mockFetch);
const projectId = await ensureAntigravityProjectAssigned("fake-token-1", mockFetch);
const loadCalls = calls.filter((u) => u.endsWith(":loadCodeAssist"));
assert.ok(loadCalls.length >= 1, ":loadCodeAssist must be called at least once");
const cached = getAntigravityProjectFromCache("fake-token-1");
assert.equal(cached, "proj-from-bootstrap", "project id must be memoized after first call");
assert.equal(projectId, "proj-from-bootstrap", "project id must be returned");
assert.equal(
getAntigravityProjectFromCache("fake-token-1"),
"proj-from-bootstrap",
"project id must be memoized after first call"
);
});
test("subsequent calls for the same token skip the network", async () => {
@@ -149,7 +152,7 @@ describe("ensureAntigravityProjectAssigned", () => {
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
hitUrls.push(url);
if (url.includes("sandbox")) {
if (url.includes("daily-cloudcode-pa.googleapis.com")) {
// First URL fails
return new Response("not found", { status: 404 });
}
@@ -160,11 +163,15 @@ describe("ensureAntigravityProjectAssigned", () => {
});
};
await ensureAntigravityProjectAssigned("fallback-token", mockFetch);
const projectId = await ensureAntigravityProjectAssigned("fallback-token", mockFetch);
assert.ok(hitUrls.length >= 2, "should try at least two URLs on the first failure");
const cached = getAntigravityProjectFromCache("fallback-token");
assert.equal(cached, "proj-fallback", "should cache the project from the successful URL");
assert.equal(projectId, "proj-fallback", "should return the project from the successful URL");
assert.equal(
getAntigravityProjectFromCache("fallback-token"),
"proj-fallback",
"should cache the project from the successful URL"
);
});
test("getAntigravityLoadCodeAssistUrls returns URLs matching ANTIGRAVITY_BASE_URLS", () => {

View File

@@ -2,6 +2,8 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_PUBLIC_MODELS,
getClientVisibleAntigravityModelName,
isUserCallableAntigravityModelId,
resolveAntigravityModelId,
toClientAntigravityModelId,
@@ -9,8 +11,13 @@ import {
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.ts";
function getPublicModel(id: string) {
return ANTIGRAVITY_PUBLIC_MODELS.find((model) => model.id === id) as any;
}
test("resolveAntigravityModelId maps the documented Antigravity aliases to upstream IDs", () => {
assert.equal(resolveAntigravityModelId("gemini-3-pro-preview"), "gemini-3.1-pro-high");
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-preview"), "gemini-3-flash-agent");
assert.equal(resolveAntigravityModelId("gemini-3-flash-preview"), "gemini-3-flash");
assert.equal(resolveAntigravityModelId("gemini-3-pro-image-preview"), "gemini-3-pro-image");
assert.equal(
@@ -28,6 +35,7 @@ test("resolveAntigravityModelId maps the documented Antigravity aliases to upstr
test("toClientAntigravityModelId exposes client-visible aliases for known upstream IDs", () => {
assert.equal(toClientAntigravityModelId("gemini-3.1-pro-high"), "gemini-3-pro-preview");
assert.equal(toClientAntigravityModelId("gemini-3-flash-agent"), "gemini-3.5-flash-preview");
assert.equal(toClientAntigravityModelId("gemini-3-flash"), "gemini-3-flash-preview");
assert.equal(toClientAntigravityModelId("gpt-oss-120b-medium"), "gpt-oss-120b-medium");
assert.equal(toClientAntigravityModelId("claude-sonnet-4-6"), "claude-sonnet-4-6");
@@ -37,12 +45,67 @@ test("toClientAntigravityModelId exposes client-visible aliases for known upstre
test("isUserCallableAntigravityModelId only allows public chat-capable model IDs", () => {
assert.equal(isUserCallableAntigravityModelId("gemini-3-pro-preview"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro-high"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-preview"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3-flash-agent"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-low"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-pro"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-thinking"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-pro-agent"), true);
assert.equal(isUserCallableAntigravityModelId("claude-sonnet-4-6"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3-flash-agent"), false);
assert.equal(isUserCallableAntigravityModelId("tab_flash_lite_preview"), false);
assert.equal(isUserCallableAntigravityModelId("unknown-model"), false);
});
test("ANTIGRAVITY_PUBLIC_MODELS exposes captured Antigravity 2.0.1 names and capabilities", () => {
assert.deepEqual(getPublicModel("claude-opus-4-6-thinking"), {
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 250000,
maxOutputTokens: 64000,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
});
assert.deepEqual(getPublicModel("gemini-3.5-flash-preview"), {
id: "gemini-3.5-flash-preview",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
});
assert.equal(
getClientVisibleAntigravityModelName("gemini-3.5-flash-low"),
"Gemini 3.5 Flash (Low)"
);
assert.equal(getClientVisibleAntigravityModelName("gemini-2.5-flash"), "Gemini 2.5 Flash");
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-lite"),
"Gemini 2.5 Flash Lite"
);
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-thinking"),
"Gemini 2.5 Flash Thinking"
);
assert.deepEqual(getPublicModel("gpt-oss-120b-medium"), {
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B (Medium)",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: true,
toolCalling: true,
});
assert.equal(getPublicModel("gemini-3-pro-image-preview").contextLength, undefined);
assert.equal(
getPublicModel("gemini-2.5-computer-use-preview-10-2025").maxOutputTokens,
undefined
);
});
test("AntigravityExecutor.transformRequest resolves alias models before dispatching upstream", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
@@ -60,6 +123,23 @@ test("AntigravityExecutor.transformRequest resolves alias models before dispatch
assert.equal(result.model, "gemini-3.1-pro-high");
});
test("AntigravityExecutor.transformRequest resolves Gemini 3.5 Flash alias upstream", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-3.5-flash-preview",
{
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
},
true,
{ projectId: "project-1" }
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
assert.equal(result.model, "gemini-3-flash-agent");
});
test("AntigravityExecutor.transformRequest sends Claude through Gemini-compatible Cloud Code schema", async () => {
const executor = new AntigravityExecutor();
const bridged = openaiToAntigravityRequest(

View File

@@ -140,14 +140,19 @@ test("sidebar visibility excludes the removed auto-combo item", async () => {
(section) => section.id === "omni-proxy"
);
assert.equal(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS.includes("auto-combo"), false);
assert.equal(
(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("auto-combo"),
false
);
assert.ok(omniProxySection);
const items = sidebarVisibility.getSectionItems(omniProxySection);
assert.equal(
items.some((item) => item.id === "auto-combo"),
items.some((item) => (item.id as string) === "auto-combo"),
false
);
assert.deepEqual(sidebarVisibility.normalizeHiddenSidebarItems(["auto-combo", "home"]), ["home"]);
assert.deepEqual(sidebarVisibility.normalizeHiddenSidebarItems(["auto-combo" as any, "home"]), [
"home",
]);
});
test("intelligent routing helpers normalize config and build provider scores", () => {

View File

@@ -742,8 +742,9 @@ test("handleImageGeneration sends Antigravity image requests with native image_g
);
assert.equal(captured.headers.Authorization, "Bearer ag-token");
assert.equal(captured.headers["x-client-name"], "antigravity");
assert.equal(captured.headers["x-goog-user-project"], "project-123");
assert.ok(captured.headers["User-Agent"].startsWith("Antigravity/"));
assert.equal(captured.headers["x-goog-user-project"], undefined);
assert.match(captured.headers["User-Agent"], /^Antigravity\//);
assert.equal(captured.headers["x-goog-api-client"], undefined);
assert.equal(captured.body.project, "project-123");
assert.match(captured.body.requestId, /^image_gen\//);
assert.equal(captured.body.model, "gemini-3.1-flash-image");
@@ -789,7 +790,7 @@ test("handleImageGeneration rejects Antigravity image requests without projectId
}
});
test("handleImageGeneration retries Antigravity image requests without billing project on 403", async () => {
test("handleImageGeneration sends Antigravity image requests without billing project header", async () => {
const originalFetch = globalThis.fetch;
const calls = [];
@@ -800,19 +801,6 @@ test("handleImageGeneration retries Antigravity image requests without billing p
body: JSON.parse(String(options.body || "{}")),
});
if (calls.length === 1) {
return new Response(
JSON.stringify({
error: {
code: 403,
message: "Cloud Code Private API has not been used in project project-123 before.",
status: "PERMISSION_DENIED",
},
}),
{ status: 403, headers: { "content-type": "application/json" } }
);
}
return new Response(
JSON.stringify({
response: {
@@ -846,10 +834,9 @@ test("handleImageGeneration retries Antigravity image requests without billing p
});
assert.equal(result.success, true);
assert.equal(calls.length, 2);
assert.equal(calls[0].headers["x-goog-user-project"], "project-123");
assert.equal(calls[1].headers["x-goog-user-project"], undefined);
assert.equal(calls[1].body.project, "project-123");
assert.equal(calls.length, 1);
assert.equal(calls[0].headers["x-goog-user-project"], undefined);
assert.equal(calls[0].body.project, "project-123");
assert.deepEqual(result.data.data, [
{ b64_json: "YmFzZTY0LXJldHJ5", revised_prompt: "painted forest" },
]);

View File

@@ -17,6 +17,7 @@ Object.assign(process.env, {
const providersModule = await import("../../src/lib/oauth/providers/index.ts");
const oauthModule = await import("../../src/lib/oauth/constants/oauth.ts");
const registryModule = await import("../../open-sse/config/providerRegistry.ts");
const antigravityHeadersModule = await import("../../open-sse/services/antigravityHeaders.ts");
const PROVIDERS = providersModule.default;
const {
@@ -38,6 +39,7 @@ const {
WINDSURF_CONFIG,
} = oauthModule;
const { REGISTRY } = registryModule;
const { getAntigravityLoadCodeAssistMetadata } = antigravityHeadersModule;
const originalFetch = globalThis.fetch;
@@ -432,24 +434,32 @@ test("Gemini and Antigravity run mocked browser OAuth exchanges and post-exchang
jsonResponse({ cloudaicompanionProject: { id: "gemini-project" } }),
jsonResponse({ access_token: "anti-access", refresh_token: "anti-refresh", expires_in: 7200 }),
jsonResponse({ email: "anti@example.com" }),
(_url, init = {}) => {
(_url, init: any = {}) => {
assert.equal(init.method, "POST");
assert.equal(init.headers.Authorization, "Bearer anti-access");
assert.match(init.headers["User-Agent"], /^vscode\/1\.X\.X \(Antigravity\//);
assert.equal(init.headers["X-Goog-Api-Client"], undefined);
assert.equal(init.headers["Client-Metadata"], undefined);
assert.deepEqual(JSON.parse(String(init.body)).metadata, { ideType: "ANTIGRAVITY" });
assert.deepEqual(
JSON.parse(String(init.body)).metadata,
getAntigravityLoadCodeAssistMetadata()
);
assert.equal(JSON.parse(String(init.body)).cloudaicompanionProject, undefined);
return jsonResponse({
cloudaicompanionProject: { id: "anti-project" },
allowedTiers: [{ id: "tier-default", isDefault: true }],
});
},
(_url, init = {}) => {
(_url, init: any = {}) => {
assert.equal(init.method, "POST");
assert.equal(init.headers.Authorization, "Bearer anti-access");
assert.match(init.headers["User-Agent"], /^vscode\/1\.X\.X \(Antigravity\//);
assert.equal(init.headers["X-Goog-Api-Client"], undefined);
assert.deepEqual(JSON.parse(String(init.body)).metadata, { ideType: "ANTIGRAVITY" });
assert.deepEqual(
JSON.parse(String(init.body)).metadata,
getAntigravityLoadCodeAssistMetadata()
);
assert.equal(JSON.parse(String(init.body)).tier_id, "tier-default");
assert.equal(JSON.parse(String(init.body)).cloudaicompanionProject, undefined);
return jsonResponse({
done: true,
response: { cloudaicompanionProject: { id: "anti-project-final" } },

View File

@@ -355,7 +355,7 @@ test("provider models route returns the local catalog for embedding and rerank p
assert.equal(voyageBody.provider, "voyage-ai");
assert.equal(voyageBody.source, "local_catalog");
assert.ok(voyageBody.models.some((model) => model.id === "voyage-4-large"));
assert.ok(voyageBody.models.some((model) => model.id === "voyage-3-large"));
assert.ok(voyageBody.models.some((model) => model.id === "voyage-code-3"));
assert.equal(jinaResponse.status, 200);
assert.equal(jinaBody.provider, "jina-ai");
@@ -663,7 +663,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
assert.equal(init.method, "POST");
assert.equal(init.headers.Authorization, "Bearer ag-access");
assert.match(init.headers["User-Agent"], /^Antigravity\//);
assert.match(init.headers["User-Agent"], /^Antigravity\/1\.22\.2 /);
assert.equal(init.headers["x-goog-api-client"], undefined);
return Response.json({
models: [{ id: "gemini-3-flash", displayName: "Gemini 3 Flash" }],
});
@@ -683,8 +684,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
assert.equal(response.status, 200);
assert.equal(body.source, "api");
assert.deepEqual(discoveryUrls, [
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels",
"https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
"https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
]);
assert.deepEqual(body.models, [{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }]);
});
@@ -710,9 +711,9 @@ test("provider models route falls back through all Antigravity discovery endpoin
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /local catalog/i);
assert.deepEqual(discoveryUrls, [
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
"https://daily-cloudcode-pa.googleapis.com/v1internal:models",
"https://cloudcode-pa.googleapis.com/v1internal:models",
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
]);
assert.ok(body.models.some((model) => model.id === "gemini-3-pro-preview"));
});

View File

@@ -36,7 +36,7 @@ test.afterEach(() => {
comboMetrics.resetAllComboMetrics();
});
test("gemini thought signature store handles invalid input, TTL expiry and max-size pruning", () => {
test("gemini thought signature store handles invalid input, memory TTL and max-size pruning", () => {
signatureStore.storeGeminiThoughtSignature("", "sig");
signatureStore.storeGeminiThoughtSignature("call-empty", "");
assert.equal(signatureStore.getGeminiThoughtSignature(""), null);
@@ -49,7 +49,8 @@ test("gemini thought signature store handles invalid input, TTL expiry and max-s
assert.equal(signatureStore.getGeminiThoughtSignature("call-live"), "sig-live");
now += 60 * 60 * 1000 + 1;
assert.equal(signatureStore.getGeminiThoughtSignature("call-live"), null);
assert.equal(signatureStore.getGeminiThoughtSignatureMemorySizeForTests(), 0);
assert.equal(signatureStore.getGeminiThoughtSignature("call-live"), "sig-live");
now = 10_000;
for (let index = 0; index < 1002; index++) {
@@ -57,7 +58,8 @@ test("gemini thought signature store handles invalid input, TTL expiry and max-s
now += 1;
}
assert.equal(signatureStore.getGeminiThoughtSignature("call-0"), null);
assert.equal(signatureStore.getGeminiThoughtSignatureMemorySizeForTests(), 1000);
assert.equal(signatureStore.getGeminiThoughtSignature("call-0"), "sig-0");
assert.equal(signatureStore.getGeminiThoughtSignature("call-1001"), "sig-1001");
});

View File

@@ -2,7 +2,9 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
clearGeminiThoughtSignatureMemoryForTests,
clearGeminiThoughtSignatures,
getGeminiThoughtSignature,
getGeminiThoughtSignatureMode,
isValidBasicGeminiThoughtSignature,
isValidFullGeminiThoughtSignature,
@@ -65,3 +67,33 @@ test("bypass-strict mode requires the full protobuf structure before accepting a
assert.equal(resolveGeminiThoughtSignature("call-1", BASIC_VALID_SIGNATURE), "stored-signature");
assert.equal(resolveGeminiThoughtSignature("call-1", INVALID_SIGNATURE), "stored-signature");
});
test("signatures persist to sqlite database and survive in-memory cache clearing", () => {
storeGeminiThoughtSignature("db-call-1", "sig-12345");
// Bypass in-memory cache to verify DB lookup
clearGeminiThoughtSignatureMemoryForTests();
assert.equal(getGeminiThoughtSignature("db-call-1"), "sig-12345");
});
test("enabled mode keeps ignoring client signatures after sqlite recovery", () => {
storeGeminiThoughtSignature("db-call-2", "stored-db-signature");
clearGeminiThoughtSignatureMemoryForTests();
setGeminiThoughtSignatureMode("enabled");
assert.equal(
resolveGeminiThoughtSignature("db-call-2", STRICT_VALID_SIGNATURE),
"stored-db-signature"
);
});
test("persisted signatures stay isolated when callers namespace tool call ids", () => {
storeGeminiThoughtSignature("conn-a:call-shared", "signature-a");
storeGeminiThoughtSignature("conn-b:call-shared", "signature-b");
clearGeminiThoughtSignatureMemoryForTests();
assert.equal(resolveGeminiThoughtSignature("conn-a:call-shared"), "signature-a");
assert.equal(resolveGeminiThoughtSignature("conn-b:call-shared"), "signature-b");
assert.equal(resolveGeminiThoughtSignature("call-shared"), null);
});

View File

@@ -59,7 +59,7 @@ test("createDisconnectAwareStream converts upstream errors into SSE error chunks
});
test("createDisconnectAwareStream emits Responses API failure events for Responses clients", async () => {
const upstreamError = Object.assign(new Error("responses stream died"), { statusCode: 503 });
const upstreamError = Object.assign(new Error("responses stream\ndied"), { statusCode: 503 });
const transformStream = {
readable: new ReadableStream({
start(controller) {
@@ -83,7 +83,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
assert.match(text, /event: response\.failed/);
assert.match(text, /"type":"response\.failed"/);
assert.match(text, /"message":"responses stream died"/);
assert.match(text, /"message":"responses stream\\ndied"/);
assert.match(text, /"type":"server_error"/);
assert.match(text, /"code":"server_error"/);
assert.doesNotMatch(text, /chat\.completion\.chunk/);
@@ -91,6 +91,37 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
assert.doesNotMatch(text, /\[DONE\]/);
});
test("createDisconnectAwareStream keeps newlines escaped inside SSE data fields", async () => {
const upstreamError = Object.assign(new Error("line one\nline two\rline three"), {
statusCode: 400,
});
const transformStream = {
readable: new ReadableStream({
start(controller) {
controller.error(upstreamError);
},
}),
writable: {
getWriter() {
return {
abort() {},
};
},
},
};
const stream = createDisconnectAwareStream(
transformStream,
createStreamController({ clientResponseFormat: FORMATS.OPENAI_RESPONSES })
);
const text = await readStreamText(stream);
assert.match(text, /^event: response\.failed\ndata: \{"type":"response\.failed"/);
assert.match(text, /"message":"line one\\nline two\\rline three"/);
assert.doesNotMatch(text, /^line two/m);
assert.doesNotMatch(text, /^line three/m);
});
test("createDisconnectAwareStream treats legacy OpenAI response format alias as Responses", async () => {
const upstreamError = Object.assign(new Error("legacy responses alias died"), {
statusCode: 429,
@@ -156,6 +187,36 @@ test("createDisconnectAwareStream emits Claude SSE errors for Claude clients", a
assert.doesNotMatch(text, /\[DONE\]/);
});
test("createDisconnectAwareStream keeps newlines escaped for Claude SSE errors", async () => {
const upstreamError = Object.assign(new Error("claude line one\nclaude line two"), {
statusCode: 502,
});
const transformStream = {
readable: new ReadableStream({
start(controller) {
controller.error(upstreamError);
},
}),
writable: {
getWriter() {
return {
abort() {},
};
},
},
};
const stream = createDisconnectAwareStream(
transformStream,
createStreamController({ clientResponseFormat: FORMATS.CLAUDE })
);
const text = await readStreamText(stream);
assert.match(text, /^event: error\ndata: \{"type":"error"/);
assert.match(text, /"message":"claude line one\\nclaude line two"/);
assert.doesNotMatch(text, /^claude line two/m);
});
test("createDisconnectAwareStream cancel propagates disconnect reason and aborts the writer", async () => {
let aborted = false;
let disconnectEvent = null;

View File

@@ -6,11 +6,12 @@ const { antigravityUserAgent } = await import("../../open-sse/services/antigravi
const { geminiCliUserAgent, GEMINI_CLI_VERSION } =
await import("../../open-sse/services/geminiCliHeaders.ts");
test("T20: antigravity config has updated User-Agent and sandbox fallback URL", () => {
test("T20: antigravity config has updated User-Agent and daily Cloud Code first URL", () => {
const antigravity = REGISTRY.antigravity;
assert.ok(Array.isArray(antigravity.baseUrls));
assert.equal(antigravity.baseUrls[0], "https://daily-cloudcode-pa.sandbox.googleapis.com");
assert.equal(antigravity.baseUrls[0], "https://daily-cloudcode-pa.googleapis.com");
assert.equal(antigravity.headers["User-Agent"], antigravityUserAgent());
assert.match(antigravity.headers["User-Agent"], /^Antigravity\//);
});
test("T20: gemini CLI fingerprint uses the current CLI version and normalizes darwin to macos", () => {

View File

@@ -620,6 +620,75 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
});
});
test("OpenAI -> Antigravity Gemini stringifies signature-less historical tool calls", () => {
const result = openaiToAntigravityRequest(
"gemini-3.5-flash-low",
{
messages: [
{ role: "user", content: "Update todo" },
{
role: "assistant",
tool_calls: [
{
id: "call_synthetic_1",
type: "function",
function: { name: "default_api:todowrite_ide", arguments: '{"todos":[]}' },
},
],
},
{
role: "tool",
tool_call_id: "call_synthetic_1",
content: "[]",
},
],
tools: [
{
type: "function",
function: {
name: "default_api:todowrite_ide",
parameters: { type: "object", properties: {} },
},
},
],
},
false,
{ projectId: "proj-antigravity-gemini" } as any
);
const modelTurn = result.request.contents.find((content) => content.role === "model");
assert.ok(modelTurn, "expected a model turn");
assert.ok(
modelTurn.parts.some(
(part) =>
typeof part.text === "string" &&
part.text.includes("[Tool call: default_api:todowrite_ide]")
),
"expected signature-less tool call to be preserved as text"
);
assert.equal(
modelTurn.parts.some((part) => part.functionCall),
false,
"signature-less historical call must not be emitted as native functionCall"
);
const toolTurn = result.request.contents.find(
(content) =>
content.role === "user" &&
content.parts.some(
(part) =>
typeof part.text === "string" &&
part.text.includes("[Tool response: default_api:todowrite_ide]")
)
);
assert.ok(toolTurn, "expected signature-less tool response to be preserved as text");
assert.equal(
toolTurn.parts.some((part) => part.functionResponse),
false,
"signature-less historical response must not be emitted as native functionResponse"
);
});
test("OpenAI -> Antigravity maps Claude-family models to Gemini-compatible schema", () => {
const result = openaiToAntigravityRequest(
"claude-3-7-sonnet",