mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(providers): expose antigravity preview aliases and gemini cli onboarding
Centralize Antigravity public model definitions and use the client-visible preview aliases in provider discovery, model catalog responses, and default alias seeding. Add Gemini CLI managed-project onboarding with retries when loadCodeAssist does not return a project, and update Gemini CLI header fingerprints to match newer native clients. Improve non-stream handling by converting NDJSON event payloads into SSE-compatible parsing for stream=false requests, add PUT support for the settings API, expand Gemini schema cleanup for local refs and unsupported keys, and include Anthropic beta headers for API-key requests.
This commit is contained in:
@@ -1,3 +1,27 @@
|
||||
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" },
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
|
||||
{ id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro Image Preview" },
|
||||
{
|
||||
id: "gemini-2.5-computer-use-preview-10-2025",
|
||||
name: "Gemini 2.5 Computer Use Preview (10/2025)",
|
||||
},
|
||||
{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" },
|
||||
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
|
||||
{ id: "gemini-claude-sonnet-4-5", name: "Claude Sonnet 4.5 (Gemini Route)" },
|
||||
{
|
||||
id: "gemini-claude-sonnet-4-5-thinking",
|
||||
name: "Claude Sonnet 4.5 Thinking (Gemini Route)",
|
||||
},
|
||||
{
|
||||
id: "gemini-claude-opus-4-5-thinking",
|
||||
name: "Claude Opus 4.5 Thinking (Gemini Route)",
|
||||
},
|
||||
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" },
|
||||
]);
|
||||
|
||||
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
|
||||
"gemini-3-pro-preview": "gemini-3.1-pro-high",
|
||||
"gemini-3-flash-preview": "gemini-3-flash",
|
||||
@@ -20,15 +44,12 @@ export const ANTIGRAVITY_REVERSE_MODEL_ALIASES = Object.freeze(
|
||||
)
|
||||
);
|
||||
|
||||
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)",
|
||||
});
|
||||
const CLIENT_VISIBLE_MODEL_NAMES = Object.freeze(
|
||||
ANTIGRAVITY_PUBLIC_MODELS.reduce<Record<string, string>>((acc, model) => {
|
||||
acc[model.id] = model.name;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
export function resolveAntigravityModelId(modelId: string): string {
|
||||
if (!modelId) return modelId;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { platform, arch } from "os";
|
||||
import { ANTIGRAVITY_BASE_URLS } from "./antigravityUpstream.ts";
|
||||
import { ANTIGRAVITY_PUBLIC_MODELS } from "./antigravityModelAliases.ts";
|
||||
import {
|
||||
ANTHROPIC_BETA_API_KEY,
|
||||
ANTHROPIC_BETA_CLAUDE_OAUTH,
|
||||
@@ -481,15 +482,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clientSecretEnv: "ANTIGRAVITY_OAUTH_CLIENT_SECRET",
|
||||
clientSecretDefault: "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",
|
||||
},
|
||||
models: [
|
||||
{ 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.1-flash-image", name: "Gemini 3.1 Flash Image" },
|
||||
{ id: "gemini-3.1-pro-high", 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" },
|
||||
],
|
||||
models: [...ANTIGRAVITY_PUBLIC_MODELS],
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
@@ -638,7 +631,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
authHeader: "x-api-key",
|
||||
defaultContextLength: 200000,
|
||||
headers: {
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
|
||||
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
|
||||
},
|
||||
models: [
|
||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||
|
||||
@@ -5,21 +5,103 @@ import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderSc
|
||||
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
|
||||
|
||||
const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
|
||||
const ONBOARD_USER_URL = "https://cloudcode-pa.googleapis.com/v1internal:onboardUser";
|
||||
const PROJECT_TTL_MS = 30_000; // 30 seconds — matches native Gemini CLI
|
||||
const MAX_CACHE_SIZE = 100;
|
||||
const LOAD_CODE_ASSIST_TIMEOUT_MS = 10_000; // 10 seconds timeout
|
||||
const ONBOARD_TIMEOUT_MS = 30_000;
|
||||
const ONBOARD_MAX_ATTEMPTS = 10;
|
||||
const ONBOARD_DELAY_MS = 5_000;
|
||||
const DEFAULT_PROJECT_ID = "default-project";
|
||||
const DEFAULT_ONBOARD_TIER = "free-tier";
|
||||
const LOAD_CODE_ASSIST_METADATA = Object.freeze({
|
||||
ideType: "ANTIGRAVITY",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
duetProject: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
const ONBOARD_METADATA = Object.freeze({
|
||||
ideType: "ANTIGRAVITY",
|
||||
pluginType: "GEMINI",
|
||||
duetProject: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
|
||||
// Per-account cache: accessToken -> { projectId, expiresAt }
|
||||
const projectCache = new Map<string, { projectId: string; expiresAt: number }>();
|
||||
// In-flight deduplication: prevents thundering herd on cache miss
|
||||
const inflightRefresh = new Map<string, Promise<string | null>>();
|
||||
|
||||
type LoadCodeAssistResponse = {
|
||||
cloudaicompanionProject?: string | { id?: string | null } | null;
|
||||
allowedTiers?: Array<{ id?: string | null; isDefault?: boolean | null }> | null;
|
||||
};
|
||||
|
||||
type OnboardOptions = {
|
||||
attempts?: number;
|
||||
delayMs?: number;
|
||||
};
|
||||
|
||||
function normalizeGeminiModel(model: string): string {
|
||||
return typeof model === "string" && model.trim().length > 0
|
||||
? model.replace(/^models\//, "").trim()
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
function extractProjectId(payload: unknown): string {
|
||||
if (!payload || typeof payload !== "object") return "";
|
||||
const data = payload as LoadCodeAssistResponse;
|
||||
if (typeof data.cloudaicompanionProject === "string") {
|
||||
return data.cloudaicompanionProject.trim();
|
||||
}
|
||||
if (typeof data.cloudaicompanionProject?.id === "string") {
|
||||
return data.cloudaicompanionProject.id.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractDefaultTierId(payload: unknown): string {
|
||||
if (!payload || typeof payload !== "object") return DEFAULT_ONBOARD_TIER;
|
||||
const tiers = Array.isArray((payload as LoadCodeAssistResponse).allowedTiers)
|
||||
? (payload as LoadCodeAssistResponse).allowedTiers
|
||||
: [];
|
||||
for (const tier of tiers) {
|
||||
if (tier?.isDefault && typeof tier.id === "string" && tier.id.trim()) {
|
||||
return tier.id.trim();
|
||||
}
|
||||
}
|
||||
return DEFAULT_ONBOARD_TIER;
|
||||
}
|
||||
|
||||
function cacheProject(accessToken: string, projectId: string): void {
|
||||
if (projectCache.size >= MAX_CACHE_SIZE) {
|
||||
const now = Date.now();
|
||||
for (const [key, val] of projectCache) {
|
||||
if (val.expiresAt <= now) projectCache.delete(key);
|
||||
}
|
||||
if (projectCache.size >= MAX_CACHE_SIZE) {
|
||||
const firstKey = projectCache.keys().next().value;
|
||||
if (firstKey !== undefined) projectCache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
projectCache.set(accessToken, {
|
||||
projectId,
|
||||
expiresAt: Date.now() + PROJECT_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
if (!ms || ms <= 0) return Promise.resolve();
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export class GeminiCLIExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("gemini-cli", PROVIDERS["gemini-cli"]);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
this._currentModel = normalizeGeminiModel(model);
|
||||
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
||||
return `${this.config.baseUrl}:${action}`;
|
||||
}
|
||||
@@ -36,9 +118,80 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
return scrubProxyAndFingerprintHeaders(raw);
|
||||
}
|
||||
|
||||
// Track current model for dynamic UA (set by transformRequest)
|
||||
// Track current model for dynamic UA. BaseExecutor calls buildUrl before buildHeaders.
|
||||
private _currentModel = "unknown";
|
||||
|
||||
async onboardManagedProject(
|
||||
accessToken: string,
|
||||
tierId = DEFAULT_ONBOARD_TIER,
|
||||
options: OnboardOptions = {}
|
||||
): Promise<string | null> {
|
||||
const attempts =
|
||||
Number.isInteger(options.attempts) && options.attempts! > 0
|
||||
? Number(options.attempts)
|
||||
: ONBOARD_MAX_ATTEMPTS;
|
||||
const delayMs =
|
||||
typeof options.delayMs === "number" &&
|
||||
Number.isFinite(options.delayMs) &&
|
||||
options.delayMs >= 0
|
||||
? options.delayMs
|
||||
: ONBOARD_DELAY_MS;
|
||||
|
||||
const requestBody = {
|
||||
tierId: tierId || DEFAULT_ONBOARD_TIER,
|
||||
metadata: { ...ONBOARD_METADATA },
|
||||
};
|
||||
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), ONBOARD_TIMEOUT_MS);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(ONBOARD_USER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": "GeminiCLI/1.0.0",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const payload = await response.json();
|
||||
const managedProjectId = extractProjectId(payload?.response);
|
||||
|
||||
if (payload?.done === true && managedProjectId) {
|
||||
return managedProjectId;
|
||||
}
|
||||
|
||||
if (payload?.done === true) {
|
||||
return DEFAULT_PROJECT_ID;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[OmniRoute] onboardUser returned ${response.status} on attempt ${attempt + 1}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[OmniRoute] onboardUser attempt ${attempt + 1} failed (${msg})`);
|
||||
}
|
||||
|
||||
if (attempt < attempts - 1) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current cloudaicompanionProject via loadCodeAssist API.
|
||||
* Native Gemini CLI refreshes this every 30 seconds — OmniRoute stores it once
|
||||
@@ -78,11 +231,8 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
},
|
||||
cloudaicompanionProject: DEFAULT_PROJECT_ID,
|
||||
metadata: { ...LOAD_CODE_ASSIST_METADATA },
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -97,37 +247,24 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let projectId = "";
|
||||
if (typeof data.cloudaicompanionProject === "string") {
|
||||
projectId = data.cloudaicompanionProject.trim();
|
||||
} else if (typeof data.cloudaicompanionProject?.id === "string") {
|
||||
projectId = data.cloudaicompanionProject.id.trim();
|
||||
const data = (await response.json()) as LoadCodeAssistResponse;
|
||||
let projectId = extractProjectId(data);
|
||||
|
||||
if (!projectId) {
|
||||
console.warn(
|
||||
"[OmniRoute] loadCodeAssist returned no project — attempting managed project onboarding"
|
||||
);
|
||||
projectId = await this.onboardManagedProject(accessToken, extractDefaultTierId(data));
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
console.warn(
|
||||
"[OmniRoute] loadCodeAssist returned no project — falling back to stored projectId"
|
||||
"[OmniRoute] managed project onboarding failed — falling back to stored projectId"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cache for 30 seconds (evict stale entries if cache is full)
|
||||
if (projectCache.size >= MAX_CACHE_SIZE) {
|
||||
const now = Date.now();
|
||||
for (const [key, val] of projectCache) {
|
||||
if (val.expiresAt <= now) projectCache.delete(key);
|
||||
}
|
||||
// If still full, evict the oldest entry (Map maintains insertion order)
|
||||
if (projectCache.size >= MAX_CACHE_SIZE) {
|
||||
const firstKey = projectCache.keys().next().value;
|
||||
if (firstKey !== undefined) projectCache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
projectCache.set(accessToken, {
|
||||
projectId,
|
||||
expiresAt: Date.now() + PROJECT_TTL_MS,
|
||||
});
|
||||
cacheProject(accessToken, projectId);
|
||||
|
||||
return projectId;
|
||||
} catch (error) {
|
||||
@@ -138,8 +275,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
async transformRequest(model, body, stream, credentials) {
|
||||
// Track model for dynamic User-Agent
|
||||
this._currentModel = model || "unknown";
|
||||
this._currentModel = normalizeGeminiModel(model);
|
||||
|
||||
// Refresh the project ID via loadCodeAssist (cached for 30s).
|
||||
if (body && typeof body === "object" && body.request && credentials.accessToken) {
|
||||
|
||||
@@ -307,6 +307,24 @@ function parseNonStreamingSSEPayload(
|
||||
return null;
|
||||
}
|
||||
|
||||
function convertNDJSONToSSE(rawBody: string): string {
|
||||
const chunks = String(rawBody || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
if (chunks.length === 0) return rawBody;
|
||||
|
||||
return `${chunks.map((chunk) => `data: ${chunk}\n`).join("\n")}\n`;
|
||||
}
|
||||
|
||||
function normalizeNonStreamingEventPayload(rawBody: string, contentType: string): string {
|
||||
if (contentType.includes("application/x-ndjson")) {
|
||||
return convertNDJSONToSSE(rawBody);
|
||||
}
|
||||
return rawBody;
|
||||
}
|
||||
|
||||
function getHeaderValueCaseInsensitive(
|
||||
headers: Record<string, unknown> | null | undefined,
|
||||
targetName: string
|
||||
@@ -2180,11 +2198,19 @@ export async function handleChatCore({
|
||||
const rawBody = await providerResponse.text();
|
||||
const normalizedProviderPayload = normalizePayloadForLog(rawBody);
|
||||
const looksLikeSSE =
|
||||
contentType.includes("text/event-stream") || /(^|\n)\s*(event|data):/m.test(rawBody);
|
||||
contentType.includes("text/event-stream") ||
|
||||
contentType.includes("application/x-ndjson") ||
|
||||
/(^|\n)\s*(event|data):/m.test(rawBody);
|
||||
|
||||
if (looksLikeSSE) {
|
||||
const streamPayload = normalizeNonStreamingEventPayload(rawBody, contentType);
|
||||
const streamKind = contentType.includes("application/x-ndjson") ? "NDJSON" : "SSE";
|
||||
log?.warn?.(
|
||||
"STREAM",
|
||||
`Unexpected ${streamKind} response for non-streaming request — buffering`
|
||||
);
|
||||
// Upstream returned SSE even though stream=false; convert best-effort to JSON.
|
||||
const parsedFromSSE = parseNonStreamingSSEPayload(rawBody, targetFormat, model);
|
||||
const parsedFromSSE = parseNonStreamingSSEPayload(streamPayload, targetFormat, model);
|
||||
|
||||
if (!parsedFromSSE) {
|
||||
appendRequestLog({
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type AntigravityHeaderProfile = "loadCodeAssist" | "fetchAvailableModels" | "models";
|
||||
|
||||
const ANTIGRAVITY_VERSION = ANTIGRAVITY_FALLBACK_VERSION;
|
||||
const GEMINI_CLI_VERSION = "0.31.0";
|
||||
const GEMINI_CLI_VERSION = "1.0.0";
|
||||
const GEMINI_SDK_VERSION = "1.41.0";
|
||||
const NODE_VERSION = "v22.19.0";
|
||||
const LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/9.15.1";
|
||||
@@ -42,9 +42,9 @@ function getPlatform(): string {
|
||||
const p = os.platform();
|
||||
switch (p) {
|
||||
case "win32":
|
||||
return "win32";
|
||||
return "windows";
|
||||
case "darwin":
|
||||
return "darwin";
|
||||
return "macos";
|
||||
default:
|
||||
return p; // "linux", etc.
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export function getAntigravityHeaders(
|
||||
|
||||
/**
|
||||
* Gemini CLI User-Agent: "GeminiCLI/VERSION/MODEL (OS; ARCH)"
|
||||
* Example: "GeminiCLI/0.31.0/gemini-3-flash (darwin; arm64)"
|
||||
* Example: "GeminiCLI/1.0.0/gemini-3-flash (macos; arm64)"
|
||||
*/
|
||||
export function geminiCLIUserAgent(model: string): string {
|
||||
return `GeminiCLI/${GEMINI_CLI_VERSION}/${model || "unknown"} (${getPlatform()}; ${getArch()})`;
|
||||
|
||||
@@ -5,7 +5,7 @@ const ANTIGRAVITY_GITHUB_RELEASE_URL =
|
||||
|
||||
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";
|
||||
export const ANTIGRAVITY_FALLBACK_VERSION = "1.22.2";
|
||||
|
||||
type VersionCache = {
|
||||
fetchedAt: number;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Gemini helper functions for translator
|
||||
|
||||
// Unsupported JSON Schema constraints that should be removed for Antigravity
|
||||
// Reference: CLIProxyAPI/internal/util/gemini_schema.go (removeUnsupportedKeywords)
|
||||
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
// Unsupported JSON Schema constraints that should be removed for Antigravity.
|
||||
// `additionalProperties` is handled separately so `true` can be preserved.
|
||||
export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([
|
||||
// Basic constraints (not supported by Gemini API)
|
||||
"minLength",
|
||||
"maxLength",
|
||||
@@ -17,14 +17,24 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
"examples",
|
||||
// JSON Schema meta keywords
|
||||
"$schema",
|
||||
"$id",
|
||||
"$anchor",
|
||||
"$dynamicRef",
|
||||
"$dynamicAnchor",
|
||||
"$vocabulary",
|
||||
"$comment",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"const",
|
||||
"$ref",
|
||||
// Object validation keywords (not supported)
|
||||
"additionalProperties",
|
||||
"propertyNames",
|
||||
"patternProperties",
|
||||
"unevaluatedProperties",
|
||||
"unevaluatedItems",
|
||||
"contains",
|
||||
"minContains",
|
||||
"maxContains",
|
||||
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
@@ -41,6 +51,9 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
"else",
|
||||
"contentMediaType",
|
||||
"contentEncoding",
|
||||
"contentSchema",
|
||||
"readOnly",
|
||||
"writeOnly",
|
||||
// Non-standard schema fields (not recognized by Gemini API)
|
||||
"deprecated",
|
||||
"optional",
|
||||
@@ -61,7 +74,9 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
"strokeColor",
|
||||
"strokeThickness",
|
||||
"textColor",
|
||||
];
|
||||
]);
|
||||
|
||||
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [...GEMINI_UNSUPPORTED_SCHEMA_KEYS];
|
||||
|
||||
// Default safety settings
|
||||
export const DEFAULT_SAFETY_SETTINGS = [
|
||||
@@ -182,6 +197,87 @@ export function generateSessionId() {
|
||||
return `-${num.toString()}`;
|
||||
}
|
||||
|
||||
function cloneSchemaValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneSchemaValue(item));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, nestedValue]) => [key, cloneSchemaValue(nestedValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function toRecord(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
||||
}
|
||||
|
||||
function decodeJsonPointerSegment(segment) {
|
||||
return String(segment).replace(/~1/g, "/").replace(/~0/g, "~");
|
||||
}
|
||||
|
||||
function resolveLocalReference(root, ref) {
|
||||
if (typeof ref !== "string" || !ref.startsWith("#/")) return null;
|
||||
|
||||
let current = root;
|
||||
const segments = ref
|
||||
.slice(2)
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((segment) => decodeJsonPointerSegment(segment));
|
||||
|
||||
for (const segment of segments) {
|
||||
if (!current || typeof current !== "object" || !(segment in current)) {
|
||||
return null;
|
||||
}
|
||||
current = current[segment];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
function inlineLocalSchemaRefs(node, root, activeRefs = new Set()) {
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((item) => inlineLocalSchemaRefs(item, root, activeRefs));
|
||||
}
|
||||
|
||||
if (!node || typeof node !== "object") {
|
||||
return node;
|
||||
}
|
||||
|
||||
const record = { ...node };
|
||||
const ref = typeof record.$ref === "string" ? record.$ref : "";
|
||||
if (ref.startsWith("#/$defs/") || ref.startsWith("#/definitions/")) {
|
||||
const rest = { ...record };
|
||||
delete rest.$ref;
|
||||
|
||||
if (activeRefs.has(ref)) {
|
||||
return inlineLocalSchemaRefs(rest, root, activeRefs);
|
||||
}
|
||||
|
||||
const resolved = resolveLocalReference(root, ref);
|
||||
if (!resolved || typeof resolved !== "object") {
|
||||
return inlineLocalSchemaRefs(rest, root, activeRefs);
|
||||
}
|
||||
|
||||
activeRefs.add(ref);
|
||||
const merged = {
|
||||
...toRecord(inlineLocalSchemaRefs(cloneSchemaValue(resolved), root, activeRefs)),
|
||||
...rest,
|
||||
};
|
||||
activeRefs.delete(ref);
|
||||
return inlineLocalSchemaRefs(merged, root, activeRefs);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(record).map(([key, value]) => [
|
||||
key,
|
||||
inlineLocalSchemaRefs(value, root, activeRefs),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: Remove unsupported keywords recursively from object/array
|
||||
function removeUnsupportedKeywords(obj, keywords) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
@@ -193,7 +289,7 @@ function removeUnsupportedKeywords(obj, keywords) {
|
||||
} else {
|
||||
// Delete unsupported keys at current level
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (keywords.includes(key) || key.startsWith("x-")) {
|
||||
if (keywords.has(key) || key.startsWith("x-")) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
@@ -206,6 +302,27 @@ function removeUnsupportedKeywords(obj, keywords) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAdditionalProperties(obj) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
normalizeAdditionalProperties(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ("additionalProperties" in obj && obj.additionalProperties !== true) {
|
||||
delete obj.additionalProperties;
|
||||
}
|
||||
|
||||
for (const value of Object.values(obj)) {
|
||||
if (value && typeof value === "object") {
|
||||
normalizeAdditionalProperties(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert const to enum
|
||||
function convertConstToEnum(obj) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
@@ -359,8 +476,8 @@ function flattenTypeArrays(obj) {
|
||||
export function cleanJSONSchemaForAntigravity(schema) {
|
||||
if (!schema || typeof schema !== "object") return schema;
|
||||
|
||||
// Mutate directly (schema is only used once per request)
|
||||
let cleaned = schema;
|
||||
const root = cloneSchemaValue(schema);
|
||||
let cleaned = inlineLocalSchemaRefs(root, root);
|
||||
|
||||
// Phase 1: Convert and prepare
|
||||
convertConstToEnum(cleaned);
|
||||
@@ -371,10 +488,13 @@ export function cleanJSONSchemaForAntigravity(schema) {
|
||||
flattenAnyOfOneOf(cleaned);
|
||||
flattenTypeArrays(cleaned);
|
||||
|
||||
// Phase 3: Remove all unsupported keywords at ALL levels (including inside arrays)
|
||||
removeUnsupportedKeywords(cleaned, UNSUPPORTED_SCHEMA_CONSTRAINTS);
|
||||
// Phase 3: Preserve the only supported additionalProperties shape before keyword cleanup.
|
||||
normalizeAdditionalProperties(cleaned);
|
||||
|
||||
// Phase 4: Cleanup required fields recursively
|
||||
// Phase 4: Remove all unsupported keywords at ALL levels (including inside arrays).
|
||||
removeUnsupportedKeywords(cleaned, GEMINI_UNSUPPORTED_SCHEMA_KEYS);
|
||||
|
||||
// Phase 5: Cleanup required fields recursively.
|
||||
function cleanupRequired(obj) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
@@ -399,7 +519,7 @@ export function cleanJSONSchemaForAntigravity(schema) {
|
||||
|
||||
cleanupRequired(cleaned);
|
||||
|
||||
// Phase 5: Add placeholder for empty object schemas (Antigravity requirement)
|
||||
// Phase 6: Add placeholder for empty object schemas (Antigravity requirement).
|
||||
function addPlaceholders(obj) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 {
|
||||
ANTIGRAVITY_PUBLIC_MODELS,
|
||||
getClientVisibleAntigravityModelName,
|
||||
toClientAntigravityModelId,
|
||||
} from "@omniroute/open-sse/config/antigravityModelAliases.ts";
|
||||
@@ -131,15 +132,7 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
|
||||
{ id: "nanobanana-flash", name: "NanoBanana Flash (Gemini 2.5 Flash)" },
|
||||
{ id: "nanobanana-pro", name: "NanoBanana Pro (Gemini 3 Pro)" },
|
||||
],
|
||||
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-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" },
|
||||
{ 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" },
|
||||
],
|
||||
antigravity: () => ANTIGRAVITY_PUBLIC_MODELS.map((model) => ({ ...model })),
|
||||
claude: () => [
|
||||
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
|
||||
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||
|
||||
@@ -187,3 +187,7 @@ export async function PATCH(request) {
|
||||
return NextResponse.json({ error: "Failed to update settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
return PATCH(request);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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-high": "antigravity/gemini-3-pro-preview",
|
||||
"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",
|
||||
"gemini-3-pro-preview": "antigravity/gemini-3-pro-preview",
|
||||
"gemini-3.1-pro-preview": "antigravity/gemini-3-pro-preview",
|
||||
"gemini-3.1-pro-preview-customtools": "antigravity/gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview": "antigravity/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
type SeedLogger = {
|
||||
|
||||
@@ -21,7 +21,7 @@ test("resolveAntigravityVersion uses the official release feed and caches the re
|
||||
let calls = 0;
|
||||
const fetchMock = async () => {
|
||||
calls += 1;
|
||||
return new Response(JSON.stringify([{ version: "1.23.2", execution_id: "4781536860569600" }]), {
|
||||
return new Response(JSON.stringify([{ version: "1.22.2", execution_id: "4781536860569600" }]), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -30,10 +30,10 @@ test("resolveAntigravityVersion uses the official release feed and caches the re
|
||||
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(first, "1.22.2");
|
||||
assert.equal(second, "1.22.2");
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(getCachedAntigravityVersion(), "1.23.2");
|
||||
assert.equal(getCachedAntigravityVersion(), "1.22.2");
|
||||
});
|
||||
|
||||
test("resolveAntigravityVersion refreshes the cache after the TTL elapses", async () => {
|
||||
@@ -41,7 +41,7 @@ test("resolveAntigravityVersion refreshes the cache after the TTL elapses", asyn
|
||||
Date.now = () => now;
|
||||
|
||||
const firstFetch = async () =>
|
||||
new Response(JSON.stringify([{ version: "1.23.2" }]), {
|
||||
new Response(JSON.stringify([{ version: "1.22.2" }]), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -51,7 +51,7 @@ test("resolveAntigravityVersion refreshes the cache after the TTL elapses", asyn
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
assert.equal(await resolveAntigravityVersion(firstFetch as typeof fetch), "1.23.2");
|
||||
assert.equal(await resolveAntigravityVersion(firstFetch as typeof fetch), "1.22.2");
|
||||
|
||||
now += ANTIGRAVITY_VERSION_CACHE_TTL_MS + 1;
|
||||
|
||||
|
||||
@@ -82,6 +82,48 @@ function buildResponsesSse(text = "Brasilia") {
|
||||
);
|
||||
}
|
||||
|
||||
function buildResponsesNdjson(text = "Brasilia") {
|
||||
return new Response(
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "response.created",
|
||||
response: {
|
||||
id: "resp_1",
|
||||
model: "gpt-5.3-codex",
|
||||
status: "in_progress",
|
||||
output: [],
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "response.output_text.delta",
|
||||
output_index: 0,
|
||||
delta: text,
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "response.completed",
|
||||
response: {
|
||||
id: "resp_1",
|
||||
object: "response",
|
||||
model: "gpt-5.3-codex",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 6, output_tokens: 1 },
|
||||
},
|
||||
}),
|
||||
].join("\n"),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/x-ndjson" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
@@ -196,6 +238,27 @@ test("chatCore converts Responses-style SSE fallback into JSON when stream=false
|
||||
assert.ok(payload.usage.completion_tokens > 0);
|
||||
});
|
||||
|
||||
test("chatCore converts Responses-style NDJSON fallback into JSON when stream=false", async () => {
|
||||
const { result, call } = await invokeChatCore({
|
||||
body: {
|
||||
model: "gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "Qual a capital do Brasil?" }],
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
responseFactory: () => buildResponsesNdjson("Brasilia"),
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.headers.Accept || call.headers.accept, "application/json");
|
||||
assert.equal(payload.object, "chat.completion");
|
||||
assert.equal(payload.choices[0].message.content, "Brasilia");
|
||||
assert.ok(payload.usage.total_tokens >= 7);
|
||||
});
|
||||
|
||||
test("handleComboChat validates non-stream quality using the original client stream intent", async () => {
|
||||
const combo = {
|
||||
name: "codex-stream-false-quality",
|
||||
|
||||
@@ -7,18 +7,21 @@ test("GeminiCLIExecutor.buildUrl and buildHeaders match the native Gemini CLI fi
|
||||
const executor = new GeminiCLIExecutor();
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("gemini-2.5-flash", true),
|
||||
executor.buildUrl("models/gemini-2.5-flash", true),
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
|
||||
);
|
||||
assert.equal(
|
||||
executor.buildUrl("gemini-2.5-flash", false),
|
||||
executor.buildUrl("models/gemini-2.5-flash", false),
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
|
||||
);
|
||||
|
||||
const headers = executor.buildHeaders({ accessToken: "gcli-token" }, true);
|
||||
assert.equal(headers.Authorization, "Bearer gcli-token");
|
||||
assert.equal(headers.Accept, "text/event-stream");
|
||||
assert.match(headers["User-Agent"], /GeminiCLI/);
|
||||
assert.match(
|
||||
headers["User-Agent"],
|
||||
/^GeminiCLI\/1\.0\.0\/gemini-2\.5-flash \((linux|macos|windows); (x64|arm64|x86)\)$/
|
||||
);
|
||||
assert.match(headers["X-Goog-Api-Client"], /google-genai-sdk/);
|
||||
});
|
||||
|
||||
@@ -65,6 +68,93 @@ test("GeminiCLIExecutor.refreshProject returns null on failed loadCodeAssist res
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.refreshProject onboards a managed project when loadCodeAssist has no project", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls = [];
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const body = init.body ? JSON.parse(String(init.body)) : null;
|
||||
calls.push({ url: String(url), body });
|
||||
|
||||
if (String(url).endsWith("loadCodeAssist")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
allowedTiers: [{ id: "free-tier", isDefault: true }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
if (String(url).endsWith("onboardUser")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
done: true,
|
||||
response: {
|
||||
cloudaicompanionProject: {
|
||||
id: "managed-project-id",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
try {
|
||||
assert.equal(await executor.refreshProject("access-token-3"), "managed-project-id");
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.url.split(":").at(-1)),
|
||||
["loadCodeAssist", "onboardUser"]
|
||||
);
|
||||
assert.equal(calls[0].body.cloudaicompanionProject, "default-project");
|
||||
assert.equal(calls[0].body.metadata.ideType, "ANTIGRAVITY");
|
||||
assert.equal(calls[0].body.metadata.duetProject, "default-project");
|
||||
assert.equal(calls[1].body.tierId, "free-tier");
|
||||
assert.equal(calls[1].body.metadata.ideType, "ANTIGRAVITY");
|
||||
assert.equal(calls[1].body.metadata.duetProject, "default-project");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.onboardManagedProject retries until completion", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let attempts = 0;
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
attempts += 1;
|
||||
assert.match(String(url), /onboardUser$/);
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
attempts === 1
|
||||
? { done: false }
|
||||
: {
|
||||
done: true,
|
||||
response: { cloudaicompanionProject: { id: "managed-project-retry" } },
|
||||
}
|
||||
),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
await executor.onboardManagedProject("access-token-4", "free-tier", {
|
||||
attempts: 2,
|
||||
delayMs: 0,
|
||||
}),
|
||||
"managed-project-retry"
|
||||
);
|
||||
assert.equal(attempts, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.refreshCredentials exchanges refresh tokens via Google OAuth", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -34,11 +34,11 @@ test("default model alias seed writes missing aliases and is idempotent", async
|
||||
|
||||
assert.deepEqual(first.failed, []);
|
||||
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-high"], "antigravity/gemini-3-pro-preview");
|
||||
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");
|
||||
assert.equal(aliases["gemini-3-pro-preview"], "antigravity/gemini-3-pro-preview");
|
||||
assert.equal(aliases["gemini-3.1-pro-preview"], "antigravity/gemini-3-pro-preview");
|
||||
assert.equal(aliases["gemini-3-flash-preview"], "antigravity/gemini-3-flash-preview");
|
||||
|
||||
const routed = await sseModelService.getModelInfo("gemini-3-pro-high");
|
||||
assert.deepEqual(routed, {
|
||||
|
||||
@@ -206,6 +206,26 @@ test("v1 models catalog exposes claude alias and provider-prefixed built-in mode
|
||||
assert.deepEqual(aliasModel.output_modalities, ["text"]);
|
||||
});
|
||||
|
||||
test("v1 models catalog exposes Antigravity client-visible preview aliases instead of upstream internal IDs", async () => {
|
||||
await seedConnection("antigravity", {
|
||||
authType: "oauth",
|
||||
name: "antigravity-preview",
|
||||
apiKey: null,
|
||||
accessToken: "antigravity-access",
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = await response.json();
|
||||
const ids = new Set(body.data.map((item) => item.id));
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(ids.has("antigravity/gemini-3-pro-preview"));
|
||||
assert.ok(ids.has("antigravity/gemini-3-flash-preview"));
|
||||
assert.equal(ids.has("antigravity/gemini-3.1-pro-high"), false);
|
||||
});
|
||||
|
||||
test("v1 models catalog uses provider-node prefixes for compatible provider custom models", async () => {
|
||||
await providersDb.createProviderNode({
|
||||
id: "anthropic-compatible-demo",
|
||||
|
||||
@@ -294,7 +294,7 @@ test("provider models route retries Antigravity discovery endpoints before retur
|
||||
apiKey: null,
|
||||
});
|
||||
const seenUrls = [];
|
||||
antigravityVersion.seedAntigravityVersionCache("1.23.2");
|
||||
antigravityVersion.seedAntigravityVersionCache("1.22.2");
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
seenUrls.push(String(url));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getSettings, updateSettings } from "../../src/lib/db/settings.ts";
|
||||
const settingsRoute = await import("../../src/app/api/settings/route.ts");
|
||||
|
||||
describe("Settings API - debugMode and hiddenSidebarItems", () => {
|
||||
describe("debugMode", () => {
|
||||
@@ -77,5 +78,19 @@ describe("Settings API - debugMode and hiddenSidebarItems", () => {
|
||||
"antigravitySignatureCacheMode should be updated"
|
||||
);
|
||||
});
|
||||
|
||||
test("PUT /api/settings reuses the PATCH update flow", async () => {
|
||||
const response = await settingsRoute.PUT(
|
||||
new Request("http://localhost/api/settings", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ antigravitySignatureCacheMode: "bypass" }),
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.antigravitySignatureCacheMode, "bypass");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,14 @@ test("T20: antigravity config has updated User-Agent and sandbox fallback URL",
|
||||
assert.equal(antigravity.headers["User-Agent"], antigravityUserAgent());
|
||||
});
|
||||
|
||||
test("T25: anthropic API-key config includes the full Anthropic beta header set", () => {
|
||||
const anthropic = REGISTRY.anthropic;
|
||||
assert.equal(anthropic.headers["Anthropic-Version"], "2023-06-01");
|
||||
assert.ok(anthropic.headers["Anthropic-Beta"]?.includes("advanced-tool-use-2025-11-20"));
|
||||
assert.ok(anthropic.headers["Anthropic-Beta"]?.includes("structured-outputs-2025-12-15"));
|
||||
assert.ok(anthropic.headers["Anthropic-Beta"]?.includes("token-efficient-tools-2026-03-28"));
|
||||
});
|
||||
|
||||
test("T22: github headers include updated editor/plugin versions and required fields", () => {
|
||||
const github = REGISTRY.github;
|
||||
assert.equal(github.headers["editor-version"], "vscode/1.110.0");
|
||||
|
||||
@@ -88,6 +88,43 @@ test("OpenAI -> Gemini helper cleans complex JSON Schema structures for Gemini c
|
||||
assert.equal(cleaned.properties.emptyObject.properties.reason.type, "string");
|
||||
});
|
||||
|
||||
test("OpenAI -> Gemini helper inlines local refs and preserves only additionalProperties=true", () => {
|
||||
const cleaned = cleanJSONSchemaForAntigravity({
|
||||
type: "object",
|
||||
$defs: {
|
||||
Address: {
|
||||
type: "object",
|
||||
properties: {
|
||||
street: { type: "string", minLength: 1 },
|
||||
},
|
||||
required: ["street"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
shipping: { $ref: "#/$defs/Address" },
|
||||
metadata: {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
},
|
||||
options: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["shipping"],
|
||||
});
|
||||
|
||||
assert.equal(cleaned.$defs, undefined);
|
||||
assert.equal(cleaned.properties.shipping.$ref, undefined);
|
||||
assert.equal(cleaned.properties.shipping.properties.street.type, "string");
|
||||
assert.equal(cleaned.properties.shipping.properties.street.minLength, undefined);
|
||||
assert.deepEqual(cleaned.properties.shipping.required, ["street"]);
|
||||
assert.equal(cleaned.properties.shipping.additionalProperties, undefined);
|
||||
assert.equal(cleaned.properties.metadata.additionalProperties, true);
|
||||
assert.equal(cleaned.properties.options.additionalProperties, undefined);
|
||||
});
|
||||
|
||||
test("OpenAI -> Gemini request maps messages, merged system instructions, tools and response schema", () => {
|
||||
const result = openaiToGeminiRequest(
|
||||
"gemini-2.5-pro",
|
||||
|
||||
Reference in New Issue
Block a user