mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
feat: align Antigravity 2.0.1 support (#2443)
Integrated into release/v3.8.1
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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" },
|
||||
]);
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user