pi-ai: remove google-antigravity provider entirely

Continues the antigravity rip-out (previous commit covered SF + pi-coding-
agent UI layer). This commit removes the code from pi-ai:

- Delete packages/pi-ai/src/utils/oauth/google-antigravity.ts (313 lines)
- Update oauth/index.ts: drop antigravityOAuthProvider, refreshAntigravityToken,
  loginAntigravity exports + registry entry. Add comment explaining why
  (no vendor core lib + Google ban risk).
- google-gemini-cli.ts: strip ANTIGRAVITY_* constants, ANTIGRAVITY_ENDPOINT_FALLBACKS,
  getAntigravityHeaders(), ANTIGRAVITY_SYSTEM_INSTRUCTION, and all
  isAntigravity branching from streamGoogleGeminiCli + buildRequest.
  File header rewritten. needsClaudeThinkingBetaHeader() collapses to
  always-false (antigravity was the only path that needed it).
- google-shared.ts: strip stale Antigravity comments (file still shared
  between google, google-gemini-cli, google-vertex).
- types.ts: drop "google-antigravity" from Api / KnownProvider union.
- models.generated.ts: remove google-antigravity provider block (~170 lines,
  4 claude-* models that were only served via Antigravity).
- models.generated.test.ts: drop from expected-providers snapshot.
- scripts/generate-models.ts: remove antigravity model emission + context-
  window override so future regenerations don't re-add it.

Reasoning (same as previous commit): Antigravity has no vendor-published
core library we can embed. Hand-rolled OAuth against
daily-cloudcode-pa.sandbox.googleapis.com was exactly the pattern
Google is banning for third-party tools. Removing it eliminates the
risk surface.

Breaking change: users with google-antigravity configured in their
models.* block will need to migrate to google-gemini-cli (OAuth via
the real `gemini` CLI), google (API key), or google-vertex (GCP auth).

Build passes. Next commit wires the google-gemini-cli provider to
@google/gemini-cli-core per the plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-19 10:45:44 +02:00
parent 59806f8cc5
commit bae6553e67
9 changed files with 27 additions and 668 deletions

View file

