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:
Paijo
2026-05-27 05:51:53 +07:00
committed by GitHub
parent bdadd44c4e
commit 3b27bd1dbd
8 changed files with 97 additions and 56 deletions

View File

@@ -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",

View File

@@ -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");

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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);
});

View File

@@ -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);
});