feat(antigravity): add client model aliases and signature bypass modes

Expose client-visible Antigravity preview model aliases while resolving
them back to upstream IDs for execution and provider discovery. Refresh
the Antigravity user agent from cached latest release metadata so model
discovery and requests track current CLI versions more reliably.

Add configurable Gemini thought signature cache modes in settings to
allow validated client-provided signatures in bypass flows while
preserving the existing stored-signature behavior by default.

Also centralize Anthropics header/version constants, enrich image model
catalog metadata with input and output modalities, add dashboard image
input support for advanced image providers, and exclude task docs from
Next standalone tracing to keep isolated builds stable.
This commit is contained in:
diegosouzapw
2026-04-16 20:53:35 -03:00
parent ce8e9b96ca
commit 7b51ccd9e4
39 changed files with 1129 additions and 130 deletions

View File

@@ -14,6 +14,11 @@ const nextConfig = {
},
},
output: "standalone",
outputFileTracingExcludes: {
// Planning/task docs are not runtime assets and can break standalone copies
// when broad fs/path tracing pulls the whole repository into the NFT graph.
"/*": ["./_tasks/**/*"],
},
serverExternalPackages: [
"pino",
"pino-pretty",

View File

@@ -0,0 +1,32 @@
export const ANTHROPIC_VERSION_HEADER = "2023-06-01";
const ANTHROPIC_BETA_BASE = Object.freeze([
"claude-code-20250219",
"oauth-2025-04-20",
"interleaved-thinking-2025-05-14",
"context-management-2025-06-27",
"prompt-caching-scope-2026-01-05",
"advanced-tool-use-2025-11-20",
"effort-2025-11-24",
"structured-outputs-2025-12-15",
"fast-mode-2026-02-01",
"redact-thinking-2026-02-12",
"token-efficient-tools-2026-03-28",
]);
const CLAUDE_OAUTH_EXTRA_BETAS = Object.freeze(["fine-grained-tool-streaming-2025-05-14"]);
export const ANTHROPIC_BETA_FULL = ANTHROPIC_BETA_BASE.join(",");
export const ANTHROPIC_BETA_API_KEY = ANTHROPIC_BETA_BASE.filter(
(beta) => beta !== "oauth-2025-04-20"
).join(",");
export const ANTHROPIC_BETA_CLAUDE_OAUTH = [
...ANTHROPIC_BETA_BASE.slice(0, 3),
...CLAUDE_OAUTH_EXTRA_BETAS,
...ANTHROPIC_BETA_BASE.slice(3),
].join(",");
export const CLAUDE_CLI_VERSION = "2.1.92";
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.80.0";
export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.14.0";

View File

@@ -0,0 +1,48 @@
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
"gemini-3-pro-preview": "gemini-3.1-pro-high",
"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",
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking",
});
export const ANTIGRAVITY_REVERSE_MODEL_ALIASES = Object.freeze(
Object.entries(ANTIGRAVITY_MODEL_ALIASES).reduce<Record<string, string>>(
(acc, [alias, target]) => {
if (!acc[target]) {
acc[target] = alias;
}
return acc;
},
{}
)
);
const CLIENT_VISIBLE_MODEL_NAMES = Object.freeze({
"gemini-3-pro-preview": "Gemini 3 Pro Preview",
"gemini-3-flash-preview": "Gemini 3 Flash Preview",
"gemini-3-pro-image-preview": "Gemini 3 Pro Image Preview",
"gemini-2.5-computer-use-preview-10-2025": "Gemini 2.5 Computer Use Preview (10/2025)",
"gemini-claude-sonnet-4-5": "Claude Sonnet 4.5 (Gemini Route)",
"gemini-claude-sonnet-4-5-thinking": "Claude Sonnet 4.5 Thinking (Gemini Route)",
"gemini-claude-opus-4-5-thinking": "Claude Opus 4.5 Thinking (Gemini Route)",
});
export function resolveAntigravityModelId(modelId: string): string {
if (!modelId) return modelId;
return ANTIGRAVITY_MODEL_ALIASES[modelId] || modelId;
}
export function toClientAntigravityModelId(modelId: string): string {
if (!modelId) return modelId;
return ANTIGRAVITY_REVERSE_MODEL_ALIASES[modelId] || modelId;
}
export function getClientVisibleAntigravityModelName(
modelId: string,
fallbackName?: string
): string {
return CLIENT_VISIBLE_MODEL_NAMES[modelId] || fallbackName || modelId;
}

View File

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

View File

