mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
fix(antigravity): stabilize model detection, OAuth, and token refresh (#2757)
Stabilized Antigravity model detection, OAuth parameters, token refresh, and PKCE transition
This commit is contained in:
@@ -2,16 +2,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
|
||||
// Gemini 3.5 Flash — flagship model in Antigravity 2.0 (May 2026)
|
||||
{
|
||||
id: "gemini-3.5-flash-preview",
|
||||
name: "Gemini 3.5 Flash (High)",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 65536,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
toolCalling: true,
|
||||
},
|
||||
{
|
||||
id: "gemini-3.5-flash-low",
|
||||
name: "Gemini 3.5 Flash (Low)",
|
||||
name: "Gemini 3.5 Flash",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 65536,
|
||||
supportsReasoning: true,
|
||||
@@ -29,16 +20,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
|
||||
},
|
||||
{
|
||||
id: "gemini-3-pro-preview",
|
||||
name: "Gemini 3.1 Pro (High)",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 65535,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
toolCalling: true,
|
||||
},
|
||||
{
|
||||
id: "gemini-3.1-pro-low",
|
||||
name: "Gemini 3.1 Pro (Low)",
|
||||
name: "Gemini 3.1 Pro",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 65535,
|
||||
supportsReasoning: true,
|
||||
@@ -116,9 +98,12 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
|
||||
},
|
||||
]);
|
||||
|
||||
// The Antigravity upstream API uses plain model IDs (no -high/-low suffix).
|
||||
// The -high/-low suffix convention was speculative and caused 404 for all
|
||||
// gemini-3.x models. Only plain IDs like "gemini-2.5-flash" are proven working.
|
||||
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
|
||||
"gemini-3-pro-preview": "gemini-3.1-pro-high",
|
||||
"gemini-3.5-flash-preview": "gemini-3.5-flash-high",
|
||||
"gemini-3-pro-preview": "gemini-3.1-pro",
|
||||
"gemini-3.5-flash-preview": "gemini-3.5-flash",
|
||||
"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",
|
||||
@@ -132,8 +117,8 @@ export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
|
||||
type AntigravityModelAliasMap = Record<string, string>;
|
||||
|
||||
export const ANTIGRAVITY_REVERSE_MODEL_ALIASES: AntigravityModelAliasMap = Object.freeze({
|
||||
"gemini-3.1-pro-high": "gemini-3-pro-preview",
|
||||
"gemini-3.5-flash-high": "gemini-3.5-flash-preview",
|
||||
"gemini-3.1-pro": "gemini-3-pro-preview",
|
||||
"gemini-3.5-flash": "gemini-3.5-flash-preview",
|
||||
"gemini-3-flash-agent": "gemini-3.5-flash-preview",
|
||||
"gemini-3-flash": "gemini-3-flash-preview",
|
||||
"gemini-3-pro-image": "gemini-3-pro-image-preview",
|
||||
|
||||
@@ -52,7 +52,10 @@ import {
|
||||
const MAX_RETRY_AFTER_MS = 60_000;
|
||||
const LONG_RETRY_THRESHOLD_MS = 60_000;
|
||||
const CREDITS_EXHAUSTED_TTL_MS = 5 * 60 * 60 * 1000; // 5 hours
|
||||
const BARE_PRO_IDS = new Set(["gemini-3.1-pro"]);
|
||||
// The upstream API uses plain model IDs (no -high/-low suffix).
|
||||
// Tier suffixes were speculative and caused 404 for gemini-3.x models.
|
||||
// Only keep models that are live-proven via streamGenerateContent.
|
||||
const BARE_PRO_IDS: Set<string> = new Set();
|
||||
|
||||
interface AntigravityContent {
|
||||
role: string;
|
||||
@@ -482,6 +485,24 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
return resp as unknown as never;
|
||||
}
|
||||
|
||||
// Validate projectId is non-empty and not just whitespace
|
||||
const trimmedProjectId = typeof projectId === "string" ? projectId.trim() : projectId;
|
||||
if (!trimmedProjectId) {
|
||||
const resp = new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message:
|
||||
"Invalid (empty) Google projectId for Antigravity account. " +
|
||||
"Please reconnect OAuth in Providers → Antigravity.",
|
||||
type: "oauth_missing_project_id",
|
||||
code: "missing_project_id",
|
||||
},
|
||||
}),
|
||||
{ status: 422, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return resp as unknown as never;
|
||||
}
|
||||
|
||||
const upstreamModel = cleanModelName(model);
|
||||
const isClaude = upstreamModel.toLowerCase().includes("claude");
|
||||
const baseBody = bodyRecord;
|
||||
@@ -594,6 +615,15 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
if (!credentials.refreshToken) return null;
|
||||
|
||||
try {
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: credentials.refreshToken,
|
||||
};
|
||||
// Only include non-empty client_id/client_secret — Google OAuth rejects
|
||||
// empty params which raw URLSearchParams produces (buildFormParams semantics).
|
||||
if (this.config.clientId) bodyParams.client_id = this.config.clientId;
|
||||
if (this.config.clientSecret) bodyParams.client_secret = this.config.clientSecret;
|
||||
|
||||
const response = await fetch(OAUTH_ENDPOINTS.google.token, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -601,15 +631,22 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
Accept: "application/json",
|
||||
"User-Agent": antigravityNativeOAuthUserAgent(),
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: credentials.refreshToken || "",
|
||||
client_id: this.config.clientId || "",
|
||||
client_secret: this.config.clientSecret || "",
|
||||
}),
|
||||
body: new URLSearchParams(bodyParams),
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
if (!response.ok) {
|
||||
// Detect unrecoverable token (invalid_grant = revoked / expired refresh token)
|
||||
try {
|
||||
const errorBody = (await response.json()) as Record<string, unknown>;
|
||||
if (errorBody.error === "invalid_grant") {
|
||||
log?.error?.("TOKEN", "Antigravity refresh token revoked. Re-authentication required.");
|
||||
return { error: "unrecoverable_refresh_error" } as unknown as AntigravityCredentials;
|
||||
}
|
||||
} catch {
|
||||
// not JSON — fall through
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = (await response.json()) as Record<string, unknown>;
|
||||
log?.info?.("TOKEN", "Antigravity refreshed");
|
||||
|
||||
@@ -164,6 +164,7 @@ export const ANTIGRAVITY_CONFIG = {
|
||||
tokenUrl: "https://oauth2.googleapis.com/token",
|
||||
userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
scopes: [
|
||||
"openid",
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
|
||||
@@ -22,8 +22,8 @@ async function fetchFirstOk(endpoints: string[], init: RequestInit) {
|
||||
|
||||
export const antigravity = {
|
||||
config: ANTIGRAVITY_CONFIG,
|
||||
flowType: "authorization_code",
|
||||
buildAuthUrl: (config, redirectUri, state) => {
|
||||
flowType: "authorization_code_pkce",
|
||||
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
response_type: "code",
|
||||
@@ -33,9 +33,13 @@ export const antigravity = {
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
});
|
||||
if (codeChallenge) {
|
||||
params.set("code_challenge", codeChallenge);
|
||||
params.set("code_challenge_method", "S256");
|
||||
}
|
||||
return `${config.authorizeUrl}?${params.toString()}`;
|
||||
},
|
||||
exchangeToken: async (config, code, redirectUri) => {
|
||||
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "authorization_code",
|
||||
client_id: config.clientId,
|
||||
@@ -47,6 +51,10 @@ export const antigravity = {
|
||||
bodyParams.client_secret = config.clientSecret;
|
||||
}
|
||||
|
||||
if (codeVerifier) {
|
||||
bodyParams.code_verifier = codeVerifier;
|
||||
}
|
||||
|
||||
const response = await fetch(config.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -56,8 +56,8 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
aliases: ["gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"],
|
||||
},
|
||||
|
||||
// ── Gemini 3.1 Pro High ─────────────────────────────────────────
|
||||
"gemini-3.1-pro-high": {
|
||||
// ── Gemini 3.1 Pro ───────────────────────────────────────────────
|
||||
"gemini-3.1-pro": {
|
||||
maxOutputTokens: 65535,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 24576,
|
||||
@@ -67,6 +67,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: [
|
||||
"gemini-3.1-pro-high",
|
||||
"gemini-3-pro-high",
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3.1-pro-preview",
|
||||
@@ -74,7 +75,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
],
|
||||
},
|
||||
|
||||
// ── Gemini 3.1 Pro Low ──────────────────────────────────────────
|
||||
// ── Gemini 3.1 Pro Low (deprecated, kept for back-compat) ────────
|
||||
"gemini-3.1-pro-low": {
|
||||
maxOutputTokens: 65535,
|
||||
contextWindow: 1048576,
|
||||
@@ -86,6 +87,16 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
aliases: ["gemini-3-pro-low"],
|
||||
},
|
||||
|
||||
// ── Gemini 3.5 Flash ─────────────────────────────────────────────
|
||||
"gemini-3.5-flash": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
supportsThinking: false,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: ["gemini-3.5-flash-high"],
|
||||
},
|
||||
|
||||
// ── Claude Opus 4.5 ─────────────────────────────────────────────
|
||||
"claude-opus-4-5": {
|
||||
maxOutputTokens: 32768,
|
||||
|
||||
@@ -16,8 +16,8 @@ function getPublicModel(id: string) {
|
||||
}
|
||||
|
||||
test("resolveAntigravityModelId maps the documented Antigravity aliases to upstream IDs", () => {
|
||||
assert.equal(resolveAntigravityModelId("gemini-3-pro-preview"), "gemini-3.1-pro-high");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-preview"), "gemini-3.5-flash-high");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3-pro-preview"), "gemini-3.1-pro");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-preview"), "gemini-3.5-flash");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3-flash-preview"), "gemini-3-flash");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3-pro-image-preview"), "gemini-3-pro-image");
|
||||
assert.equal(
|
||||
@@ -34,7 +34,7 @@ test("resolveAntigravityModelId maps the documented Antigravity aliases to upstr
|
||||
});
|
||||
|
||||
test("toClientAntigravityModelId exposes client-visible aliases for known upstream IDs", () => {
|
||||
assert.equal(toClientAntigravityModelId("gemini-3.1-pro-high"), "gemini-3-pro-preview");
|
||||
assert.equal(toClientAntigravityModelId("gemini-3.1-pro"), "gemini-3-pro-preview");
|
||||
assert.equal(toClientAntigravityModelId("gemini-3-flash-agent"), "gemini-3.5-flash-preview");
|
||||
assert.equal(toClientAntigravityModelId("gemini-3-flash"), "gemini-3-flash-preview");
|
||||
assert.equal(toClientAntigravityModelId("gpt-oss-120b-medium"), "gpt-oss-120b-medium");
|
||||
@@ -44,10 +44,9 @@ test("toClientAntigravityModelId exposes client-visible aliases for known upstre
|
||||
|
||||
test("isUserCallableAntigravityModelId only allows public chat-capable model IDs", () => {
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3-pro-preview"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro-high"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-preview"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3-flash-agent"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-low"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-flash-lite"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-pro"), true);
|
||||
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash"), true);
|
||||
@@ -67,7 +66,7 @@ test("ANTIGRAVITY_PUBLIC_MODELS exposes captured Antigravity 2.0.1 names and cap
|
||||
assert.equal(getPublicModel("claude-opus-4-6-thinking"), undefined);
|
||||
assert.deepEqual(getPublicModel("gemini-3.5-flash-preview"), {
|
||||
id: "gemini-3.5-flash-preview",
|
||||
name: "Gemini 3.5 Flash (High)",
|
||||
name: "Gemini 3.5 Flash",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 65536,
|
||||
supportsReasoning: true,
|
||||
@@ -75,8 +74,8 @@ test("ANTIGRAVITY_PUBLIC_MODELS exposes captured Antigravity 2.0.1 names and cap
|
||||
toolCalling: true,
|
||||
});
|
||||
assert.equal(
|
||||
getClientVisibleAntigravityModelName("gemini-3.5-flash-low"),
|
||||
"Gemini 3.5 Flash (Low)"
|
||||
getClientVisibleAntigravityModelName("gemini-3.5-flash-preview"),
|
||||
"Gemini 3.5 Flash"
|
||||
);
|
||||
assert.equal(getClientVisibleAntigravityModelName("gemini-2.5-flash"), "Gemini 2.5 Flash");
|
||||
assert.equal(
|
||||
@@ -136,7 +135,7 @@ test("AntigravityExecutor.transformRequest resolves alias models before dispatch
|
||||
);
|
||||
|
||||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||||
assert.equal(result.model, "gemini-3.1-pro-high");
|
||||
assert.equal(result.model, "gemini-3.1-pro");
|
||||
});
|
||||
|
||||
test("AntigravityExecutor.transformRequest resolves Gemini 3.5 Flash alias upstream", async () => {
|
||||
@@ -153,7 +152,7 @@ test("AntigravityExecutor.transformRequest resolves Gemini 3.5 Flash alias upstr
|
||||
);
|
||||
|
||||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||||
assert.equal(result.model, "gemini-3.5-flash-high");
|
||||
assert.equal(result.model, "gemini-3.5-flash");
|
||||
});
|
||||
|
||||
test("AntigravityExecutor.transformRequest sends Claude through Gemini-compatible Cloud Code schema", async () => {
|
||||
|
||||
@@ -128,7 +128,7 @@ test("AntigravityExecutor.transformRequest normalizes model, project and content
|
||||
|
||||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||||
assert.equal(result.project, "project-1");
|
||||
assert.equal(result.model, "gemini-3.1-pro-low");
|
||||
assert.equal(result.model, "gemini-3.1-pro");
|
||||
assert.deepEqual(Object.keys(result), [
|
||||
"project",
|
||||
"requestId",
|
||||
@@ -215,7 +215,7 @@ test("AntigravityExecutor.transformRequest tolerates a missing body when project
|
||||
|
||||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||||
assert.equal(result.project, "project-1");
|
||||
assert.equal(result.model, "gemini-3.1-pro-low");
|
||||
assert.equal(result.model, "gemini-3.1-pro");
|
||||
assert.ok(result.request.sessionId);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ test("T31: antigravity static catalog exposes client-visible Gemini preview IDs"
|
||||
// still accepts its internal model identifiers.
|
||||
const staticIds = (getStaticModelsForProvider("antigravity") || []).map((m) => m.id);
|
||||
assert.ok(staticIds.includes("gemini-3-pro-preview"));
|
||||
assert.ok(staticIds.includes("gemini-3.1-pro-low"));
|
||||
assert.ok(!staticIds.includes("gemini-claude-sonnet-4-5"));
|
||||
assert.ok(!staticIds.includes("gemini-3.1-pro-low"));
|
||||
assert.ok(!staticIds.includes("claude-sonnet-4-6"));
|
||||
assert.ok(!staticIds.includes("gemini-claude-sonnet-4-5-thinking"));
|
||||
assert.ok(!staticIds.includes("gemini-claude-opus-4-5-thinking"));
|
||||
});
|
||||
@@ -56,7 +56,7 @@ test("T34: max output tokens are capped by model spec", () => {
|
||||
test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", () => {
|
||||
assert.equal(getModelSpec("gpt-5.5").contextWindow, 1050000);
|
||||
assert.equal(getModelSpec("gpt-5.5-high").maxOutputTokens, 128000);
|
||||
assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object");
|
||||
assert.equal(typeof MODEL_SPECS["gemini-3.1-pro"], "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);
|
||||
@@ -65,9 +65,9 @@ test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup",
|
||||
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(resolveModelAlias("gemini-3-pro-preview"), "gemini-3.1-pro");
|
||||
assert.equal(resolveModelAlias("gemini-3.1-pro-preview"), "gemini-3.1-pro");
|
||||
assert.equal(resolveModelAlias("gemini-3.1-pro-preview-customtools"), "gemini-3.1-pro");
|
||||
assert.equal(getDefaultThinkingBudget("gemini-3.1-pro-high"), 24576);
|
||||
assert.equal(capThinkingBudget("gemini-3.1-pro-low", 50000), 16000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user