@ -681,12 +681,6 @@ async function generateModels() {
) {
candidate.contextWindow = 1000000;
}
if (
candidate.provider === "google-antigravity" &&
(candidate.id === "claude-opus-4-6-thinking" || candidate.id === "claude-sonnet-4-6")
) {
candidate.contextWindow = 1000000;
}
// OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K
if (
(candidate.provider === "opencode" || candidate.provider === "opencode-go") &&
@ -1145,122 +1139,6 @@ async function generateModels() {
];
allModels.push(...cloudCodeAssistModels);
// Antigravity models (Gemini 3, Claude, GPT-OSS via Google Cloud)
// Uses sandbox endpoint and different OAuth credentials for access to additional models
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
const antigravityModels: Model<"google-gemini-cli">[] = [
{
id: "gemini-3.1-pro-high",
name: "Gemini 3.1 Pro High (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
// the Model type doesn't seem to support having extended-context costs, so I'm just using the pricing for <200k input
cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.375 },
contextWindow: 1048576,
maxTokens: 65535,
},
{
id: "gemini-3.1-pro-low",
name: "Gemini 3.1 Pro Low (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
// the Model type doesn't seem to support having extended-context costs, so I'm just using the pricing for <200k input
cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.375 },
contextWindow: 1048576,
maxTokens: 65535,
},
{
id: "gemini-3-flash",
name: "Gemini 3 Flash (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
cost: { input: 0.5, output: 3, cacheRead: 0.5, cacheWrite: 0 },
contextWindow: 1048576,
maxTokens: 65535,
},
{
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5 (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: false,
input: ["text", "image"],
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
contextWindow: 200000,
maxTokens: 64000,
},
{
id: "claude-sonnet-4-5-thinking",
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
contextWindow: 200000,
maxTokens: 64000,
},
{
id: "claude-opus-4-5-thinking",
name: "Claude Opus 4.5 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 200000,
maxTokens: 64000,
},
{
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 200000,
maxTokens: 128000,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: true,
input: ["text", "image"],
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
contextWindow: 200000,
maxTokens: 64000,
},
{
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B Medium (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_ENDPOINT,
reasoning: false,
input: ["text"],
cost: { input: 0.09, output: 0.36, cacheRead: 0, cacheWrite: 0 },
contextWindow: 131072,
maxTokens: 32768,
},
];
allModels.push(...antigravityModels);
const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com";
const vertexModels: Model<"google-vertex">[] = [

View file

@ -278,7 +278,6 @@ describe("MODELS registry shape", () => {
"cerebras",
"github-copilot",
"google",
"google-antigravity",
"google-gemini-cli",
"google-vertex",
"groq",

View file

@ -3600,178 +3600,6 @@ export const MODELS = {
maxTokens: 8192,
} satisfies Model<"google-generative-ai">,
},
"google-antigravity": {
"claude-opus-4-5-thinking": {
id: "claude-opus-4-5-thinking",
name: "Claude Opus 4.5 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 5,
output: 25,
cacheRead: 0.5,
cacheWrite: 6.25,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"google-gemini-cli">,
"claude-opus-4-7-thinking": {
id: "claude-opus-4-7-thinking",
name: "Claude Opus 4.7 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 5,
output: 25,
cacheRead: 0.5,
cacheWrite: 6.25,
},
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"google-gemini-cli">,
"claude-opus-4-6-thinking": {
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 5,
output: 25,
cacheRead: 0.5,
cacheWrite: 6.25,
},
contextWindow: 200000,
maxTokens: 128000,
} satisfies Model<"google-gemini-cli">,
"claude-sonnet-4-5": {
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5 (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: false,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"google-gemini-cli">,
"claude-sonnet-4-5-thinking": {
id: "claude-sonnet-4-5-thinking",
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"google-gemini-cli">,
"claude-sonnet-4-6": {
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"google-gemini-cli">,
"gemini-3-flash": {
id: "gemini-3-flash",
name: "Gemini 3 Flash (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.5,
output: 3,
cacheRead: 0.5,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 65535,
} satisfies Model<"google-gemini-cli">,
"gemini-3.1-pro-high": {
id: "gemini-3.1-pro-high",
name: "Gemini 3.1 Pro High (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 12,
cacheRead: 0.2,
cacheWrite: 2.375,
},
contextWindow: 1048576,
maxTokens: 65535,
} satisfies Model<"google-gemini-cli">,
"gemini-3.1-pro-low": {
id: "gemini-3.1-pro-low",
name: "Gemini 3.1 Pro Low (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 12,
cacheRead: 0.2,
cacheWrite: 2.375,
},
contextWindow: 1048576,
maxTokens: 65535,
} satisfies Model<"google-gemini-cli">,
"gpt-oss-120b-medium": {
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B Medium (Antigravity)",
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
reasoning: false,
input: ["text"],
cost: {
input: 0.09,
output: 0.36,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 32768,
} satisfies Model<"google-gemini-cli">,
},
"google-gemini-cli": {
"gemini-2.0-flash": {
id: "gemini-2.0-flash",

View file

@ -1,7 +1,10 @@
/**
* Google Gemini CLI / Antigravity provider.
* Shared implementation for both google-gemini-cli and google-antigravity providers.
* Uses the Cloud Code Assist API endpoint to access Gemini and Claude models.
* Google Gemini CLI provider.
* Uses the Cloud Code Assist API endpoint (cloudcode-pa.googleapis.com) to
* access Gemini models with the user's OAuth credentials from the real
* `gemini` CLI. Follow-up work will re-platform this on top of
* @google/gemini-cli-core so our requests are indistinguishable from the
* official CLI's. See google-gemini-cli-core-plan.md.
*/
import type { Content, ThinkingConfig } from "@google/genai";
@ -58,13 +61,6 @@ export interface GoogleGeminiCliOptions extends StreamOptions {
}
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
const ANTIGRAVITY_AUTOPUSH_ENDPOINT = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
const ANTIGRAVITY_ENDPOINT_FALLBACKS = [
ANTIGRAVITY_DAILY_ENDPOINT,
ANTIGRAVITY_AUTOPUSH_ENDPOINT,
DEFAULT_ENDPOINT,
] as const;
// Headers for Gemini CLI (prod endpoint)
const GEMINI_CLI_HEADERS = {
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
@ -76,23 +72,6 @@ const GEMINI_CLI_HEADERS = {
}),
};
// Headers for Antigravity (sandbox endpoint) - requires specific User-Agent
const DEFAULT_ANTIGRAVITY_VERSION = "1.18.4";
function getAntigravityHeaders() {
const version = process.env.PI_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
return {
"User-Agent": `antigravity/${version} darwin/arm64`,
};
}
// Antigravity system instruction (compact version from CLIProxyAPI).
const ANTIGRAVITY_SYSTEM_INSTRUCTION =
"You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding." +
"You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question." +
"**Absolute paths only**" +
"**Proactiveness**";
// Counter for generating unique tool call IDs
let toolCallCounter = 0;
@ -203,8 +182,11 @@ function extractRetryDelay(errorText: string, response?: Response | Headers): nu
return undefined;
}
function needsClaudeThinkingBetaHeader(model: Model<"google-gemini-cli">): boolean {
return model.provider === "google-antigravity" && model.id.startsWith("claude-") && model.reasoning;
function needsClaudeThinkingBetaHeader(_model: Model<"google-gemini-cli">): boolean {
// Antigravity-only path. Gemini CLI doesn't serve Claude models so this is
// always false now. Kept as a no-op to minimise downstream edits; can be
// inlined away once the streaming function is rewritten on cli-core.
return false;
}
function isGemini3ProModel(modelId: string): boolean {
@ -365,16 +347,15 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGe
throw new Error("Missing token or projectId in Google Cloud credentials. Use /login to re-authenticate.");
}
const isAntigravity = model.provider === "google-antigravity";
const baseUrl = model.baseUrl?.trim();
const endpoints = baseUrl ? [baseUrl] : isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT];
const endpoints = baseUrl ? [baseUrl] : [DEFAULT_ENDPOINT];
let requestBody = buildRequest(model, context, projectId, options, isAntigravity);
let requestBody = buildRequest(model, context, projectId, options);
const nextRequestBody = await options?.onPayload?.(requestBody, model);
if (nextRequestBody !== undefined) {
requestBody = nextRequestBody as CloudCodeAssistRequest;
}
const headers = isAntigravity ? getAntigravityHeaders() : GEMINI_CLI_HEADERS;
const headers = GEMINI_CLI_HEADERS;
const requestHeaders = {
Authorization: `Bearer ${accessToken}`,
@ -870,7 +851,6 @@ function buildRequest(
context: Context,
projectId: string,
options: GoogleGeminiCliOptions = {},
isAntigravity = false,
): CloudCodeAssistRequest {
const contents = convertMessages(model, context);
@ -927,25 +907,12 @@ function buildRequest(
}
}
if (isAntigravity) {
const existingParts = request.systemInstruction?.parts ?? [];
request.systemInstruction = {
role: "user",
parts: [
{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
{ text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
...existingParts,
],
};
}
return {
project: projectId,
model: model.id,
request,
...(isAntigravity ? { requestType: "agent" } : {}),
userAgent: isAntigravity ? "antigravity" : "pi-coding-agent",
requestId: `${isAntigravity ? "agent" : "pi"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
userAgent: "pi-coding-agent",
requestId: `pi-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
};
}

View file

@ -116,7 +116,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
for (const block of msg.content) {
if (block.type === "text") {
// Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity)
// Skip empty text blocks - they can cause issues with some models
if (!block.text || block.text.trim() === "") continue;
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature);
parts.push({
@ -144,7 +144,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
// Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled.
// Use the skip_thought_signature_validator sentinel for unsigned function calls
// (e.g. replayed from providers without thought signatures like Claude via Antigravity).
// (e.g. replayed from providers without thought signatures).
const isGemini3 = model.id.toLowerCase().includes("gemini-3");
const effectiveSignature = thoughtSignature || (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined);
const part: Part = {
@ -234,8 +234,8 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
* - Removes all `patternProperties` fields
* - Converts `const: "value"` to `enum: ["value"]` in anyOf/oneOf blocks
*
* This is needed for providers like `google-antigravity` when proxying Claude models,
* since Google Cloud Code Assist uses a restricted subset of JSON Schema.
* Needed because Google Cloud Code Assist (google-gemini-cli provider) uses a
* restricted subset of JSON Schema and rejects patternProperties / const.
*/
export function sanitizeSchemaForGoogle(schema: unknown): unknown {
if (!schema || typeof schema !== "object") {

View file

@ -24,7 +24,6 @@ export type KnownProvider =
| "anthropic-vertex"
| "google"
| "google-gemini-cli"
| "google-antigravity"
| "google-vertex"
| "openai"
| "azure-openai-responses"

View file

@ -1,313 +0,0 @@
/**
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
*
* NOTE: This module uses Node.js http.createServer for the OAuth callback.
* It is only intended for CLI use, not browser environments.
*/
import {
type CallbackServerInfo,
getGoogleUserEmail,
parseRedirectUrl,
refreshGoogleOAuthToken,
startCallbackServer,
} from "./google-oauth-utils.js";
import { generatePKCE } from "./pkce.js";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js";
type AntigravityCredentials = OAuthCredentials & {
projectId: string;
};
// Antigravity OAuth credentials (different from Gemini CLI)
const decode = (s: string) => atob(s);
const CLIENT_ID = decode(
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
);
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
// Antigravity requires additional scopes
const SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
];
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
// Callback server configuration
const CALLBACK_PORT = 51121;
const CALLBACK_PATH = "/oauth-callback";
// Fallback project ID when discovery fails
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
interface LoadCodeAssistPayload {
cloudaicompanionProject?: string | { id?: string };
currentTier?: { id?: string };
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
}
/**
* Discover or provision a project for the user
*/
async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
}),
};
// Try endpoints in order: prod first, then sandbox
const endpoints = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"];
onProgress?.("Checking for existing project...");
for (const endpoint of endpoints) {
try {
const loadResponse = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
method: "POST",
headers,
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
}),
signal: AbortSignal.timeout(30_000),
});
if (loadResponse.ok) {
const data = (await loadResponse.json()) as LoadCodeAssistPayload;
// Handle both string and object formats
if (typeof data.cloudaicompanionProject === "string" && data.cloudaicompanionProject) {
return data.cloudaicompanionProject;
}
if (
data.cloudaicompanionProject &&
typeof data.cloudaicompanionProject === "object" &&
data.cloudaicompanionProject.id
) {
return data.cloudaicompanionProject.id;
}
}
} catch {
// Try next endpoint
}
}
// Use fallback project ID
onProgress?.("Using default project...");
return DEFAULT_PROJECT_ID;
}
/**
* Refresh Antigravity token
*/
export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
return refreshGoogleOAuthToken(refreshToken, CLIENT_ID, CLIENT_SECRET, "Antigravity", { projectId });
}
/**
* Login with Antigravity OAuth
*
* @param onAuth - Callback with URL and optional instructions
* @param onProgress - Optional progress callback
* @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.
* Races with browser callback - whichever completes first wins.
*/
export async function loginAntigravity(
onAuth: (info: { url: string; instructions?: string }) => void,
onProgress?: (message: string) => void,
onManualCodeInput?: () => Promise<string>,
): Promise<OAuthCredentials> {
const { verifier, challenge } = await generatePKCE();
// Start local server for callback
onProgress?.("Starting local server for OAuth callback...");
const server: CallbackServerInfo = await startCallbackServer(CALLBACK_PORT, CALLBACK_PATH, "Antigravity");
let code: string | undefined;
try {
// Build authorization URL
const authParams = new URLSearchParams({
client_id: CLIENT_ID,
response_type: "code",
redirect_uri: REDIRECT_URI,
scope: SCOPES.join(" "),
code_challenge: challenge,
code_challenge_method: "S256",
state: verifier,
access_type: "offline",
prompt: "consent",
});
const authUrl = `${AUTH_URL}?${authParams.toString()}`;
// Notify caller with URL to open
onAuth({
url: authUrl,
instructions: "Complete the sign-in in your browser.",
});
// Wait for the callback, racing with manual input if provided
onProgress?.("Waiting for OAuth callback...");
if (onManualCodeInput) {
// Race between browser callback and manual input
let manualInput: string | undefined;
let manualError: Error | undefined;
const manualPromise = onManualCodeInput()
.then((input) => {
manualInput = input;
server.cancelWait();
})
.catch((err) => {
manualError = err instanceof Error ? err : new Error(String(err));
server.cancelWait();
});
const result = await server.waitForCode();
// If manual input was cancelled, throw that error
if (manualError) {
throw manualError;
}
if (result?.code) {
// Browser callback won - verify state
if (result.state !== verifier) {
throw new Error("OAuth state mismatch - possible CSRF attack");
}
code = result.code;
} else if (manualInput) {
// Manual input won
const parsed = parseRedirectUrl(manualInput);
if (parsed.state && parsed.state !== verifier) {
throw new Error("OAuth state mismatch - possible CSRF attack");
}
code = parsed.code;
}
// If still no code, wait for manual promise and try that
if (!code) {
await manualPromise;
if (manualError) {
throw manualError;
}
if (manualInput) {
const parsed = parseRedirectUrl(manualInput);
if (parsed.state && parsed.state !== verifier) {
throw new Error("OAuth state mismatch - possible CSRF attack");
}
code = parsed.code;
}
}
} else {
// Original flow: just wait for callback
const result = await server.waitForCode();
if (result?.code) {
if (result.state !== verifier) {
throw new Error("OAuth state mismatch - possible CSRF attack");
}
code = result.code;
}
}
if (!code) {
throw new Error("No authorization code received");
}
// Exchange code for tokens
onProgress?.("Exchanging authorization code for tokens...");
const tokenResponse = await fetch(TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
}),
signal: AbortSignal.timeout(30_000),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}
const tokenData = (await tokenResponse.json()) as {
access_token: string;
refresh_token: string;
expires_in: number;
};
if (!tokenData.refresh_token) {
throw new Error("No refresh token received. Please try again.");
}
// Get user email
onProgress?.("Getting user info...");
const email = await getGoogleUserEmail(tokenData.access_token);
// Discover project
const projectId = await discoverProject(tokenData.access_token, onProgress);
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
const credentials: OAuthCredentials = {
refresh: tokenData.refresh_token,
access: tokenData.access_token,
expires: expiresAt,
projectId,
email,
};
return credentials;
} finally {
server.server.close();
}
}
export const antigravityOAuthProvider: OAuthProviderInterface = {
id: "google-antigravity",
name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
usesCallbackServer: true,
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
return loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);
},
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
const creds = credentials as AntigravityCredentials;
if (!creds.projectId) {
throw new Error("Antigravity credentials missing projectId");
}
return refreshAntigravityToken(creds.refresh, creds.projectId);
},
getApiKey(credentials: OAuthCredentials): string {
const creds = credentials as AntigravityCredentials;
return JSON.stringify({ token: creds.access, projectId: creds.projectId });
},
};