@@ -1,10 +1,12 @@
import { ANTHROPIC_BETA_API_KEY, ANTHROPIC_VERSION_HEADER } from "./anthropicHeaders.ts";
type JsonRecord = Record<string, unknown>;
export type GlmApiRegion = "international" | "china";
export const GLM_SHARED_HEADERS = Object.freeze({
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
});
export const GLM_SHARED_MODELS = Object.freeze([

View File

@@ -5,36 +5,71 @@
* Each provider has its own request format and endpoint.
*/
const IMAGE_MODEL_ALIASES = {
interface ImageModelEntry {
id: string;
name: string;
inputModalities?: string[];
description?: string;
}
interface ImageProviderConfig {
id: string;
baseUrl: string;
fallbackUrl?: string;
proUrl?: string;
statusUrl?: string;
alias?: string;
authType: string;
authHeader: string;
format: string;
models: ImageModelEntry[];
supportedSizes: string[];
}
interface ImageModelAliasEntry {
provider: string;
model: string;
name: string;
listInCatalog: boolean;
inputModalities?: string[];
description?: string;
}
const IMAGE_MODEL_ALIASES: Record<string, ImageModelAliasEntry> = {
"flux-kontext": {
provider: "pollinations",
model: "flux-kontext",
name: "FLUX Kontext",
listInCatalog: true,
inputModalities: ["text", "image"],
},
"flux-kontext-max": {
provider: "together",
model: "black-forest-labs/FLUX.1.1-pro",
name: "FLUX Kontext Max",
listInCatalog: true,
inputModalities: ["text", "image"],
},
"flux-redux": {
provider: "together",
model: "black-forest-labs/FLUX.1-redux",
name: "FLUX Redux",
listInCatalog: true,
inputModalities: ["text", "image"],
},
"flux-depth": {
provider: "together",
model: "black-forest-labs/FLUX.1-depth",
name: "FLUX Depth",
listInCatalog: true,
inputModalities: ["text", "image"],
},
"flux-canny": {
provider: "together",
model: "black-forest-labs/FLUX.1-canny",
name: "FLUX Canny",
listInCatalog: true,
inputModalities: ["text", "image"],
},
"flux-dev-lora": {
provider: "together",
@@ -67,7 +102,7 @@ function findImageModelConfig(providerId, modelId) {
return provider.models.find((model) => model.id === modelId) || null;
}
export const IMAGE_PROVIDERS = {
export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
openai: {
id: "openai",
baseUrl: "https://api.openai.com/v1/images/generations",
@@ -99,12 +134,32 @@ export const IMAGE_PROVIDERS = {
authHeader: "bearer",
format: "openai",
models: [
{ id: "black-forest-labs/FLUX.1.1-pro", name: "FLUX 1.1 Pro" },
{
id: "black-forest-labs/FLUX.1.1-pro",
name: "FLUX 1.1 Pro",
inputModalities: ["text", "image"],
description: "Advanced contextual image editing and image-to-image generation",
},
{ id: "black-forest-labs/FLUX.1-schnell-Free", name: "FLUX 1 Schnell (Free)" },
{ id: "stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0" },
{ id: "black-forest-labs/FLUX.1-redux", name: "FLUX.1 Redux" },
{ id: "black-forest-labs/FLUX.1-depth", name: "FLUX.1 Depth" },
{ id: "black-forest-labs/FLUX.1-canny", name: "FLUX.1 Canny" },
{
id: "black-forest-labs/FLUX.1-redux",
name: "FLUX.1 Redux",
inputModalities: ["text", "image"],
description: "Generate prompt-guided variations from an input image",
},
{
id: "black-forest-labs/FLUX.1-depth",
name: "FLUX.1 Depth",
inputModalities: ["text", "image"],
description: "Depth-conditioned image generation from an input image",
},
{
id: "black-forest-labs/FLUX.1-canny",
name: "FLUX.1 Canny",
inputModalities: ["text", "image"],
description: "Canny edge guided image generation from an input image",
},
{ id: "black-forest-labs/FLUX.1-dev-lora", name: "FLUX.1 Dev LoRA" },
],
supportedSizes: ["1024x1024", "512x512"],
@@ -231,8 +286,18 @@ export const IMAGE_PROVIDERS = {
{ id: "gptimage", name: "GPT Image 1 Mini" },
{ id: "qwen-image", name: "Qwen Image Plus" },
{ id: "wan-image", name: "Wan 2.7 Image" },
{ id: "flux-kontext", name: "FLUX.1 Kontext" },
{ id: "flux-kontext-max", name: "FLUX.1 Kontext Max" },
{
id: "flux-kontext",
name: "FLUX.1 Kontext",
inputModalities: ["text", "image"],
description: "Context-aware image editing with optional source image",
},
{
id: "flux-kontext-max",
name: "FLUX.1 Kontext Max",
inputModalities: ["text", "image"],
description: "Higher quality Kontext editing with optional source image",
},
{ id: "gptimage-large", name: "GPT Image 1.5" },
],
supportedSizes: ["1024x1024", "512x512"],
@@ -277,20 +342,24 @@ export const IMAGE_PROVIDERS = {
{ id: "sd3.5-medium", name: "sd3.5-medium" },
{ id: "stable-image-ultra", name: "Stable Image Ultra" },
{ id: "stable-image-core", name: "Stable Image Core" },
{ id: "inpaint", name: "Inpaint" },
{ id: "outpaint", name: "Outpaint" },
{ id: "erase", name: "Erase" },
{ id: "search-and-replace", name: "Search and Replace" },
{ id: "search-and-recolor", name: "Search and Recolor" },
{ id: "remove-background", name: "Remove Background" },
{ id: "replace-background-and-relight", name: "Replace Background and Relight" },
{ id: "fast", name: "Fast Upscale" },
{ id: "conservative", name: "Conservative Upscale" },
{ id: "creative", name: "Creative Upscale" },
{ id: "sketch", name: "Sketch Control" },
{ id: "structure", name: "Structure Control" },
{ id: "style", name: "Style Control" },
{ id: "style-transfer", name: "Style Transfer" },
{ id: "inpaint", name: "Inpaint", inputModalities: ["text", "image"] },
{ id: "outpaint", name: "Outpaint", inputModalities: ["text", "image"] },
{ id: "erase", name: "Erase", inputModalities: ["image"] },
{ id: "search-and-replace", name: "Search and Replace", inputModalities: ["text", "image"] },
{ id: "search-and-recolor", name: "Search and Recolor", inputModalities: ["text", "image"] },
{ id: "remove-background", name: "Remove Background", inputModalities: ["image"] },
{
id: "replace-background-and-relight",
name: "Replace Background and Relight",
inputModalities: ["text", "image"],
},
{ id: "fast", name: "Fast Upscale", inputModalities: ["image"] },
{ id: "conservative", name: "Conservative Upscale", inputModalities: ["image"] },
{ id: "creative", name: "Creative Upscale", inputModalities: ["text", "image"] },
{ id: "sketch", name: "Sketch Control", inputModalities: ["text", "image"] },
{ id: "structure", name: "Structure Control", inputModalities: ["text", "image"] },
{ id: "style", name: "Style Control", inputModalities: ["text", "image"] },
{ id: "style-transfer", name: "Style Transfer", inputModalities: ["text", "image"] },
],
supportedSizes: ["1024x1024", "1024x1280", "1280x1024"],
},
@@ -302,10 +371,14 @@ export const IMAGE_PROVIDERS = {
authHeader: "x-key",
format: "black-forest-labs",
models: [
{ id: "flux-kontext-pro", name: "flux-kontext-pro" },
{ id: "flux-kontext-max", name: "flux-kontext-max" },
{ id: "flux-pro-1.0-fill", name: "flux-pro-1.0-fill" },
{ id: "flux-pro-1.0-expand", name: "flux-pro-1.0-expand" },
{ id: "flux-kontext-pro", name: "flux-kontext-pro", inputModalities: ["text", "image"] },
{ id: "flux-kontext-max", name: "flux-kontext-max", inputModalities: ["text", "image"] },
{ id: "flux-pro-1.0-fill", name: "flux-pro-1.0-fill", inputModalities: ["text", "image"] },
{
id: "flux-pro-1.0-expand",
name: "flux-pro-1.0-expand",
inputModalities: ["text", "image"],
},
{ id: "flux-pro-1.1", name: "flux-pro-1.1" },
{ id: "flux-pro-1.1-ultra", name: "flux-pro-1.1-ultra" },
{ id: "flux-dev", name: "flux-dev" },
@@ -333,7 +406,7 @@ export const IMAGE_PROVIDERS = {
authType: "apikey",
authHeader: "x-api-key",
format: "topaz",
models: [{ id: "topaz-enhance", name: "topaz-enhance" }],
models: [{ id: "topaz-enhance", name: "topaz-enhance", inputModalities: ["image"] }],
supportedSizes: ["1024x1024"],
},
};
@@ -396,6 +469,8 @@ export function getAllImageModels() {
name: model.name,
provider: providerId,
supportedSizes: config.supportedSizes,
inputModalities: model.inputModalities || ["text"],
description: model.description || undefined,
});
}
}
@@ -408,6 +483,8 @@ export function getAllImageModels() {
name: target.name || modelConfig?.name || alias,
provider: target.provider,
supportedSizes: providerConfig?.supportedSizes || [],
inputModalities: target.inputModalities || modelConfig?.inputModalities || ["text"],
description: target.description || modelConfig?.description || undefined,
});
}
return models;

View File

@@ -8,6 +8,14 @@
import { platform, arch } from "os";
import { ANTIGRAVITY_BASE_URLS } from "./antigravityUpstream.ts";
import {
ANTHROPIC_BETA_API_KEY,
ANTHROPIC_BETA_CLAUDE_OAUTH,
ANTHROPIC_VERSION_HEADER,
CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
CLAUDE_CLI_USER_AGENT,
} from "./anthropicHeaders.ts";
import { getCodexDefaultHeaders } from "./codexClient.ts";
import {
GLMT_REQUEST_DEFAULTS,
@@ -108,8 +116,8 @@ const KIMI_CODING_SHARED = {
baseUrl: "https://api.kimi.com/coding/v1/messages",
authHeader: "x-api-key",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
{ id: "kimi-k2.5", name: "Kimi K2.5" },
@@ -255,16 +263,15 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authHeader: "x-api-key",
defaultContextLength: 200000,
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta":
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_CLAUDE_OAUTH,
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"User-Agent": "claude-cli/2.1.63 (external, cli)",
"User-Agent": CLAUDE_CLI_USER_AGENT,
"X-App": "cli",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Package-Version": "0.74.0",
"X-Stainless-Runtime-Version": CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
"X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
"X-Stainless-Runtime": "node",
"X-Stainless-Lang": "js",
"X-Stainless-Arch": mapStainlessArch(),
@@ -744,8 +751,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "x-api-key",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
@@ -769,8 +776,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "x-api-key",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
{ id: "glm-5", name: "GLM 5" },
@@ -901,8 +908,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "bearer",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
// T12/T28: MiniMax default upgraded from M2.5 to M2.7
@@ -925,8 +932,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "bearer",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
// Keep parity with minimax to ensure model discovery works for minimax-cn connections.

View File

@@ -1,8 +1,8 @@
import crypto, { randomUUID } from "crypto";
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";
import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts";
import { antigravityUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts";
import { antigravityUserAgent } from "../services/antigravityHeaders.ts";
import { classify429, decide429, type Decision } from "../services/antigravity429Engine.ts";
import {
injectCreditsField,
@@ -13,6 +13,8 @@ import {
} from "../services/antigravityCredits.ts";
import { persistCreditBalance, getAllPersistedCreditBalances } from "@/lib/db/creditBalance";
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
import { resolveAntigravityVersion } from "../services/antigravityVersion.ts";
import { resolveAntigravityModelId } from "../config/antigravityModelAliases.ts";
const MAX_RETRY_AFTER_MS = 60_000;
const LONG_RETRY_THRESHOLD_MS = 60_000;
@@ -91,6 +93,7 @@ function markCreditsExhausted(accountId: string): void {
function cleanModelName(model: string): string {
if (!model) return model;
let clean = model.includes("/") ? model.split("/").pop()! : model;
clean = resolveAntigravityModelId(clean);
// Normalize bare Pro IDs to the Low tier (matching OpenClaw convention).
// The upstream API requires an explicit tier suffix; bare IDs cause errors.
if (BARE_PRO_IDS.has(clean)) {
@@ -120,7 +123,6 @@ export class AntigravityExecutor extends BaseExecutor {
"Content-Type": "application/json",
Authorization: `Bearer ${credentials.accessToken}`,
"User-Agent": antigravityUserAgent(),
"X-Goog-Api-Client": googApiClientHeader(),
Accept: "text/event-stream",
"X-OmniRoute-Source": "omniroute",
};
@@ -442,7 +444,16 @@ export class AntigravityExecutor extends BaseExecutor {
return collect();
}
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
async execute({
model,
body,
stream,
credentials,
signal,
log,
upstreamExtraHeaders,
}: ExecuteInput) {
await resolveAntigravityVersion();
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastStatus = 0;
@@ -517,7 +528,7 @@ export class AntigravityExecutor extends BaseExecutor {
// signal — multi-hour Retry-After upgrades rate_limited to
// quota_exhausted so the GOOGLE_ONE_AI credits retry fires).
const effectiveRetryHintMs = retryMs ?? parsedRetryMs ?? null;
const category = classify429(errorMessage, effectiveRetryHintMs);
const category = classify429(errorMessage);
// 3. For quota_exhausted, attempt Google One AI credits retry FIRST!
// Skip if credits were already injected on the first call

View File

@@ -132,6 +132,7 @@ import {
isClaudeCodeCompatibleProvider,
resolveClaudeCodeCompatibleSessionId,
} from "../services/claudeCodeCompatible.ts";
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
function extractMemoryTextFromResponse(
response: Record<string, unknown> | null | undefined
@@ -858,6 +859,7 @@ export async function handleChatCore({
const stream = resolveStreamFlag(body?.stream, acceptHeader);
const settings = await getCachedSettings();
setGeminiThoughtSignatureMode(settings.antigravitySignatureCacheMode);
const semanticCacheEnabled = settings.semanticCacheEnabled !== false;
// Create request logger for this session: sourceFormat_targetFormat_model

View File

@@ -1,4 +1,9 @@
import os from "node:os";
import {
ANTIGRAVITY_FALLBACK_VERSION,
getCachedAntigravityVersion,
resolveAntigravityVersion,
} from "./antigravityVersion.ts";
/**
* Antigravity and Gemini CLI header utilities.
@@ -11,7 +16,7 @@ import os from "node:os";
type AntigravityHeaderProfile = "loadCodeAssist" | "fetchAvailableModels" | "models";
const ANTIGRAVITY_VERSION = "1.21.9";
const ANTIGRAVITY_VERSION = ANTIGRAVITY_FALLBACK_VERSION;
const GEMINI_CLI_VERSION = "0.31.0";
const GEMINI_SDK_VERSION = "1.41.0";
const NODE_VERSION = "v22.19.0";
@@ -68,7 +73,12 @@ function getArch(): string {
* darwin/arm64. Matches CLIProxyAPI's proven production behavior.
*/
export function antigravityUserAgent(): string {
return `antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`;
return `antigravity/${getCachedAntigravityVersion()} darwin/arm64`;
}
export async function resolveAntigravityUserAgent(): Promise<string> {
const version = await resolveAntigravityVersion();
return `antigravity/${version} darwin/arm64`;
}
export function getAntigravityLoadCodeAssistMetadata(): Record<string, string> {

View File

@@ -0,0 +1,141 @@
const ANTIGRAVITY_RELEASE_FEED_URL =
"https://antigravity-auto-updater-974169037036.us-central1.run.app/releases";
const ANTIGRAVITY_GITHUB_RELEASE_URL =
"https://api.github.com/repos/antigravityide/antigravity/releases/latest";
export const ANTIGRAVITY_VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
export const ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS = 5_000;
export const ANTIGRAVITY_FALLBACK_VERSION = "1.23.2";
type VersionCache = {
fetchedAt: number;
version: string;
};
type FetchLike = typeof fetch;
let versionCache: VersionCache | null = null;
let inFlightRequest: Promise<string> | null = null;
function normalizeVersion(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim().replace(/^v/i, "");
const match = trimmed.match(/^(\d+\.\d+\.\d+)\b/);
return match ? match[1] : null;
}
async function fetchJsonWithTimeout(fetchImpl: FetchLike, url: string): Promise<unknown> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS);
try {
const response = await fetchImpl(url, {
headers: {
Accept: "application/json",
"User-Agent": "OmniRoute-AntigravityVersion/1.0",
},
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`Version source ${url} returned ${response.status}`);
}
return response.json();
} finally {
clearTimeout(timeoutId);
}
}
function parseOfficialReleaseFeed(payload: unknown): string | null {
if (!Array.isArray(payload)) return null;
for (const entry of payload) {
const version = normalizeVersion((entry as { version?: unknown })?.version);
if (version) return version;
}
return null;
}
function parseGitHubRelease(payload: unknown): string | null {
if (!payload || typeof payload !== "object") return null;
const candidate =
(payload as { tag_name?: unknown }).tag_name ?? (payload as { name?: unknown }).name;
return normalizeVersion(candidate);
}
async function fetchLatestAntigravityVersion(fetchImpl: FetchLike): Promise<string | null> {
const sources = [
{
parse: parseOfficialReleaseFeed,
url: ANTIGRAVITY_RELEASE_FEED_URL,
},
{
parse: parseGitHubRelease,
url: ANTIGRAVITY_GITHUB_RELEASE_URL,
},
];
for (const source of sources) {
try {
const payload = await fetchJsonWithTimeout(fetchImpl, source.url);
const version = source.parse(payload);
if (version) return version;
} catch {
// Try the next source and fall back to the last known good version if all fail.
}
}
return null;
}
export async function resolveAntigravityVersion(fetchImpl: FetchLike = fetch): Promise<string> {
const now = Date.now();
if (versionCache && now - versionCache.fetchedAt < ANTIGRAVITY_VERSION_CACHE_TTL_MS) {
return versionCache.version;
}
if (inFlightRequest) {
return inFlightRequest;
}
inFlightRequest = (async () => {
const resolved = await fetchLatestAntigravityVersion(fetchImpl);
const version = resolved || versionCache?.version || ANTIGRAVITY_FALLBACK_VERSION;
if (resolved) {
versionCache = {
fetchedAt: Date.now(),
version,
};
}
return version;
})();
try {
return await inFlightRequest;
} finally {
inFlightRequest = null;
}
}
export function getCachedAntigravityVersion(): string {
return versionCache?.version || ANTIGRAVITY_FALLBACK_VERSION;
}
export function seedAntigravityVersionCache(version: string, fetchedAt = Date.now()): void {
versionCache = {
fetchedAt,
version,
};
}
export function clearAntigravityVersionCache(): void {
versionCache = null;
inFlightRequest = null;
}

View File

@@ -1,6 +1,14 @@
import { createHash, randomUUID } from "node:crypto";
import { getStainlessTimeoutSeconds } from "@/shared/utils/runtimeTimeouts";
import {
ANTHROPIC_BETA_FULL,
ANTHROPIC_VERSION_HEADER,
CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
CLAUDE_CLI_USER_AGENT,
CLAUDE_CLI_VERSION,
} from "../config/anthropicHeaders.ts";
import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts";
import { signRequestBody } from "./claudeCodeCCH.ts";
import { computeFingerprint, extractFirstUserMessageText } from "./claudeCodeFingerprint.ts";
@@ -26,11 +34,10 @@ export const CLAUDE_CODE_COMPATIBLE_PREFIX = "anthropic-compatible-cc-";
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH = "/models";
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS = 8092;
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = "2023-06-01";
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA =
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,token-efficient-tools-2025-02-19";
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.87";
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = `claude-cli/${CLAUDE_CODE_COMPATIBLE_VERSION} (external, cli)`;
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = ANTHROPIC_VERSION_HEADER;
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = ANTHROPIC_BETA_FULL;
export const CLAUDE_CODE_COMPATIBLE_VERSION = CLAUDE_CLI_VERSION;
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = CLAUDE_CLI_USER_AGENT;
/**
* Build the billing header dynamically with fingerprint and CCH placeholder.
* The cch=00000 placeholder is later replaced by signRequestBody().
@@ -136,11 +143,11 @@ export function buildClaudeCodeCompatibleHeaders(
"X-Stainless-Retry-Count": "0",
"X-Stainless-Timeout": String(CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS),
"X-Stainless-Lang": "js",
"X-Stainless-Package-Version": "0.80.0",
"X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
"X-Stainless-OS": "MacOS",
"X-Stainless-Arch": "arm64",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Runtime-Version": CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
"accept-language": "*",
"sec-fetch-mode": "cors",
"accept-encoding": "identity",

View File

@@ -1,12 +1,15 @@
const MAX_SIGNATURES = 1000;
const TTL_MS = 1000 * 60 * 60;
export type SignatureCacheMode = "enabled" | "bypass" | "bypass-strict";
type Entry = {
signature: string;
expiresAt: number;
};
const signatures = new Map<string, Entry>();
let signatureCacheMode: SignatureCacheMode = "enabled";
function pruneExpired() {
const now = Date.now();
@@ -42,3 +45,120 @@ export function getGeminiThoughtSignature(toolCallId: unknown) {
if (!entry) return null;
return entry.signature;
}
export function normalizeSignatureCacheMode(value: unknown): SignatureCacheMode {
return value === "bypass" || value === "bypass-strict" ? value : "enabled";
}
export function setGeminiThoughtSignatureMode(mode: unknown) {
signatureCacheMode = normalizeSignatureCacheMode(mode);
}
export function getGeminiThoughtSignatureMode(): SignatureCacheMode {
return signatureCacheMode;
}
function decodeSignature(signature: string): Buffer | null {
if (!signature || (signature[0] !== "R" && signature[0] !== "E")) return null;
const payload = signature.slice(1);
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 === 1) {
return null;
}
try {
const decoded = Buffer.from(payload, "base64");
if (decoded.length === 0) return null;
const canonical = decoded.toString("base64").replace(/=+$/g, "");
if (canonical !== payload.replace(/=+$/g, "")) {
return null;
}
return decoded;
} catch {
return null;
}
}
function readVarint(
buffer: Buffer,
startOffset: number
): { nextOffset: number; value: number } | null {
let offset = startOffset;
let result = 0;
let shift = 0;
while (offset < buffer.length && shift < 35) {
const byte = buffer[offset];
result |= (byte & 0x7f) << shift;
offset += 1;
if ((byte & 0x80) === 0) {
return { nextOffset: offset, value: result };
}
shift += 7;
}
return null;
}
export function isValidBasicGeminiThoughtSignature(signature: unknown): boolean {
if (typeof signature !== "string") return false;
const decoded = decodeSignature(signature);
return Boolean(decoded && decoded[0] === 0x12);
}
export function isValidFullGeminiThoughtSignature(signature: unknown): boolean {
if (typeof signature !== "string") return false;
const decoded = decodeSignature(signature);
if (!decoded || decoded[0] !== 0x12) return false;
const outerLength = readVarint(decoded, 1);
if (!outerLength) return false;
const outerEnd = outerLength.nextOffset + outerLength.value;
if (outerEnd !== decoded.length) return false;
const inner = decoded.subarray(outerLength.nextOffset, outerEnd);
if (inner.length === 0 || inner[0] !== 0x0a) return false;
const innerLength = readVarint(inner, 1);
if (!innerLength) return false;
return innerLength.nextOffset + innerLength.value === inner.length;
}
export function resolveGeminiThoughtSignature(
toolCallId: unknown,
clientSignature?: unknown
): string | null {
const persisted = getGeminiThoughtSignature(toolCallId);
if (typeof clientSignature !== "string" || clientSignature.length === 0) {
return persisted;
}
if (signatureCacheMode === "enabled") {
return persisted;
}
const isValid =
signatureCacheMode === "bypass-strict"
? isValidFullGeminiThoughtSignature(clientSignature)
: isValidBasicGeminiThoughtSignature(clientSignature);
if (isValid) {
return clientSignature;
}
console.warn(
`[signature-cache] ${signatureCacheMode}: invalid client thought signature, falling back`
);
return persisted;
}
export function clearGeminiThoughtSignatures() {
signatures.clear();
signatureCacheMode = "enabled";
}

View File

@@ -1,4 +1,5 @@
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts";
import { ANTIGRAVITY_MODEL_ALIASES } from "../config/antigravityModelAliases.ts";
import { resolveWildcardAlias } from "./wildcardRouter.ts";
// Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS)
@@ -36,7 +37,7 @@ const PROVIDER_MODEL_ALIASES = {
"gpt-oss-120b": "openai/gpt-oss-120b",
"nvidia/gpt-oss-120b": "openai/gpt-oss-120b",
},
antigravity: {},
antigravity: ANTIGRAVITY_MODEL_ALIASES,
};
const CROSS_PROXY_MODEL_ALIASES = {

View File

@@ -2,7 +2,7 @@ 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 { getGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts";
import { resolveGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.ts";
import {
capMaxOutputTokens,
@@ -98,6 +98,18 @@ function buildChangedToolNameMap(toolNameMap: Map<string, string>): Map<string,
return changedEntries.length > 0 ? new Map(changedEntries) : null;
}
function extractClientThoughtSignature(toolCall) {
if (!toolCall || typeof toolCall !== "object") return null;
return (
toolCall.thoughtSignature ||
toolCall.thought_signature ||
toolCall.function?.thoughtSignature ||
toolCall.function?.thought_signature ||
null
);
}
// Core: Convert OpenAI request to Gemini format (base for all variants)
function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolNameOptions = {}) {
const result: GeminiRequest = {
@@ -210,7 +222,7 @@ 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) => getGeminiThoughtSignature(tc.id))
.map((tc) => resolveGeminiThoughtSignature(tc.id, extractClientThoughtSignature(tc)))
.find((signature) => typeof signature === "string" && signature.length > 0);
const shouldUseEmbeddedSignature = !parts.some((p) => p.thoughtSignature);
@@ -219,7 +231,10 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
if (tc.type !== "function") continue;
const args = tryParseJSON(tc.function?.arguments || "{}");
const signatureForToolCall = getGeminiThoughtSignature(tc.id);
const signatureForToolCall = resolveGeminiThoughtSignature(
tc.id,
extractClientThoughtSignature(tc)
);
const embeddedThoughtSignature = shouldUseEmbeddedSignature
? firstPersistedSignature || signatureForToolCall || DEFAULT_THINKING_GEMINI_SIGNATURE
: undefined;

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { pathToFileURL } from "node:url";
@@ -13,8 +14,19 @@ import { pathToFileURL } from "node:url";
*/
const projectRoot = process.cwd();
const legacyAppDir = path.join(projectRoot, "app");
const backupDir = path.join(projectRoot, `.app-build-backup-${process.pid}-${Date.now()}`);
const backupRoot = path.join(os.tmpdir(), `omniroute-build-isolated-${process.pid}-${Date.now()}`);
const transientBuildPaths = [
{
label: "legacy app snapshot",
sourcePath: path.join(projectRoot, "app"),
backupPath: path.join(backupRoot, "app"),
},
{
label: "task planning workspace",
sourcePath: path.join(projectRoot, "_tasks"),
backupPath: path.join(backupRoot, "_tasks"),
},
];
async function exists(targetPath) {
try {
@@ -26,6 +38,8 @@ async function exists(targetPath) {
}
export async function movePath(sourcePath, destinationPath, fsImpl = fs) {
await fsImpl.mkdir(path.dirname(destinationPath), { recursive: true });
try {
await fsImpl.rename(sourcePath, destinationPath);
} catch (error) {
@@ -82,12 +96,13 @@ export function resolveNextBuildEnv(baseEnv = process.env) {
}
export async function main() {
let moved = false;
const movedPaths = [];
try {
if (await exists(legacyAppDir)) {
await movePath(legacyAppDir, backupDir);
moved = true;
for (const entry of transientBuildPaths) {
if (!(await exists(entry.sourcePath))) continue;
await movePath(entry.sourcePath, entry.backupPath);
movedPaths.push(entry);
}
const result = await runNextBuild();
@@ -113,17 +128,25 @@ export async function main() {
console.error("[build-next-isolated] Build failed:", error);
process.exitCode = 1;
} finally {
if (moved) {
while (movedPaths.length > 0) {
const entry = movedPaths.pop();
if (!entry) continue;
try {
await movePath(backupDir, legacyAppDir);
await movePath(entry.backupPath, entry.sourcePath);
} catch (restoreError) {
console.error(
`[build-next-isolated] Failed to restore legacy app dir from ${backupDir}:`,
`[build-next-isolated] Failed to restore ${entry.label} from ${entry.backupPath}:`,
restoreError
);
process.exitCode = 1;
}
}
try {
await fs.rm(backupRoot, { recursive: true, force: true });
} catch (cleanupError) {
console.warn("[build-next-isolated] Failed to clean temporary backup root:", cleanupError);
}
}
}

View File

@@ -311,6 +311,21 @@ function formatFileSize(bytes: number): string {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
function fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
return;
}
reject(new Error("Failed to read file"));
};
reader.onerror = () => reject(reader.error || new Error("Failed to read file"));
reader.readAsDataURL(file);
});
}
/** Render image result thumbnails */
function ImageResults({ data }: { data: any }) {
const images: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> =
@@ -383,6 +398,8 @@ export default function MediaPageClient() {
const MAX_TRANSCRIPTION_FILE_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
const [audioFile, setAudioFile] = useState<File | null>(null);
const [fileSizeError, setFileSizeError] = useState<string | null>(null);
const [imageInputFile, setImageInputFile] = useState<File | null>(null);
const [imageMaskFile, setImageMaskFile] = useState<File | null>(null);
// Fix #390: Track which local providers (sdwebui, comfyui) are actually configured
// so we can hide them when they haven't been set up in the providers page
@@ -433,6 +450,8 @@ export default function MediaPageClient() {
setError(null);
setIsCredentialsError(false);
setAudioFile(null);
setImageInputFile(null);
setImageMaskFile(null);
// Pick first provider and first model automatically
const providers = PROVIDER_MODELS[tab] ?? [];
const firstProvider = providers[0];
@@ -473,9 +492,10 @@ export default function MediaPageClient() {
try {
const config = MODALITY_CONFIG[activeTab];
const modelId = selectedModel;
const promptValue = prompt.trim();
if (activeTab === "speech") {
if (!prompt.trim()) {
if (!promptValue) {
setError("Please enter text to synthesize.");
setLoading(false);
return;
@@ -485,7 +505,7 @@ export default function MediaPageClient() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: modelId,
input: prompt.trim(),
input: promptValue,
voice: speechVoice,
response_format: speechFormat,
}),
@@ -549,19 +569,44 @@ export default function MediaPageClient() {
return;
}
if (!prompt.trim()) {
setError("Please enter a prompt.");
if (activeTab === "image" && selectedProvider === "topaz" && !imageInputFile) {
setError("Topaz requires an input image.");
setLoading(false);
return;
}
if (!prompt.trim()) {
if (activeTab !== "image" || selectedProvider !== "topaz") {
setError("Please enter a prompt.");
setLoading(false);
return;
}
}
const payload: Record<string, unknown> = {
model: modelId,
prompt:
promptValue ||
(activeTab === "image" && selectedProvider === "topaz" ? "Enhance this image" : ""),
...(activeTab === "image" ? { size: "1024x1024", n: 1 } : {}),
};
if (activeTab === "image" && imageInputFile) {
const imageDataUrl = await fileToDataUrl(imageInputFile);
payload.image_url = imageDataUrl;
payload.imageUrls = [imageDataUrl];
}
if (activeTab === "image" && imageMaskFile) {
const maskDataUrl = await fileToDataUrl(imageMaskFile);
payload.mask = maskDataUrl;
payload.mask_url = maskDataUrl;
}
const res = await fetch(config.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: modelId,
prompt: prompt.trim(),
...(activeTab === "image" ? { size: "1024x1024", n: 1 } : {}),
}),
body: JSON.stringify(payload),
});
if (!res.ok) {
const raw = await res.json().catch(() => ({}));
@@ -579,6 +624,16 @@ export default function MediaPageClient() {
const config = MODALITY_CONFIG[activeTab];
const voiceList = getVoiceList(selectedProvider);
const isTopazImageFlow = activeTab === "image" && selectedProvider === "topaz";
const isGenerateDisabled =
loading ||
(activeTab === "transcription"
? !audioFile
: activeTab === "image"
? isTopazImageFlow
? !imageInputFile
: !prompt.trim()
: !prompt.trim());
return (
<div className="space-y-6">
@@ -735,27 +790,80 @@ export default function MediaPageClient() {
</p>
</div>
) : (
/* Prompt / Text */
<div>
<label className="block text-sm font-medium text-text-main mb-2">
{activeTab === "speech" ? "Text" : t("prompt")}
</label>
<textarea
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder={config.placeholder}
className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
</div>
<>
{activeTab === "image" && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-main mb-2">
Source Image
</label>
<input
type="file"
accept="image/*"
onChange={(e) => setImageInputFile(e.target.files?.[0] ?? null)}
className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-primary/10 file:text-primary file:text-sm"
/>
{imageInputFile && (
<p className="text-xs text-text-muted mt-1">
{imageInputFile.name} ({formatFileSize(imageInputFile.size)})
</p>
)}
<p className="text-[10px] text-text-muted/60 mt-1">
Optional for image-to-image, editing and upscale workflows.
</p>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-2">
Mask Image
</label>
<input
type="file"
accept="image/*"
onChange={(e) => setImageMaskFile(e.target.files?.[0] ?? null)}
className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-primary/10 file:text-primary file:text-sm"
/>
{imageMaskFile && (
<p className="text-xs text-text-muted mt-1">
{imageMaskFile.name} ({formatFileSize(imageMaskFile.size)})
</p>
)}
<p className="text-[10px] text-text-muted/60 mt-1">
Optional. Used by inpaint-style models that support masks.
</p>
</div>
</div>
)}
{/* Prompt / Text */}
<div>
<label className="block text-sm font-medium text-text-main mb-2">
{activeTab === "speech"
? "Text"
: activeTab === "image" && selectedProvider === "topaz"
? "Prompt (optional)"
: t("prompt")}
</label>
<textarea
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder={
activeTab === "image" && selectedProvider === "topaz"
? "Optional enhancement instructions..."
: config.placeholder
}
className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
</div>
</>
)}
{/* Generate button */}
<button
onClick={handleGenerate}
disabled={loading || (activeTab === "transcription" ? !audioFile : !prompt.trim())}
disabled={isGenerateDisabled}
className={`w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-white font-medium transition-all bg-gradient-to-r ${config.color} ${
loading || (activeTab === "transcription" ? !audioFile : !prompt.trim())
isGenerateDisabled
? "opacity-50 cursor-not-allowed"
: "hover:opacity-90 hover:shadow-lg"
}`}

View File

@@ -8,6 +8,7 @@ import FallbackChainsEditor from "./FallbackChainsEditor";
export default function RoutingTab() {
const [settings, setSettings] = useState<any>({
alwaysPreserveClientCache: "auto",
antigravitySignatureCacheMode: "enabled",
});
const [loading, setLoading] = useState(true);
const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false);
@@ -155,6 +156,74 @@ export default function RoutingTab() {
<FallbackChainsEditor />
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-sky-500/10 text-sky-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
fingerprint
</span>
</div>
<div>
<h3 className="text-lg font-semibold">Antigravity Signature Cache Mode</h3>
<p className="text-sm text-text-muted">
Control whether OmniRoute reuses only stored Gemini thought signatures or accepts
validated client-provided signatures in Antigravity-compatible tool-call flows.
</p>
</div>
</div>
<div className="space-y-3">
{[
{
value: "enabled",
label: "Enabled",
desc: "Current behavior. Ignore client-provided signatures and keep using the stored OmniRoute flow.",
},
{
value: "bypass",
label: "Bypass",
desc: "Accept client-provided signatures after lightweight validation and fall back to the stored signature when invalid.",
},
{
value: "bypass-strict",
label: "Bypass Strict",
desc: "Require full protobuf validation before accepting a client-provided signature.",
},
].map((option) => (
<button
key={option.value}
onClick={() => updateSetting({ antigravitySignatureCacheMode: option.value })}
disabled={loading}
className={`w-full flex flex-col items-start gap-1 p-3 rounded-lg border text-left transition-all ${
settings.antigravitySignatureCacheMode === option.value
? "border-sky-500/50 bg-sky-500/5 ring-1 ring-sky-500/20"
: "border-border/50 hover:border-border hover:bg-surface/30"
}`}
>
<div className="flex items-center gap-2">
<span
className={`material-symbols-outlined text-[16px] ${
settings.antigravitySignatureCacheMode === option.value
? "text-sky-400"
: "text-text-muted"
}`}
>
{settings.antigravitySignatureCacheMode === option.value
? "check_circle"
: "radio_button_unchecked"}
</span>
<span
className={`text-sm font-medium ${settings.antigravitySignatureCacheMode === option.value ? "text-sky-400" : ""}`}
>
{option.label}
</span>
</div>
<p className="text-xs text-text-muted ml-7">{option.desc}</p>
</button>
))}
</div>
</Card>
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-green-500/10 text-green-500">

View File

@@ -18,6 +18,11 @@ import { getAntigravityHeaders } from "@omniroute/open-sse/services/antigravityH
import { getAntigravityModelsDiscoveryUrls } from "@omniroute/open-sse/config/antigravityUpstream.ts";
import { getGlmModelsUrl } from "@omniroute/open-sse/config/glmProvider.ts";
import { getImageProvider } from "@omniroute/open-sse/config/imageRegistry.ts";
import { resolveAntigravityVersion } from "@omniroute/open-sse/services/antigravityVersion.ts";
import {
getClientVisibleAntigravityModelName,
toClientAntigravityModelId,
} from "@omniroute/open-sse/config/antigravityModelAliases.ts";
type JsonRecord = Record<string, unknown>;
@@ -78,6 +83,17 @@ function normalizeAntigravityModelsResponse(data: unknown): Array<{ id: string;
.filter((value): value is { id: string; name: string } => Boolean(value));
}
function mapAntigravityModelForClient(model: { id: string; name: string }): {
id: string;
name: string;
} {
const clientId = toClientAntigravityModelId(model.id);
return {
id: clientId,
name: getClientVisibleAntigravityModelName(clientId, model.name),
};
}
type ProviderModelsConfigEntry = {
url: string;
method: "GET" | "POST";
@@ -118,9 +134,9 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
antigravity: () => [
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" },
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" },
],
@@ -682,6 +698,8 @@ export async function GET(
});
}
await resolveAntigravityVersion();
for (const discoveryUrl of discoveryUrls) {
try {
const response = await safeOutboundFetch(discoveryUrl, {
@@ -701,7 +719,9 @@ export async function GET(
continue;
}
const remoteModels = normalizeAntigravityModelsResponse(await response.json());
const remoteModels = normalizeAntigravityModelsResponse(await response.json()).map(
mapAntigravityModelForClient
);
if (remoteModels.length > 0) {
return buildResponse({
provider,

View File

@@ -7,6 +7,7 @@ import { getRuntimePorts } from "@/lib/runtime/ports";
import { updateSettingsSchema } from "@/shared/validation/settingsSchemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setCliCompatProviders } from "../../../../open-sse/config/cliFingerprints";
import { setGeminiThoughtSignatureMode } from "@omniroute/open-sse/services/geminiThoughtSignatureStore.ts";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { validateProxyUrl, upsertUpstreamProxyConfig } from "@/lib/db/upstreamProxy";
@@ -155,6 +156,10 @@ export async function PATCH(request) {
invalidateCacheControlSettingsCache();
}
if ("antigravitySignatureCacheMode" in body) {
setGeminiThoughtSignatureMode(settings.antigravitySignatureCacheMode);
}
// Sync models.dev sync settings (compare old vs new state)
if (oldSettings && ("modelsDevSyncEnabled" in body || "modelsDevSyncInterval" in body)) {
const { stopPeriodicSync, startPeriodicSync } = await import("@/lib/modelsDevSync");

View File

@@ -48,6 +48,9 @@ export async function GET() {
owned_by: m.provider,
type: "image",
supported_sizes: m.supportedSizes,
input_modalities: m.inputModalities || ["text"],
output_modalities: ["image"],
...(m.description ? { description: m.description } : {}),
}));
// Include custom models tagged for images
@@ -67,6 +70,8 @@ export async function GET() {
owned_by: providerId,
type: "image",
supported_sizes: null,
input_modalities: ["text"],
output_modalities: ["image"],
});
}
}

View File

@@ -382,6 +382,9 @@ export async function getUnifiedModelsResponse(
owned_by: imgModel.provider,
type: "image",
supported_sizes: imgModel.supportedSizes,
input_modalities: imgModel.inputModalities || ["text"],
output_modalities: ["image"],
...(imgModel.description ? { description: imgModel.description } : {}),
});
}

View File

@@ -47,6 +47,7 @@ export async function getSettings() {
stickyRoundRobinLimit: 3,
requestRetry: 3,
maxRetryIntervalSec: 30,
antigravitySignatureCacheMode: "enabled",
requireLogin: true,
hiddenSidebarItems: [],
alwaysPreserveClientCache: "auto",

View File

@@ -3,6 +3,7 @@ import { getModelAliases, setModelAlias } from "@/lib/db/models";
export const DEFAULT_MODEL_ALIAS_SEED = Object.freeze({
"gemini-3-pro-high": "antigravity/gemini-3.1-pro-high",
"gemini-3-pro-low": "antigravity/gemini-3.1-pro-low",
"gemini-3-pro-preview": "antigravity/gemini-3.1-pro-high",
"gemini-3.1-pro-preview": "antigravity/gemini-3.1-pro-high",
"gemini-3.1-pro-preview-customtools": "antigravity/gemini-3.1-pro-high",
"gemini-3-flash-preview": "antigravity/gemini-3-flash",

View File

@@ -40,7 +40,12 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsThinking: true,
supportsTools: true,
supportsVision: true,
aliases: ["gemini-3-pro-high", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview-customtools"],
aliases: [
"gemini-3-pro-high",
"gemini-3-pro-preview",
"gemini-3.1-pro-preview",
"gemini-3.1-pro-preview-customtools",
],
},
// ── Gemini 3.1 Pro Low ──────────────────────────────────────────

View File

@@ -24,6 +24,8 @@ const fallbackStrategyValues = [
"lkgp",
] as const;
const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const;
export const updateSettingsSchema = z.object({
newPassword: z.string().min(1).max(200).optional(),
currentPassword: z.string().max(200).optional(),
@@ -68,6 +70,7 @@ export const updateSettingsSchema = z.object({
stripModelPrefix: z.boolean().optional(),
// Cache control preservation mode
alwaysPreserveClientCache: z.enum(["auto", "always", "never"]).optional(),
antigravitySignatureCacheMode: z.enum(signatureCacheModeValues).optional(),
// Adaptive Volume Routing
adaptiveVolumeRouting: z.boolean().optional(),
// Usage token buffer — safety margin added to reported prompt/input token counts.

View File

@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
resolveAntigravityModelId,
toClientAntigravityModelId,
} from "../../open-sse/config/antigravityModelAliases.ts";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
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-flash-preview"), "gemini-3-flash");
assert.equal(resolveAntigravityModelId("gemini-3-pro-image-preview"), "gemini-3-pro-image");
assert.equal(
resolveAntigravityModelId("gemini-2.5-computer-use-preview-10-2025"),
"rev19-uic3-1p"
);
assert.equal(resolveAntigravityModelId("gemini-claude-sonnet-4-5"), "claude-sonnet-4-5");
assert.equal(
resolveAntigravityModelId("gemini-claude-sonnet-4-5-thinking"),
"claude-sonnet-4-5-thinking"
);
assert.equal(
resolveAntigravityModelId("gemini-claude-opus-4-5-thinking"),
"claude-opus-4-5-thinking"
);
assert.equal(resolveAntigravityModelId("unknown-model"), "unknown-model");
});
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"), "gemini-3-flash-preview");
assert.equal(toClientAntigravityModelId("gpt-oss-120b-medium"), "gpt-oss-120b-medium");
});
test("AntigravityExecutor.transformRequest resolves alias models before dispatching upstream", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-3-pro-preview",
{
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
},
true,
{ projectId: "project-1" }
);
assert.equal(result.model, "gemini-3.1-pro-high");
});

View File

@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_FALLBACK_VERSION,
ANTIGRAVITY_VERSION_CACHE_TTL_MS,
clearAntigravityVersionCache,
getCachedAntigravityVersion,
resolveAntigravityVersion,
seedAntigravityVersionCache,
} from "../../open-sse/services/antigravityVersion.ts";
const originalDateNow = Date.now;
test.afterEach(() => {
Date.now = originalDateNow;
clearAntigravityVersionCache();
});
test("resolveAntigravityVersion uses the official release feed and caches the result for 6 hours", async () => {
let calls = 0;
const fetchMock = async () => {
calls += 1;
return new Response(JSON.stringify([{ version: "1.23.2", execution_id: "4781536860569600" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const first = await resolveAntigravityVersion(fetchMock as typeof fetch);
const second = await resolveAntigravityVersion(fetchMock as typeof fetch);
assert.equal(first, "1.23.2");
assert.equal(second, "1.23.2");
assert.equal(calls, 1);
assert.equal(getCachedAntigravityVersion(), "1.23.2");
});
test("resolveAntigravityVersion refreshes the cache after the TTL elapses", async () => {
let now = 1_000;
Date.now = () => now;
const firstFetch = async () =>
new Response(JSON.stringify([{ version: "1.23.2" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
const secondFetch = async () =>
new Response(JSON.stringify([{ version: "1.24.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
assert.equal(await resolveAntigravityVersion(firstFetch as typeof fetch), "1.23.2");
now += ANTIGRAVITY_VERSION_CACHE_TTL_MS + 1;
assert.equal(await resolveAntigravityVersion(secondFetch as typeof fetch), "1.24.0");
assert.equal(getCachedAntigravityVersion(), "1.24.0");
});
test("resolveAntigravityVersion falls back to the last known good version or bundled fallback", async () => {
const failingFetch = async () => {
throw new Error("network down");
};
assert.equal(
await resolveAntigravityVersion(failingFetch as typeof fetch),
ANTIGRAVITY_FALLBACK_VERSION
);
seedAntigravityVersionCache("1.23.1", 0);
assert.equal(await resolveAntigravityVersion(failingFetch as typeof fetch), "1.23.1");
});
test("resolveAntigravityVersion parses GitHub-style tag_name payloads with or without a v prefix", async () => {
let calls = 0;
const fetchMock = async () => {
calls += 1;
if (calls === 1) {
return new Response(JSON.stringify({ malformed: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ tag_name: "v1.24.3" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
assert.equal(await resolveAntigravityVersion(fetchMock as typeof fetch), "1.24.3");
});

View File

@@ -58,7 +58,9 @@ test("buildClaudeCodeCompatibleHeaders emits stream-aware auth headers and sessi
test("Claude Code compatible beta set stays conservative for third-party proxies", () => {
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("oauth-2025-04-20"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("token-efficient-tools-2025-02-19"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("advanced-tool-use-2025-11-20"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("fast-mode-2026-02-01"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("token-efficient-tools-2026-03-28"));
assert.equal(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("fast-mode-2025-04-01"), false);
assert.equal(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("redact-thinking-2025-06-20"), false);
});

View File

@@ -14,6 +14,7 @@ import assert from "node:assert/strict";
// ── CCH signing ───────────────────────────────────────────────────────────────
import { computeCCH, signRequestBody, CCH_PATTERN } from "../../open-sse/services/claudeCodeCCH.ts";
import { CLAUDE_CODE_COMPATIBLE_VERSION } from "../../open-sse/services/claudeCodeCompatible.ts";
// ── Fingerprint ───────────────────────────────────────────────────────────────
import {
@@ -63,8 +64,7 @@ describe("computeCCH", () => {
describe("signRequestBody", () => {
it("replaces cch=00000 placeholder with computed hash", async () => {
const body =
'{"x-anthropic-billing-header":"cc_version=2.1.87.abc; cch=00000;","model":"claude"}';
const body = `{"x-anthropic-billing-header":"cc_version=${CLAUDE_CODE_COMPATIBLE_VERSION}.abc; cch=00000;","model":"claude"}`;
const signed = await signRequestBody(body);
assert.ok(signed.includes("cch="), "signed body should contain cch=");
assert.ok(!signed.includes("cch=00000"), "placeholder cch=00000 should be replaced");
@@ -91,31 +91,31 @@ describe("signRequestBody", () => {
describe("computeFingerprint", () => {
it("returns a 3-character hex string", () => {
const fp = computeFingerprint("Hello, world!", "2.1.87");
const fp = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
assert.equal(fp.length, 3, "fingerprint must be 3 chars");
assert.match(fp, /^[0-9a-f]{3}$/, "fingerprint must be lowercase hex");
});
it("is deterministic — same text + version always produces the same fingerprint", () => {
const fp1 = computeFingerprint("Hello, world!", "2.1.87");
const fp2 = computeFingerprint("Hello, world!", "2.1.87");
const fp1 = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
const fp2 = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
assert.equal(fp1, fp2, "fingerprint must be deterministic");
});
it("changes when fingerprint version changes", () => {
const fp1 = computeFingerprint("same text", "2.1.87");
const fp2 = computeFingerprint("same text", "2.1.88");
const fp1 = computeFingerprint("same text", CLAUDE_CODE_COMPATIBLE_VERSION);
const fp2 = computeFingerprint("same text", "2.1.93");
assert.notEqual(fp1, fp2, "different versions must produce different fingerprints");
});
it("handles short messages safely — uses '0' for missing indices", () => {
// indices are [4, 7, 20]; short string should not throw
const fp = computeFingerprint("hi", "2.1.87");
const fp = computeFingerprint("hi", CLAUDE_CODE_COMPATIBLE_VERSION);
assert.ok(fp.length === 3, "short input should not throw and return 3-char fingerprint");
});
it("handles empty string without throwing", () => {
const fp = computeFingerprint("", "2.1.87");
const fp = computeFingerprint("", CLAUDE_CODE_COMPATIBLE_VERSION);
assert.ok(fp.length === 3, "empty string should not throw");
});
});

View File

@@ -68,11 +68,13 @@ test("getSettings exposes defaults and updateSettings persists typed values", as
assert.equal(defaults.idempotencyWindowMs, 5000);
assert.equal(defaults.requestRetry, 3);
assert.equal(defaults.maxRetryIntervalSec, 30);
assert.equal(defaults.antigravitySignatureCacheMode, "enabled");
assert.equal(updated.requireLogin, false);
assert.equal(updated.cloudEnabled, true);
assert.equal(updated.stickyRoundRobinLimit, 7);
assert.equal(updated.requestRetry, 5);
assert.equal(updated.maxRetryIntervalSec, 12);
assert.equal(updated.antigravitySignatureCacheMode, "enabled");
assert.equal(updated.label, "task-303");
assert.equal(await settingsDb.isCloudEnabled(), true);
});

View File

@@ -263,6 +263,8 @@ test("image registry resolves flux aliases and exposes planned catalog aliases",
});
const modelIds = new Set(getAllImageModels().map((model) => model.id));
const fluxRedux = getAllImageModels().find((model) => model.id === "flux-redux");
const fluxKontext = getAllImageModels().find((model) => model.id === "flux-kontext");
for (const alias of [
"flux-kontext",
"flux-kontext-max",
@@ -273,6 +275,8 @@ test("image registry resolves flux aliases and exposes planned catalog aliases",
]) {
assert.equal(modelIds.has(alias), true, `Expected alias ${alias} in image catalog`);
}
assert.deepEqual(fluxRedux?.inputModalities, ["text", "image"]);
assert.deepEqual(fluxKontext?.inputModalities, ["text", "image"]);
});
test("handleImageGeneration calls Fal AI with Key auth and normalizes URL results to base64", async () => {

View File

@@ -36,6 +36,7 @@ test("default model alias seed writes missing aliases and is idempotent", async
assert.equal(first.applied.length, Object.keys(DEFAULT_MODEL_ALIAS_SEED).length);
assert.equal(aliases["gemini-3-pro-high"], "antigravity/gemini-3.1-pro-high");
assert.equal(aliases["gemini-3-pro-low"], "antigravity/gemini-3.1-pro-low");
assert.equal(aliases["gemini-3-pro-preview"], "antigravity/gemini-3.1-pro-high");
assert.equal(aliases["gemini-3.1-pro-preview"], "antigravity/gemini-3.1-pro-high");
assert.equal(aliases["gemini-3-flash-preview"], "antigravity/gemini-3-flash");

View File

@@ -337,6 +337,25 @@ test("v1 models catalog includes media, moderation, rerank, video, and music mod
assert.equal(byId.get("comfyui/stable-audio-open")?.type, "music");
});
test("v1 models catalog exposes image model input and output modalities for advanced image providers", async () => {
await seedConnection("together", { name: "together-images" });
await seedConnection("topaz", { name: "topaz-images" });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = await response.json();
const byId = new Map(body.data.map((item) => [item.id, item]));
assert.equal(response.status, 200);
assert.deepEqual(byId.get("flux-redux")?.input_modalities, ["text", "image"]);
assert.deepEqual(byId.get("flux-redux")?.output_modalities, ["image"]);
assert.equal(byId.get("flux-redux")?.type, "image");
assert.ok(byId.get("flux-redux")?.supported_sizes?.includes("1024x1024"));
assert.deepEqual(byId.get("topaz/topaz-enhance")?.input_modalities, ["image"]);
assert.deepEqual(byId.get("topaz/topaz-enhance")?.output_modalities, ["image"]);
});
test("v1 models catalog tolerates custom model lookup failures and keeps builtin models available", async () => {
await seedConnection("openai", { name: "openai-custom-failure" });

View File

@@ -11,6 +11,7 @@ const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
const antigravityVersion = await import("../../open-sse/services/antigravityVersion.ts");
const originalFetch = globalThis.fetch;
const originalAllowPrivateProviderUrls = process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS;
@@ -22,6 +23,7 @@ async function resetStorage() {
} else {
process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS = originalAllowPrivateProviderUrls;
}
antigravityVersion.clearAntigravityVersionCache();
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
@@ -206,6 +208,20 @@ test("provider models route returns static catalog entries for providers with ha
assert.equal(body.models.length, 8);
});
test("provider models route returns the local catalog for built-in image providers", async () => {
const connection = await seedConnection("topaz", {
apiKey: "topaz-key",
});
const response = await callRoute(connection.id);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.provider, "topaz");
assert.ok(Array.isArray(body.models));
assert.deepEqual(body.models, [{ id: "topaz-enhance", name: "topaz-enhance" }]);
});
test("provider models route returns the local catalog for new built-in chat-openai-compat providers", async () => {
const connection = await seedConnection("deepinfra", {
apiKey: "deepinfra-key",
@@ -278,6 +294,7 @@ test("provider models route retries Antigravity discovery endpoints before retur
apiKey: null,
});
const seenUrls = [];
antigravityVersion.seedAntigravityVersionCache("1.23.2");
globalThis.fetch = async (url, init = {}) => {
seenUrls.push(String(url));
@@ -295,17 +312,18 @@ test("provider models route retries Antigravity discovery endpoints before retur
const response = await callRoute(connection.id);
const body = await response.json();
const discoveryUrls = seenUrls.filter((url) => url.includes("/v1internal:models"));
assert.equal(response.status, 200);
assert.equal(body.source, "api");
assert.deepEqual(seenUrls, [
assert.deepEqual(discoveryUrls, [
"https://cloudcode-pa.googleapis.com/v1internal:models",
"https://daily-cloudcode-pa.googleapis.com/v1internal:models",
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
]);
assert.deepEqual(body.models, [{ id: "gemini-3-flash", name: "Gemini 3 Flash" }]);
assert.deepEqual(body.models, [{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }]);
});
test("provider models route falls back to the static Antigravity catalog when discovery fails", async () => {
test("provider models route falls back through all Antigravity discovery endpoints when needed", async () => {
const connection = await seedConnection("antigravity", {
authType: "oauth",
accessToken: "ag-access",
@@ -320,12 +338,17 @@ test("provider models route falls back to the static Antigravity catalog when di
const response = await callRoute(connection.id);
const body = await response.json();
const discoveryUrls = seenUrls.filter((url) => url.includes("/v1internal:models"));
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /cached catalog/i);
assert.equal(seenUrls.length, 3);
assert.ok(body.models.some((model) => model.id === "gemini-3.1-pro-high"));
assert.deepEqual(discoveryUrls, [
"https://cloudcode-pa.googleapis.com/v1internal:models",
"https://daily-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"));
});
test("provider models route returns the local catalog for OAuth-backed Qwen connections", async () => {
@@ -515,8 +538,8 @@ test("provider models route stops pagination when the upstream repeats the next
});
test("provider models route forwards upstream status codes for generic provider model fetch failures", async () => {
const connection = await seedConnection("openai", {
apiKey: "sk-openai-models",
const connection = await seedConnection("groq", {
apiKey: "groq-models-token",
});
globalThis.fetch = async () => new Response("upstream unavailable", { status: 503 });
@@ -530,8 +553,8 @@ test("provider models route forwards upstream status codes for generic provider
});
test("provider models route returns 500 when fetching models throws unexpectedly", async () => {
const connection = await seedConnection("openai", {
apiKey: "sk-openai-models",
const connection = await seedConnection("groq", {
apiKey: "groq-models-token",
});
globalThis.fetch = async () => {
@@ -547,7 +570,7 @@ test("provider models route returns 500 when fetching models throws unexpectedly
});
test("provider models route rejects generic providers without any configured token", async () => {
const connection = await seedConnection("openai", {
const connection = await seedConnection("groq", {
apiKey: null,
accessToken: null,
});

View File

@@ -63,5 +63,19 @@ describe("Settings API - debugMode and hiddenSidebarItems", () => {
"hiddenSidebarItems should be updated"
);
});
test("updateSettings persists antigravitySignatureCacheMode", async () => {
const result = await updateSettings({
antigravitySignatureCacheMode: "bypass-strict",
});
assert.ok(result, "updateSettings should return truthy result");
const settings = await getSettings();
assert.strictEqual(
settings.antigravitySignatureCacheMode,
"bypass-strict",
"antigravitySignatureCacheMode should be updated"
);
});
});
});

View File

@@ -0,0 +1,67 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
clearGeminiThoughtSignatures,
getGeminiThoughtSignatureMode,
isValidBasicGeminiThoughtSignature,
isValidFullGeminiThoughtSignature,
resolveGeminiThoughtSignature,
setGeminiThoughtSignatureMode,
storeGeminiThoughtSignature,
} from "../../open-sse/services/geminiThoughtSignatureStore.ts";
function makeSignature(bytes: number[]): string {
return `R${Buffer.from(bytes).toString("base64")}`;
}
const BASIC_VALID_SIGNATURE = makeSignature([0x12, 0x00]);
const STRICT_VALID_SIGNATURE = makeSignature([0x12, 0x02, 0x0a, 0x00]);
const INVALID_SIGNATURE = makeSignature([0x13, 0x00]);
test.beforeEach(() => {
clearGeminiThoughtSignatures();
});
test("signature bypass validators distinguish basic and full protobuf signatures", () => {
assert.equal(isValidBasicGeminiThoughtSignature(BASIC_VALID_SIGNATURE), true);
assert.equal(isValidBasicGeminiThoughtSignature(INVALID_SIGNATURE), false);
assert.equal(isValidBasicGeminiThoughtSignature("not-base64"), false);
assert.equal(isValidFullGeminiThoughtSignature(STRICT_VALID_SIGNATURE), true);
assert.equal(isValidFullGeminiThoughtSignature(BASIC_VALID_SIGNATURE), false);
assert.equal(isValidFullGeminiThoughtSignature(INVALID_SIGNATURE), false);
});
test("enabled mode preserves the current stored-signature behavior", () => {
storeGeminiThoughtSignature("call-1", "stored-signature");
setGeminiThoughtSignatureMode("enabled");
assert.equal(getGeminiThoughtSignatureMode(), "enabled");
assert.equal(resolveGeminiThoughtSignature("call-1", STRICT_VALID_SIGNATURE), "stored-signature");
assert.equal(resolveGeminiThoughtSignature("missing-call", STRICT_VALID_SIGNATURE), null);
});
test("bypass mode accepts basic-valid client signatures and falls back on invalid ones", () => {
storeGeminiThoughtSignature("call-1", "stored-signature");
setGeminiThoughtSignatureMode("bypass");
assert.equal(
resolveGeminiThoughtSignature("call-1", BASIC_VALID_SIGNATURE),
BASIC_VALID_SIGNATURE
);
assert.equal(resolveGeminiThoughtSignature("call-1", INVALID_SIGNATURE), "stored-signature");
assert.equal(resolveGeminiThoughtSignature("call-1"), "stored-signature");
});
test("bypass-strict mode requires the full protobuf structure before accepting a client signature", () => {
storeGeminiThoughtSignature("call-1", "stored-signature");
setGeminiThoughtSignatureMode("bypass-strict");
assert.equal(
resolveGeminiThoughtSignature("call-1", STRICT_VALID_SIGNATURE),
STRICT_VALID_SIGNATURE
);
assert.equal(resolveGeminiThoughtSignature("call-1", BASIC_VALID_SIGNATURE), "stored-signature");
assert.equal(resolveGeminiThoughtSignature("call-1", INVALID_SIGNATURE), "stored-signature");
});

View File

@@ -17,14 +17,14 @@ test("T28: gemini-cli catalog includes preview models, gemini uses API sync", ()
assert.ok(geminiCliIds.includes("gemini-3-flash-preview"));
});
test("T28: antigravity static catalog exposes current Gemini 3.1 model IDs", () => {
test("T28: antigravity static catalog exposes client-visible Gemini preview IDs", () => {
const staticIds = (getStaticModelsForProvider("antigravity") || []).map((m) => m.id);
assert.ok(staticIds.includes("gemini-3.1-pro-high"));
assert.ok(staticIds.includes("gemini-3-pro-preview"));
assert.ok(staticIds.includes("gemini-3.1-pro-low"));
assert.ok(staticIds.includes("gemini-3-flash"));
assert.ok(staticIds.includes("gemini-3-flash-preview"));
assert.ok(!staticIds.includes("gemini-3-pro-high"));
assert.ok(!staticIds.includes("gemini-3-pro-low"));
assert.ok(!staticIds.includes("gemini-3.1-pro-high"));
});
test("T28: github registry exposes Gemini 3.1 Pro Preview and keeps legacy alias compatibility", async () => {

View File

@@ -16,11 +16,11 @@ const {
capThinkingBudget,
} = await import("../../src/shared/constants/modelSpecs.ts");
test("T31: antigravity static catalog exposes Gemini 3.1 Pro High/Low model IDs", () => {
// gemini-3.1-pro-high/low are Antigravity (Cloud Code sandbox) models,
// not Gemini AI Studio models. They live in the static catalog, not the registry.
test("T31: antigravity static catalog exposes client-visible Gemini preview IDs", () => {
// Antigravity exposes preview aliases to clients even though the upstream
// still accepts its internal model identifiers.
const staticIds = (getStaticModelsForProvider("antigravity") || []).map((m) => m.id);
assert.ok(staticIds.includes("gemini-3.1-pro-high"));
assert.ok(staticIds.includes("gemini-3-pro-preview"));
assert.ok(staticIds.includes("gemini-3.1-pro-low"));
});
@@ -51,12 +51,14 @@ test("T34: max output tokens are capped by model spec", () => {
test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", () => {
assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object");
assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 65535);
assert.equal(getModelSpec("gemini-3-pro-preview").maxOutputTokens, 65535);
assert.equal(getModelSpec("gemini-3-flash-preview").maxOutputTokens, 65536);
assert.equal(getModelSpec("gemini-3.1-pro-preview").maxOutputTokens, 65535);
assert.equal(getModelSpec("gemini-3.1-pro-preview-customtools").maxOutputTokens, 65535);
assert.equal(getModelSpec("claude-opus-4-7").contextWindow, 1000000);
assert.equal(getModelSpec("claude-opus-4.7").maxOutputTokens, 128000);
assert.equal(resolveModelAlias("gemini-3-pro-low"), "gemini-3.1-pro-low");
assert.equal(resolveModelAlias("gemini-3-pro-preview"), "gemini-3.1-pro-high");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview"), "gemini-3.1-pro-high");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview-customtools"), "gemini-3.1-pro-high");
assert.equal(getDefaultThinkingBudget("gemini-3.1-pro-high"), 24576);