@xkei/openclaude 0.30.0-antigravity
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +518 -0
- package/bin/import-specifier.mjs +13 -0
- package/bin/import-specifier.test.mjs +13 -0
- package/bin/node-compile-cache.mjs +17 -0
- package/bin/openclaude +126 -0
- package/dist/cli.mjs +11292 -0
- package/dist/sdk.mjs +284293 -0
- package/docs/antigravity-plugin-install.md +223 -0
- package/docs/windows-aliases-and-launchers.md +162 -0
- package/package.json +226 -0
- package/scripts/windows/openclaude-aliases.ps1 +206 -0
- package/src/entrypoints/sdk/coreTypes.generated.ts +2385 -0
- package/src/entrypoints/sdk.d.ts +601 -0
- package/vendor/node-domexception-shim/index.js +3 -0
- package/vendor/node-domexception-shim/package.json +8 -0
- package/vendor/openclaude-antigravity-provider/.claude-plugin/marketplace.json +17 -0
- package/vendor/openclaude-antigravity-provider/.claude-plugin/plugin.json +38 -0
- package/vendor/openclaude-antigravity-provider/bin/antigravity-proxy.exe +0 -0
- package/vendor/openclaude-antigravity-provider/hooks/SessionEnd.ps1 +22 -0
- package/vendor/openclaude-antigravity-provider/hooks/SessionStart.ps1 +111 -0
- package/vendor/openclaude-antigravity-provider/hooks/Watchdog-Stop.ps1 +82 -0
- package/vendor/openclaude-antigravity-provider/hooks/hooks.json +37 -0
- package/vendor/openclaude-antigravity-provider/hooks/inject-provider.js +110 -0
- package/vendor/openclaude-antigravity-provider/hooks/session-end.bat +22 -0
- package/vendor/openclaude-antigravity-provider/hooks/start.bat +7 -0
- package/vendor/openclaude-antigravity-provider/package.json +24 -0
- package/vendor/openclaude-antigravity-provider/src/accounts.ts +115 -0
- package/vendor/openclaude-antigravity-provider/src/auth-cli.ts +125 -0
- package/vendor/openclaude-antigravity-provider/src/auth.ts +221 -0
- package/vendor/openclaude-antigravity-provider/src/config.ts +68 -0
- package/vendor/openclaude-antigravity-provider/src/constants.ts +139 -0
- package/vendor/openclaude-antigravity-provider/src/gemini-fallback.ts +157 -0
- package/vendor/openclaude-antigravity-provider/src/server.ts +367 -0
- package/vendor/openclaude-antigravity-provider/src/storage.ts +56 -0
- package/vendor/openclaude-antigravity-provider/src/transform.ts +241 -0
- package/vendor/openclaude-antigravity-provider/tsconfig.json +15 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth.ts
|
|
3
|
+
* Google OAuth 2.0 PKCE flow using Bun native crypto and fetch.
|
|
4
|
+
* No external dependencies — purely Bun built-ins.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
GOOGLE_AUTH_URL,
|
|
9
|
+
GOOGLE_TOKEN_URL,
|
|
10
|
+
GOOGLE_OAUTH_CLIENT_ID,
|
|
11
|
+
GOOGLE_OAUTH_CLIENT_SECRET,
|
|
12
|
+
GOOGLE_OAUTH_REDIRECT_PORT,
|
|
13
|
+
GOOGLE_OAUTH_SCOPES,
|
|
14
|
+
} from "./constants.ts";
|
|
15
|
+
|
|
16
|
+
const REDIRECT_URI = `http://localhost:${GOOGLE_OAUTH_REDIRECT_PORT}/oauth-callback`;
|
|
17
|
+
|
|
18
|
+
// ── PKCE helpers ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function generateCodeVerifier(): string {
|
|
21
|
+
const bytes = new Uint8Array(32);
|
|
22
|
+
crypto.getRandomValues(bytes);
|
|
23
|
+
return btoa(String.fromCharCode(...bytes))
|
|
24
|
+
.replace(/\+/g, "-")
|
|
25
|
+
.replace(/\//g, "_")
|
|
26
|
+
.replace(/=+$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
const data = encoder.encode(verifier);
|
|
32
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
33
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
34
|
+
.replace(/\+/g, "-")
|
|
35
|
+
.replace(/\//g, "_")
|
|
36
|
+
.replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Public types ──────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export interface AuthorizationUrlResult {
|
|
42
|
+
url: string;
|
|
43
|
+
verifier: string;
|
|
44
|
+
state: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OAuthTokens {
|
|
48
|
+
access_token: string;
|
|
49
|
+
refresh_token: string;
|
|
50
|
+
expires_in: number;
|
|
51
|
+
email?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AccessTokenResult {
|
|
55
|
+
access: string;
|
|
56
|
+
expires: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Build the Google authorization URL ───────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export async function buildAuthorizationUrl(): Promise<AuthorizationUrlResult> {
|
|
62
|
+
const verifier = generateCodeVerifier();
|
|
63
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
64
|
+
const stateBytes = new Uint8Array(16);
|
|
65
|
+
crypto.getRandomValues(stateBytes);
|
|
66
|
+
const state = Array.from(stateBytes)
|
|
67
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
68
|
+
.join("");
|
|
69
|
+
|
|
70
|
+
const params = new URLSearchParams({
|
|
71
|
+
client_id: GOOGLE_OAUTH_CLIENT_ID,
|
|
72
|
+
redirect_uri: REDIRECT_URI,
|
|
73
|
+
response_type: "code",
|
|
74
|
+
scope: GOOGLE_OAUTH_SCOPES,
|
|
75
|
+
code_challenge: challenge,
|
|
76
|
+
code_challenge_method: "S256",
|
|
77
|
+
state,
|
|
78
|
+
access_type: "offline",
|
|
79
|
+
prompt: "consent",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { url: `${GOOGLE_AUTH_URL}?${params.toString()}`, verifier, state };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Exchange authorization code for tokens ────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export async function exchangeCodeForTokens(
|
|
88
|
+
code: string,
|
|
89
|
+
verifier: string,
|
|
90
|
+
): Promise<OAuthTokens> {
|
|
91
|
+
const params = new URLSearchParams({
|
|
92
|
+
client_id: GOOGLE_OAUTH_CLIENT_ID,
|
|
93
|
+
client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
|
|
94
|
+
code,
|
|
95
|
+
code_verifier: verifier,
|
|
96
|
+
grant_type: "authorization_code",
|
|
97
|
+
redirect_uri: REDIRECT_URI,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body: params.toString(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const body = await res.text();
|
|
108
|
+
throw new Error(`Token exchange failed (${res.status}): ${body}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = (await res.json()) as {
|
|
112
|
+
access_token: string;
|
|
113
|
+
refresh_token: string;
|
|
114
|
+
expires_in: number;
|
|
115
|
+
id_token?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Decode email from id_token JWT payload
|
|
119
|
+
let email: string | undefined;
|
|
120
|
+
if (data.id_token) {
|
|
121
|
+
try {
|
|
122
|
+
const payloadB64 = data.id_token.split(".")[1]!;
|
|
123
|
+
const payload = JSON.parse(atob(payloadB64)) as { email?: string };
|
|
124
|
+
email = payload.email;
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore decode errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
access_token: data.access_token,
|
|
132
|
+
refresh_token: data.refresh_token,
|
|
133
|
+
expires_in: data.expires_in,
|
|
134
|
+
email,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Refresh an existing access token ──────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export async function refreshAccessToken(
|
|
141
|
+
refreshToken: string,
|
|
142
|
+
): Promise<AccessTokenResult> {
|
|
143
|
+
const params = new URLSearchParams({
|
|
144
|
+
client_id: GOOGLE_OAUTH_CLIENT_ID,
|
|
145
|
+
client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
|
|
146
|
+
grant_type: "refresh_token",
|
|
147
|
+
refresh_token: refreshToken,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
153
|
+
body: params.toString(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const body = await res.text();
|
|
158
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = (await res.json()) as {
|
|
162
|
+
access_token: string;
|
|
163
|
+
expires_in: number;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
access: data.access_token,
|
|
168
|
+
// Subtract 60s buffer so we refresh before actual expiry
|
|
169
|
+
expires: Date.now() + data.expires_in * 1000 - 60_000,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function accessTokenExpired(expires: number): boolean {
|
|
174
|
+
return Date.now() >= expires;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Local OAuth callback server (Bun.serve) ───────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export function waitForOAuthCallback(
|
|
180
|
+
expectedState: string,
|
|
181
|
+
): Promise<{ code: string; state: string }> {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
184
|
+
|
|
185
|
+
const timeout = setTimeout(() => {
|
|
186
|
+
server?.stop(true);
|
|
187
|
+
reject(new Error("OAuth callback timed out after 5 minutes."));
|
|
188
|
+
}, 5 * 60 * 1000);
|
|
189
|
+
|
|
190
|
+
server = Bun.serve({
|
|
191
|
+
port: GOOGLE_OAUTH_REDIRECT_PORT,
|
|
192
|
+
hostname: "localhost",
|
|
193
|
+
fetch(req) {
|
|
194
|
+
const url = new URL(req.url);
|
|
195
|
+
if (url.pathname !== "/oauth-callback") {
|
|
196
|
+
return new Response("Not found", { status: 404 });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const code = url.searchParams.get("code");
|
|
200
|
+
const state = url.searchParams.get("state");
|
|
201
|
+
|
|
202
|
+
if (!code || state !== expectedState) {
|
|
203
|
+
return new Response("Invalid OAuth callback.", { status: 400 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
server?.stop(true);
|
|
208
|
+
|
|
209
|
+
resolve({ code, state });
|
|
210
|
+
|
|
211
|
+
return new Response(
|
|
212
|
+
`<html><body style="font-family:sans-serif;padding:40px;text-align:center">
|
|
213
|
+
<h2>Authentication successful!</h2>
|
|
214
|
+
<p>You can close this tab and return to OpenClaude.</p>
|
|
215
|
+
</body></html>`,
|
|
216
|
+
{ headers: { "Content-Type": "text/html" } },
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts
|
|
3
|
+
* Reads ~/.openclaude.json and extracts the Gemini-CLI provider profile
|
|
4
|
+
* so the proxy can use its API key as a fallback when all Antigravity
|
|
5
|
+
* OAuth accounts are rate-limited on a Gemini model request.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { OPENCLAUDE_CONFIG_DIR } from "./constants.ts";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
export interface GeminiCliProfile {
|
|
12
|
+
/** The base URL without trailing slash, e.g.
|
|
13
|
+
* "https://generativelanguage.googleapis.com/v1beta/openai" */
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
/** Gemini API key */
|
|
16
|
+
apiKey: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ProviderProfile {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface OpenClaudeConfig {
|
|
29
|
+
providerProfiles?: ProviderProfile[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CONFIG_FILE = join(OPENCLAUDE_CONFIG_DIR, "..", ".openclaude.json");
|
|
33
|
+
|
|
34
|
+
// In-memory cache so we don't stat the file on every request.
|
|
35
|
+
let cached: GeminiCliProfile | null | undefined = undefined; // undefined = not yet loaded
|
|
36
|
+
|
|
37
|
+
export async function getGeminiCliProfile(): Promise<GeminiCliProfile | null> {
|
|
38
|
+
if (cached !== undefined) return cached;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const file = Bun.file(CONFIG_FILE);
|
|
42
|
+
if (!(await file.exists())) {
|
|
43
|
+
cached = null;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const config = (await file.json()) as OpenClaudeConfig;
|
|
47
|
+
const profile = (config.providerProfiles ?? []).find(
|
|
48
|
+
(p) => p.provider === "gemini" && p.apiKey && p.baseUrl,
|
|
49
|
+
);
|
|
50
|
+
if (!profile || !profile.apiKey || !profile.baseUrl) {
|
|
51
|
+
cached = null;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
cached = {
|
|
55
|
+
baseUrl: profile.baseUrl.replace(/\/$/, ""),
|
|
56
|
+
apiKey: profile.apiKey,
|
|
57
|
+
};
|
|
58
|
+
return cached;
|
|
59
|
+
} catch {
|
|
60
|
+
cached = null;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Call this after the user edits their provider profiles so the cache refreshes. */
|
|
66
|
+
export function invalidateGeminiCliProfileCache(): void {
|
|
67
|
+
cached = undefined;
|
|
68
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constants.ts
|
|
3
|
+
* Antigravity API endpoints, model map, and storage paths.
|
|
4
|
+
* All paths anchored to ~/.openclaude/ (OpenClaude's config dir).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const PROXY_PORT = 51122;
|
|
8
|
+
export const GOOGLE_OAUTH_REDIRECT_PORT = 51121;
|
|
9
|
+
|
|
10
|
+
// Antigravity endpoint fallback order (daily → autopush → prod)
|
|
11
|
+
export const ANTIGRAVITY_ENDPOINT_DAILY =
|
|
12
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
13
|
+
export const ANTIGRAVITY_ENDPOINT_AUTOPUSH =
|
|
14
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com";
|
|
15
|
+
export const ANTIGRAVITY_ENDPOINT_PROD =
|
|
16
|
+
"https://cloudcode-pa.googleapis.com";
|
|
17
|
+
|
|
18
|
+
// Primary endpoint used for requests — daily sandbox mirrors the OpenCode plugin
|
|
19
|
+
// and has no per-account Free Tier quota limits (unlike the prod endpoint).
|
|
20
|
+
export const ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_ENDPOINT_DAILY;
|
|
21
|
+
|
|
22
|
+
// Ordered fallback chain: try daily first, then autopush, then prod.
|
|
23
|
+
export const ANTIGRAVITY_ENDPOINT_FALLBACKS = [
|
|
24
|
+
ANTIGRAVITY_ENDPOINT_DAILY,
|
|
25
|
+
ANTIGRAVITY_ENDPOINT_AUTOPUSH,
|
|
26
|
+
ANTIGRAVITY_ENDPOINT_PROD,
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
// NOTE: The opencode-antigravity-auth plugin defaults to project
|
|
30
|
+
// "rising-fact-p41fc", but that shared project is only usable by accounts
|
|
31
|
+
// provisioned through its loadCodeAssist discovery flow. Our accounts have no
|
|
32
|
+
// project IDs, and forcing this one is rejected with USER_PROJECT_DENIED.
|
|
33
|
+
// The correct behavior is to OMIT x-goog-user-project when the account has
|
|
34
|
+
// no project of its own — kept here for documentation only.
|
|
35
|
+
export const ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc";
|
|
36
|
+
|
|
37
|
+
// Real Antigravity OAuth app credentials (from opencode-antigravity-auth)
|
|
38
|
+
export const GOOGLE_OAUTH_CLIENT_ID =
|
|
39
|
+
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
40
|
+
|
|
41
|
+
export const GOOGLE_OAUTH_CLIENT_SECRET = "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";
|
|
42
|
+
|
|
43
|
+
export const GOOGLE_AUTH_URL =
|
|
44
|
+
"https://accounts.google.com/o/oauth2/v2/auth";
|
|
45
|
+
|
|
46
|
+
export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
47
|
+
|
|
48
|
+
// OpenClaude config directory (Windows: C:\Users\<user>\.openclaude)
|
|
49
|
+
export const OPENCLAUDE_CONFIG_DIR =
|
|
50
|
+
`${process.env.USERPROFILE ?? process.env.HOME ?? "~"}/.openclaude`;
|
|
51
|
+
|
|
52
|
+
export const ACCOUNTS_FILE = `${OPENCLAUDE_CONFIG_DIR}/antigravity-accounts.json`;
|
|
53
|
+
|
|
54
|
+
export const PROXY_PID_FILE = `${OPENCLAUDE_CONFIG_DIR}/antigravity-proxy.pid`;
|
|
55
|
+
|
|
56
|
+
// Exact scopes required by the Antigravity OAuth app
|
|
57
|
+
export const GOOGLE_OAUTH_SCOPES = [
|
|
58
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
59
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
60
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
61
|
+
"https://www.googleapis.com/auth/cclog",
|
|
62
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
63
|
+
].join(" ");
|
|
64
|
+
|
|
65
|
+
// Maps OpenAI-style model names to Gemini API model names
|
|
66
|
+
// (used by the generativelanguage Gemini-CLI fallback path)
|
|
67
|
+
export const MODEL_MAP: Record<string, string> = {
|
|
68
|
+
"antigravity-claude-sonnet-4-6": "claude-sonnet-4-6",
|
|
69
|
+
"antigravity-claude-opus-4-6-thinking": "claude-opus-4-6",
|
|
70
|
+
"antigravity-gemini-3-pro": "gemini-3-pro-preview",
|
|
71
|
+
"antigravity-gemini-3.1-pro": "gemini-3.1-pro-preview",
|
|
72
|
+
"antigravity-gemini-3-flash": "gemini-3-flash-preview",
|
|
73
|
+
"gemini-2.5-flash": "gemini-2.5-flash",
|
|
74
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
75
|
+
"gemini-3-flash-preview": "gemini-3-flash-preview",
|
|
76
|
+
"gemini-3-pro-preview": "gemini-3-pro-preview",
|
|
77
|
+
"gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Maps exposed model IDs to the names served by the Antigravity daily sandbox.
|
|
81
|
+
// Empirically verified (200 OK) against daily-cloudcode-pa.sandbox.googleapis.com:
|
|
82
|
+
// - Pro models REQUIRE a thinking-tier suffix (-low / -high)
|
|
83
|
+
// - Flash models use the BARE name (tier suffix -> 404)
|
|
84
|
+
// - Claude Sonnet uses the bare name; Claude Opus REQUIRES -thinking
|
|
85
|
+
export const ANTIGRAVITY_MODEL_MAP: Record<string, string> = {
|
|
86
|
+
"antigravity-claude-sonnet-4-6": "claude-sonnet-4-6",
|
|
87
|
+
"antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
|
88
|
+
"antigravity-gemini-3-pro": "gemini-3-pro-low",
|
|
89
|
+
"antigravity-gemini-3.1-pro": "gemini-3.1-pro-low",
|
|
90
|
+
"antigravity-gemini-3-flash": "gemini-3-flash",
|
|
91
|
+
"gemini-2.5-flash": "gemini-2.5-flash",
|
|
92
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
93
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
94
|
+
"gemini-3-pro-preview": "gemini-3-pro-low",
|
|
95
|
+
"gemini-3.1-pro-preview": "gemini-3.1-pro-low",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const AVAILABLE_MODELS = Object.keys(MODEL_MAP);
|
|
99
|
+
|
|
100
|
+
export function isClaudeModel(model: string): boolean {
|
|
101
|
+
return model.includes("claude");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveGeminiModel(model: string): string {
|
|
105
|
+
return MODEL_MAP[model] ?? model;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveAntigravityModel(model: string): string {
|
|
109
|
+
return ANTIGRAVITY_MODEL_MAP[model] ?? model;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Antigravity-style User-Agent — the Antigravity Manager sends ONLY this
|
|
113
|
+
// header (no X-Goog-Api-Client / Client-Metadata) on content requests.
|
|
114
|
+
export function getAntigravityUserAgent(): string {
|
|
115
|
+
const platform = process.platform === "win32" ? "windows/amd64" : "darwin/arm64";
|
|
116
|
+
return `antigravity/1.18.3 ${platform}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const SYNTHETIC_PROJECT_ADJECTIVES = ["useful", "bright", "swift", "calm", "bold"];
|
|
120
|
+
const SYNTHETIC_PROJECT_NOUNS = ["fuze", "wave", "spark", "flow", "core"];
|
|
121
|
+
|
|
122
|
+
// Synthetic project id (same scheme as opencode-antigravity-auth). The daily
|
|
123
|
+
// sandbox accepts arbitrary ids here — the project routes the request into
|
|
124
|
+
// the Antigravity agent quota pool instead of the per-account free tier.
|
|
125
|
+
export function generateSyntheticProjectId(): string {
|
|
126
|
+
const adj = SYNTHETIC_PROJECT_ADJECTIVES[Math.floor(Math.random() * SYNTHETIC_PROJECT_ADJECTIVES.length)]!;
|
|
127
|
+
const noun = SYNTHETIC_PROJECT_NOUNS[Math.floor(Math.random() * SYNTHETIC_PROJECT_NOUNS.length)]!;
|
|
128
|
+
const randomPart = crypto.randomUUID().slice(0, 5).toLowerCase();
|
|
129
|
+
return `${adj}-${noun}-${randomPart}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getAntigravityHeaders(): Record<string, string> {
|
|
133
|
+
const platform = process.platform === "win32" ? "WINDOWS" : "MACOS";
|
|
134
|
+
return {
|
|
135
|
+
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/1.18.3 Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,
|
|
136
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
137
|
+
"Client-Metadata": `{"ideType":"ANTIGRAVITY","platform":"${platform}","pluginType":"GEMINI"}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gemini-fallback.ts
|
|
3
|
+
*
|
|
4
|
+
* When ALL Antigravity OAuth accounts are rate-limited on a Gemini model
|
|
5
|
+
* request, this module forwards the request directly to the official
|
|
6
|
+
* Google Gemini OpenAI-compatible endpoint using the Gemini-CLI API key
|
|
7
|
+
* stored in ~/.openclaude.json.
|
|
8
|
+
*
|
|
9
|
+
* Claude model requests are NOT routed here — they always return the 429
|
|
10
|
+
* back to OpenClaude so the normal per-provider retry logic applies.
|
|
11
|
+
*
|
|
12
|
+
* The Gemini endpoint at generativelanguage.googleapis.com/v1beta/openai
|
|
13
|
+
* is fully OpenAI-compatible, so no payload translation is needed.
|
|
14
|
+
* We only swap the model name to the native Gemini model ID.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { OpenAIChatRequest } from "./transform.ts";
|
|
18
|
+
import type { GeminiCliProfile } from "./config.ts";
|
|
19
|
+
import { MODEL_MAP } from "./constants.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Resolve the native Gemini model name for the fallback request.
|
|
23
|
+
// MODEL_MAP already has the mapping (e.g. "antigravity-gemini-3.1-pro" ->
|
|
24
|
+
// "gemini-3.1-pro-preview"). Fall back to the model name as-is if it
|
|
25
|
+
// doesn't appear in the map (e.g. the user already requested a raw model id).
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function resolveNativeGeminiModel(model: string): string {
|
|
28
|
+
return MODEL_MAP[model] ?? model;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Build the fallback OpenAI-compatible payload.
|
|
33
|
+
// We clone the original body and replace the model with the native name.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
function buildFallbackPayload(body: OpenAIChatRequest): OpenAIChatRequest {
|
|
36
|
+
return {
|
|
37
|
+
...body,
|
|
38
|
+
model: resolveNativeGeminiModel(body.model),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Forward a streaming request to the Gemini-CLI endpoint and pipe the
|
|
44
|
+
// SSE stream back. Returns a Response whose body is the upstream SSE stream.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
async function streamFallback(
|
|
47
|
+
profile: GeminiCliProfile,
|
|
48
|
+
payload: OpenAIChatRequest,
|
|
49
|
+
): Promise<Response> {
|
|
50
|
+
const upstream = await fetch(`${profile.baseUrl}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(payload),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!upstream.ok || !upstream.body) {
|
|
60
|
+
const errText = await upstream.text().catch(() => "(unreadable)");
|
|
61
|
+
return new Response(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
error: {
|
|
64
|
+
message: `Gemini-CLI fallback upstream error (${upstream.status}): ${errText}`,
|
|
65
|
+
type: "upstream_error",
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
{
|
|
69
|
+
status: upstream.status,
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pipe the upstream SSE stream directly — the Gemini OpenAI-compatible
|
|
76
|
+
// endpoint emits standard "data: {...}\n\n" chunks identical to what
|
|
77
|
+
// OpenClaude expects, so no translation is required.
|
|
78
|
+
return new Response(upstream.body, {
|
|
79
|
+
status: 200,
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "text/event-stream",
|
|
82
|
+
"Cache-Control": "no-cache",
|
|
83
|
+
Connection: "keep-alive",
|
|
84
|
+
"Access-Control-Allow-Origin": "*",
|
|
85
|
+
"X-Antigravity-Fallback": "gemini-cli",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Forward a non-streaming request to the Gemini-CLI endpoint.
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
async function jsonFallback(
|
|
94
|
+
profile: GeminiCliProfile,
|
|
95
|
+
payload: OpenAIChatRequest,
|
|
96
|
+
): Promise<Response> {
|
|
97
|
+
const upstream = await fetch(`${profile.baseUrl}/chat/completions`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(payload),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const body = await upstream.text();
|
|
107
|
+
|
|
108
|
+
if (!upstream.ok) {
|
|
109
|
+
return new Response(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
error: {
|
|
112
|
+
message: `Gemini-CLI fallback upstream error (${upstream.status}): ${body}`,
|
|
113
|
+
type: "upstream_error",
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
{
|
|
117
|
+
status: upstream.status,
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"Access-Control-Allow-Origin": "*",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pass the JSON response through directly with the fallback header so
|
|
127
|
+
// logs / health checks can distinguish the path taken.
|
|
128
|
+
return new Response(body, {
|
|
129
|
+
status: 200,
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"Access-Control-Allow-Origin": "*",
|
|
133
|
+
"X-Antigravity-Fallback": "gemini-cli",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Public entry point called from server.ts when all Antigravity accounts
|
|
140
|
+
// are exhausted on a Gemini model request.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
export async function handleGeminiCliFallback(
|
|
143
|
+
body: OpenAIChatRequest,
|
|
144
|
+
profile: GeminiCliProfile,
|
|
145
|
+
): Promise<Response> {
|
|
146
|
+
const payload = buildFallbackPayload(body);
|
|
147
|
+
const isStream = body.stream !== false;
|
|
148
|
+
|
|
149
|
+
console.log(
|
|
150
|
+
`[antigravity-provider] Gemini-CLI fallback: ${body.model} -> ${payload.model} (stream=${isStream})`,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (isStream) {
|
|
154
|
+
return streamFallback(profile, payload);
|
|
155
|
+
}
|
|
156
|
+
return jsonFallback(profile, payload);
|
|
157
|
+
}
|