From 3b27bd1dbd3e7d2ce33a928d37d549db00c95f0f Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Wed, 27 May 2026 05:51:53 +0700 Subject: [PATCH] fix(antigravity): stabilize model detection, OAuth, and token refresh (#2757) Stabilized Antigravity model detection, OAuth parameters, token refresh, and PKCE transition --- open-sse/config/antigravityModelAliases.ts | 33 ++++-------- open-sse/executors/antigravity.ts | 53 ++++++++++++++++--- src/lib/oauth/constants/oauth.ts | 1 + src/lib/oauth/providers/antigravity.ts | 14 +++-- src/shared/constants/modelSpecs.ts | 17 ++++-- tests/unit/antigravity-model-aliases.test.ts | 19 ++++--- tests/unit/executor-antigravity.test.ts | 4 +- .../unit/t31-t33-t34-t38-model-specs.test.ts | 12 ++--- 8 files changed, 97 insertions(+), 56 deletions(-) diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts index 8f3551e585..2b069c3d94 100644 --- a/open-sse/config/antigravityModelAliases.ts +++ b/open-sse/config/antigravityModelAliases.ts @@ -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; 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", diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 6e14c2aedf..4900021652 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -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 = 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 = { + 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; + 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; log?.info?.("TOKEN", "Antigravity refreshed"); diff --git a/src/lib/oauth/constants/oauth.ts b/src/lib/oauth/constants/oauth.ts index 6dfa1ee5ab..c535e68c87 100644 --- a/src/lib/oauth/constants/oauth.ts +++ b/src/lib/oauth/constants/oauth.ts @@ -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", diff --git a/src/lib/oauth/providers/antigravity.ts b/src/lib/oauth/providers/antigravity.ts index 06ef9ea510..86277ae3fb 100644 --- a/src/lib/oauth/providers/antigravity.ts +++ b/src/lib/oauth/providers/antigravity.ts @@ -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 = { 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: { diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 6fd35ba3f7..6a7d5e3712 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -56,8 +56,8 @@ export const MODEL_SPECS: Record = { 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 = { 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 = { ], }, - // ── 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 = { 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, diff --git a/tests/unit/antigravity-model-aliases.test.ts b/tests/unit/antigravity-model-aliases.test.ts index 177dc6a67b..511d7e7030 100644 --- a/tests/unit/antigravity-model-aliases.test.ts +++ b/tests/unit/antigravity-model-aliases.test.ts @@ -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 () => { diff --git a/tests/unit/executor-antigravity.test.ts b/tests/unit/executor-antigravity.test.ts index e7d067fcaf..04fb1f0d23 100644 --- a/tests/unit/executor-antigravity.test.ts +++ b/tests/unit/executor-antigravity.test.ts @@ -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); }); diff --git a/tests/unit/t31-t33-t34-t38-model-specs.test.ts b/tests/unit/t31-t33-t34-t38-model-specs.test.ts index 3220969dc1..47b7c7fdc7 100644 --- a/tests/unit/t31-t33-t34-t38-model-specs.test.ts +++ b/tests/unit/t31-t33-t34-t38-model-specs.test.ts @@ -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); });