View file

@ -1,5 +1,5 @@
/**
* Shared utilities for Google OAuth providers (Gemini CLI and Antigravity).
* Shared utilities for Google OAuth providers (Gemini CLI).
*
* NOTE: This module uses Node.js http.createServer for the OAuth callback.
* It is only intended for CLI use, not browser environments.

View file

@ -5,10 +5,15 @@
* for OAuth-based providers:
* - GitHub Copilot
* - Google Cloud Code Assist (Gemini CLI)
* - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
*
* Note: Anthropic OAuth was removed per TOS compliance (see docs/user-docs/claude-code-auth-compliance.md).
* Use API keys or the local Claude Code CLI for Anthropic access.
*
* Note: Antigravity OAuth was removed because Google does not publish a
* vendor core library for it and hand-rolled OAuth against its endpoints
* is at risk of ban per Google's third-party-tool policy. Users wanting
* Gemini access should authenticate via the real `gemini` CLI
* (google-gemini-cli provider) or use the google API key.
*/
// GitHub Copilot
@ -19,8 +24,6 @@ export {
normalizeDomain,
refreshGitHubCopilotToken,
} from "./github-copilot.js";
// Google Antigravity
export { antigravityOAuthProvider, loginAntigravity, refreshAntigravityToken } from "./google-antigravity.js";
// Google Gemini CLI
export { geminiCliOAuthProvider, loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli.js";
// OpenAI Codex (ChatGPT OAuth)
@ -33,7 +36,6 @@ export * from "./types.js";
// ============================================================================
import { githubCopilotOAuthProvider } from "./github-copilot.js";
import { antigravityOAuthProvider } from "./google-antigravity.js";
import { geminiCliOAuthProvider } from "./google-gemini-cli.js";
import { openaiCodexOAuthProvider } from "./openai-codex.js";
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInterface } from "./types.js";
@ -41,7 +43,6 @@ import type { OAuthCredentials, OAuthProviderId, OAuthProviderInterface } from "
const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
githubCopilotOAuthProvider,
geminiCliOAuthProvider,
antigravityOAuthProvider,
openaiCodexOAuthProvider,
];