opencode-antigravity-auth 1.2.0 → 1.2.2
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/README.md +212 -98
- package/dist/src/constants.d.ts +6 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +5 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/hooks/auto-update-checker/cache.d.ts +3 -0
- package/dist/src/hooks/auto-update-checker/cache.d.ts.map +1 -0
- package/dist/src/hooks/auto-update-checker/cache.js +71 -0
- package/dist/src/hooks/auto-update-checker/cache.js.map +1 -0
- package/dist/src/hooks/auto-update-checker/checker.d.ts +16 -0
- package/dist/src/hooks/auto-update-checker/checker.d.ts.map +1 -0
- package/dist/src/hooks/auto-update-checker/checker.js +237 -0
- package/dist/src/hooks/auto-update-checker/checker.js.map +1 -0
- package/dist/src/hooks/auto-update-checker/constants.d.ts +9 -0
- package/dist/src/hooks/auto-update-checker/constants.d.ts.map +1 -0
- package/dist/src/hooks/auto-update-checker/constants.js +23 -0
- package/dist/src/hooks/auto-update-checker/constants.js.map +1 -0
- package/dist/src/hooks/auto-update-checker/index.d.ts +34 -0
- package/dist/src/hooks/auto-update-checker/index.d.ts.map +1 -0
- package/dist/src/hooks/auto-update-checker/index.js +121 -0
- package/dist/src/hooks/auto-update-checker/index.js.map +1 -0
- package/dist/src/hooks/auto-update-checker/types.d.ts +25 -0
- package/dist/src/hooks/auto-update-checker/types.d.ts.map +1 -0
- package/dist/src/hooks/auto-update-checker/types.js +1 -0
- package/dist/src/hooks/auto-update-checker/types.js.map +1 -0
- package/dist/src/plugin/accounts.d.ts +25 -11
- package/dist/src/plugin/accounts.d.ts.map +1 -1
- package/dist/src/plugin/accounts.js +161 -55
- package/dist/src/plugin/accounts.js.map +1 -1
- package/dist/src/plugin/debug.d.ts +32 -0
- package/dist/src/plugin/debug.d.ts.map +1 -1
- package/dist/src/plugin/debug.js +140 -12
- package/dist/src/plugin/debug.js.map +1 -1
- package/dist/src/plugin/request.d.ts +6 -2
- package/dist/src/plugin/request.d.ts.map +1 -1
- package/dist/src/plugin/request.js +361 -21
- package/dist/src/plugin/request.js.map +1 -1
- package/dist/src/plugin/storage.d.ts +52 -9
- package/dist/src/plugin/storage.d.ts.map +1 -1
- package/dist/src/plugin/storage.js +91 -10
- package/dist/src/plugin/storage.js.map +1 -1
- package/dist/src/plugin/types.d.ts +8 -0
- package/dist/src/plugin/types.d.ts.map +1 -1
- package/dist/src/plugin.d.ts +3 -3
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +865 -486
- package/dist/src/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/src/plugin.js
CHANGED
|
@@ -4,13 +4,31 @@ import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
|
|
|
4
4
|
import { accessTokenExpired, isOAuthAuth, parseRefreshParts } from "./plugin/auth";
|
|
5
5
|
import { promptAddAnotherAccount, promptLoginMode, promptProjectId } from "./plugin/cli";
|
|
6
6
|
import { ensureProjectContext } from "./plugin/project";
|
|
7
|
-
import { startAntigravityDebugRequest } from "./plugin/debug";
|
|
8
|
-
import { isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
|
|
7
|
+
import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, } from "./plugin/debug";
|
|
8
|
+
import { buildThinkingWarmupBody, isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
|
|
9
9
|
import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
|
|
10
10
|
import { startOAuthListener } from "./plugin/server";
|
|
11
11
|
import { clearAccounts, loadAccounts, saveAccounts } from "./plugin/storage";
|
|
12
12
|
import { AccountManager } from "./plugin/accounts";
|
|
13
|
+
import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
|
|
13
14
|
const MAX_OAUTH_ACCOUNTS = 10;
|
|
15
|
+
const MAX_WARMUP_SESSIONS = 1000;
|
|
16
|
+
const warmupAttemptedSessionIds = new Set();
|
|
17
|
+
function trackWarmupSession(sessionId) {
|
|
18
|
+
if (warmupAttemptedSessionIds.has(sessionId)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (warmupAttemptedSessionIds.size >= MAX_WARMUP_SESSIONS) {
|
|
22
|
+
const first = warmupAttemptedSessionIds.values().next().value;
|
|
23
|
+
if (first)
|
|
24
|
+
warmupAttemptedSessionIds.delete(first);
|
|
25
|
+
}
|
|
26
|
+
warmupAttemptedSessionIds.add(sessionId);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
function untrackWarmupSession(sessionId) {
|
|
30
|
+
warmupAttemptedSessionIds.delete(sessionId);
|
|
31
|
+
}
|
|
14
32
|
async function openBrowser(url) {
|
|
15
33
|
try {
|
|
16
34
|
if (process.platform === "darwin") {
|
|
@@ -115,8 +133,6 @@ async function persistAccountPool(results, replaceAll = false) {
|
|
|
115
133
|
managedProjectId: parts.managedProjectId,
|
|
116
134
|
addedAt: now,
|
|
117
135
|
lastUsed: now,
|
|
118
|
-
isRateLimited: false,
|
|
119
|
-
rateLimitResetTime: 0,
|
|
120
136
|
});
|
|
121
137
|
continue;
|
|
122
138
|
}
|
|
@@ -140,9 +156,13 @@ async function persistAccountPool(results, replaceAll = false) {
|
|
|
140
156
|
? 0
|
|
141
157
|
: (typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex) ? stored.activeIndex : 0);
|
|
142
158
|
await saveAccounts({
|
|
143
|
-
version:
|
|
159
|
+
version: 3,
|
|
144
160
|
accounts,
|
|
145
161
|
activeIndex: clampInt(activeIndex, 0, accounts.length - 1),
|
|
162
|
+
activeIndexByFamily: {
|
|
163
|
+
claude: clampInt(activeIndex, 0, accounts.length - 1),
|
|
164
|
+
gemini: clampInt(activeIndex, 0, accounts.length - 1),
|
|
165
|
+
},
|
|
146
166
|
});
|
|
147
167
|
}
|
|
148
168
|
function retryAfterMsFromResponse(response) {
|
|
@@ -162,6 +182,150 @@ function retryAfterMsFromResponse(response) {
|
|
|
162
182
|
}
|
|
163
183
|
return 60_000;
|
|
164
184
|
}
|
|
185
|
+
function parseDurationToMs(duration) {
|
|
186
|
+
const match = duration.match(/^(\d+(?:\.\d+)?)(s|m|h)?$/i);
|
|
187
|
+
if (!match)
|
|
188
|
+
return null;
|
|
189
|
+
const value = parseFloat(match[1]);
|
|
190
|
+
const unit = (match[2] || "s").toLowerCase();
|
|
191
|
+
switch (unit) {
|
|
192
|
+
case "h": return value * 3600 * 1000;
|
|
193
|
+
case "m": return value * 60 * 1000;
|
|
194
|
+
case "s": return value * 1000;
|
|
195
|
+
default: return value * 1000;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function extractRateLimitBodyInfo(body) {
|
|
199
|
+
if (!body || typeof body !== "object") {
|
|
200
|
+
return { retryDelayMs: null };
|
|
201
|
+
}
|
|
202
|
+
const error = body.error;
|
|
203
|
+
const message = error && typeof error === "object"
|
|
204
|
+
? error.message
|
|
205
|
+
: undefined;
|
|
206
|
+
const details = error && typeof error === "object"
|
|
207
|
+
? error.details
|
|
208
|
+
: undefined;
|
|
209
|
+
let reason;
|
|
210
|
+
if (Array.isArray(details)) {
|
|
211
|
+
for (const detail of details) {
|
|
212
|
+
if (!detail || typeof detail !== "object")
|
|
213
|
+
continue;
|
|
214
|
+
const type = detail["@type"];
|
|
215
|
+
if (typeof type === "string" && type.includes("google.rpc.ErrorInfo")) {
|
|
216
|
+
const detailReason = detail.reason;
|
|
217
|
+
if (typeof detailReason === "string") {
|
|
218
|
+
reason = detailReason;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const detail of details) {
|
|
224
|
+
if (!detail || typeof detail !== "object")
|
|
225
|
+
continue;
|
|
226
|
+
const type = detail["@type"];
|
|
227
|
+
if (typeof type === "string" && type.includes("google.rpc.RetryInfo")) {
|
|
228
|
+
const retryDelay = detail.retryDelay;
|
|
229
|
+
if (typeof retryDelay === "string") {
|
|
230
|
+
const retryDelayMs = parseDurationToMs(retryDelay);
|
|
231
|
+
if (retryDelayMs !== null) {
|
|
232
|
+
return { retryDelayMs, message, reason };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const detail of details) {
|
|
238
|
+
if (!detail || typeof detail !== "object")
|
|
239
|
+
continue;
|
|
240
|
+
const metadata = detail.metadata;
|
|
241
|
+
if (metadata && typeof metadata === "object") {
|
|
242
|
+
const quotaResetDelay = metadata.quotaResetDelay;
|
|
243
|
+
const quotaResetTime = metadata.quotaResetTimeStamp;
|
|
244
|
+
if (typeof quotaResetDelay === "string") {
|
|
245
|
+
const quotaResetDelayMs = parseDurationToMs(quotaResetDelay);
|
|
246
|
+
if (quotaResetDelayMs !== null) {
|
|
247
|
+
return { retryDelayMs: quotaResetDelayMs, message, quotaResetTime, reason };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (message) {
|
|
254
|
+
const afterMatch = message.match(/reset after\s+([0-9hms.]+)/i);
|
|
255
|
+
const rawDuration = afterMatch?.[1];
|
|
256
|
+
if (rawDuration) {
|
|
257
|
+
const parsed = parseDurationToMs(rawDuration);
|
|
258
|
+
if (parsed !== null) {
|
|
259
|
+
return { retryDelayMs: parsed, message, reason };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { retryDelayMs: null, message, reason };
|
|
264
|
+
}
|
|
265
|
+
async function extractRetryInfoFromBody(response) {
|
|
266
|
+
try {
|
|
267
|
+
const text = await response.clone().text();
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(text);
|
|
270
|
+
return extractRateLimitBodyInfo(parsed);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return { retryDelayMs: null };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return { retryDelayMs: null };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function formatWaitTime(ms) {
|
|
281
|
+
if (ms < 1000)
|
|
282
|
+
return `${ms}ms`;
|
|
283
|
+
const seconds = Math.ceil(ms / 1000);
|
|
284
|
+
if (seconds < 60)
|
|
285
|
+
return `${seconds}s`;
|
|
286
|
+
const minutes = Math.floor(seconds / 60);
|
|
287
|
+
const remainingSeconds = seconds % 60;
|
|
288
|
+
if (minutes < 60) {
|
|
289
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
290
|
+
}
|
|
291
|
+
const hours = Math.floor(minutes / 60);
|
|
292
|
+
const remainingMinutes = minutes % 60;
|
|
293
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
294
|
+
}
|
|
295
|
+
const SHORT_RETRY_THRESHOLD_MS = 5000;
|
|
296
|
+
const rateLimitStateByAccount = new Map();
|
|
297
|
+
function getRateLimitBackoff(accountIndex, serverRetryAfterMs) {
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const previous = rateLimitStateByAccount.get(accountIndex);
|
|
300
|
+
const attempt = previous && (now - previous.lastAt < 120_000) ? previous.consecutive429 + 1 : 1;
|
|
301
|
+
rateLimitStateByAccount.set(accountIndex, { consecutive429: attempt, lastAt: now });
|
|
302
|
+
const baseDelay = serverRetryAfterMs ?? 1000;
|
|
303
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), 60_000);
|
|
304
|
+
return { attempt, delayMs: Math.max(baseDelay, backoffDelay) };
|
|
305
|
+
}
|
|
306
|
+
function resetRateLimitState(accountIndex) {
|
|
307
|
+
rateLimitStateByAccount.delete(accountIndex);
|
|
308
|
+
}
|
|
309
|
+
// Track consecutive non-429 failures per account to prevent infinite loops
|
|
310
|
+
const accountFailureState = new Map();
|
|
311
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
312
|
+
const FAILURE_COOLDOWN_MS = 30_000; // 30 seconds cooldown after max failures
|
|
313
|
+
const FAILURE_STATE_RESET_MS = 120_000; // Reset failure count after 2 minutes of no failures
|
|
314
|
+
function trackAccountFailure(accountIndex) {
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const previous = accountFailureState.get(accountIndex);
|
|
317
|
+
// Reset if last failure was more than 2 minutes ago
|
|
318
|
+
const failures = previous && (now - previous.lastFailureAt < FAILURE_STATE_RESET_MS)
|
|
319
|
+
? previous.consecutiveFailures + 1
|
|
320
|
+
: 1;
|
|
321
|
+
accountFailureState.set(accountIndex, { consecutiveFailures: failures, lastFailureAt: now });
|
|
322
|
+
const shouldCooldown = failures >= MAX_CONSECUTIVE_FAILURES;
|
|
323
|
+
const cooldownMs = shouldCooldown ? FAILURE_COOLDOWN_MS : 0;
|
|
324
|
+
return { failures, shouldCooldown, cooldownMs };
|
|
325
|
+
}
|
|
326
|
+
function resetAccountFailureState(accountIndex) {
|
|
327
|
+
accountFailureState.delete(accountIndex);
|
|
328
|
+
}
|
|
165
329
|
/**
|
|
166
330
|
* Sleep for a given number of milliseconds, respecting an abort signal.
|
|
167
331
|
*/
|
|
@@ -189,371 +353,699 @@ function sleep(ms, signal) {
|
|
|
189
353
|
/**
|
|
190
354
|
* Creates an Antigravity OAuth plugin for a specific provider ID.
|
|
191
355
|
*/
|
|
192
|
-
export const createAntigravityPlugin = (providerId) => async ({ client }) =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return {};
|
|
206
|
-
}
|
|
207
|
-
// Validate that stored accounts are in sync with OpenCode's auth
|
|
208
|
-
// If OpenCode's refresh token doesn't match any stored account, clear stale storage
|
|
209
|
-
const authParts = parseRefreshParts(auth.refresh);
|
|
210
|
-
const storedAccounts = await loadAccounts();
|
|
211
|
-
if (storedAccounts && storedAccounts.accounts.length > 0 && authParts.refreshToken) {
|
|
212
|
-
const hasMatchingAccount = storedAccounts.accounts.some((acc) => acc.refreshToken === authParts.refreshToken);
|
|
213
|
-
if (!hasMatchingAccount) {
|
|
214
|
-
// OpenCode's auth doesn't match any stored account - storage is stale
|
|
215
|
-
// Clear it and let the user re-authenticate
|
|
216
|
-
console.warn("[opencode-antigravity-auth] Stored accounts don't match OpenCode's auth. Clearing stale storage.");
|
|
356
|
+
export const createAntigravityPlugin = (providerId) => async ({ client, directory }) => {
|
|
357
|
+
const updateChecker = createAutoUpdateCheckerHook(client, directory, {
|
|
358
|
+
showStartupToast: true,
|
|
359
|
+
autoUpdate: true,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
event: updateChecker.event,
|
|
363
|
+
auth: {
|
|
364
|
+
provider: providerId,
|
|
365
|
+
loader: async (getAuth, provider) => {
|
|
366
|
+
const auth = await getAuth();
|
|
367
|
+
// If OpenCode has no valid OAuth auth, clear any stale account storage
|
|
368
|
+
if (!isOAuthAuth(auth)) {
|
|
217
369
|
try {
|
|
218
370
|
await clearAccounts();
|
|
219
371
|
}
|
|
220
372
|
catch {
|
|
221
373
|
// ignore
|
|
222
374
|
}
|
|
375
|
+
return {};
|
|
223
376
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
377
|
+
// Validate that stored accounts are in sync with OpenCode's auth
|
|
378
|
+
// If OpenCode's refresh token doesn't match any stored account, clear stale storage
|
|
379
|
+
const authParts = parseRefreshParts(auth.refresh);
|
|
380
|
+
const storedAccounts = await loadAccounts();
|
|
381
|
+
if (storedAccounts && storedAccounts.accounts.length > 0 && authParts.refreshToken) {
|
|
382
|
+
const hasMatchingAccount = storedAccounts.accounts.some((acc) => acc.refreshToken === authParts.refreshToken);
|
|
383
|
+
if (!hasMatchingAccount) {
|
|
384
|
+
// OpenCode's auth doesn't match any stored account - storage is stale
|
|
385
|
+
// Clear it and let the user re-authenticate
|
|
386
|
+
console.warn("[opencode-antigravity-auth] Stored accounts don't match OpenCode's auth. Clearing stale storage.");
|
|
387
|
+
try {
|
|
388
|
+
await clearAccounts();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
238
393
|
}
|
|
239
394
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// If the request is for the *other* provider, we might still want to intercept if URL matches
|
|
245
|
-
// But strict compliance means we only handle requests if the auth provider matches.
|
|
246
|
-
// Since loader is instantiated per provider, we are good.
|
|
247
|
-
if (!isGenerativeLanguageRequest(input)) {
|
|
248
|
-
return fetch(input, init);
|
|
249
|
-
}
|
|
250
|
-
const latestAuth = await getAuth();
|
|
251
|
-
if (!isOAuthAuth(latestAuth)) {
|
|
252
|
-
return fetch(input, init);
|
|
395
|
+
const accountManager = await AccountManager.loadFromDisk(auth);
|
|
396
|
+
if (accountManager.getAccountCount() > 0) {
|
|
397
|
+
try {
|
|
398
|
+
await accountManager.saveToDisk();
|
|
253
399
|
}
|
|
254
|
-
|
|
255
|
-
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.error("[opencode-antigravity-auth] Failed to persist initial account pool:", error);
|
|
256
402
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
// This is scoped to the fetch call so it resets per-request
|
|
262
|
-
let previousAccountIndex = null;
|
|
263
|
-
// Helper to check if request was aborted
|
|
264
|
-
const checkAborted = () => {
|
|
265
|
-
if (abortSignal?.aborted) {
|
|
266
|
-
throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("Aborted");
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
// Helper to show toast without blocking on abort
|
|
270
|
-
const showToast = async (message, variant) => {
|
|
271
|
-
if (abortSignal?.aborted)
|
|
272
|
-
return;
|
|
403
|
+
}
|
|
404
|
+
if (isDebugEnabled()) {
|
|
405
|
+
const logPath = getLogFilePath();
|
|
406
|
+
if (logPath) {
|
|
273
407
|
try {
|
|
274
408
|
await client.tui.showToast({
|
|
275
|
-
body: { message
|
|
409
|
+
body: { message: `Debug log: ${logPath}`, variant: "info" },
|
|
276
410
|
});
|
|
277
411
|
}
|
|
278
412
|
catch {
|
|
279
413
|
// TUI may not be available
|
|
280
414
|
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const accountCount = accountManager.getAccountCount();
|
|
288
|
-
if (accountCount === 0) {
|
|
289
|
-
throw new Error("No Antigravity accounts available. Run `opencode auth login`.");
|
|
290
|
-
}
|
|
291
|
-
const account = accountManager.pickNext();
|
|
292
|
-
if (!account) {
|
|
293
|
-
// All accounts are rate-limited - wait and retry
|
|
294
|
-
const waitMs = accountManager.getMinWaitTimeMs() || 60_000;
|
|
295
|
-
const waitSec = Math.max(1, Math.ceil(waitMs / 1000));
|
|
296
|
-
await showToast(`All ${accountCount} account(s) rate-limited. Waiting ${waitSec}s...`, "warning");
|
|
297
|
-
// Wait for the cooldown to expire
|
|
298
|
-
await sleep(waitMs, abortSignal);
|
|
299
|
-
continue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (provider.models) {
|
|
418
|
+
for (const model of Object.values(provider.models)) {
|
|
419
|
+
if (model) {
|
|
420
|
+
model.cost = { input: 0, output: 0 };
|
|
300
421
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
apiKey: "",
|
|
426
|
+
async fetch(input, init) {
|
|
427
|
+
// If the request is for the *other* provider, we might still want to intercept if URL matches
|
|
428
|
+
// But strict compliance means we only handle requests if the auth provider matches.
|
|
429
|
+
// Since loader is instantiated per provider, we are good.
|
|
430
|
+
if (!isGenerativeLanguageRequest(input)) {
|
|
431
|
+
return fetch(input, init);
|
|
306
432
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
433
|
+
const latestAuth = await getAuth();
|
|
434
|
+
if (!isOAuthAuth(latestAuth)) {
|
|
435
|
+
return fetch(input, init);
|
|
310
436
|
}
|
|
311
|
-
|
|
312
|
-
|
|
437
|
+
if (accountManager.getAccountCount() === 0) {
|
|
438
|
+
throw new Error("No Antigravity accounts configured. Run `opencode auth login`.");
|
|
313
439
|
}
|
|
314
|
-
|
|
315
|
-
|
|
440
|
+
const urlString = toUrlString(input);
|
|
441
|
+
const family = getModelFamilyFromUrl(urlString);
|
|
442
|
+
const debugLines = [];
|
|
443
|
+
const pushDebug = (line) => {
|
|
444
|
+
if (!isDebugEnabled())
|
|
445
|
+
return;
|
|
446
|
+
debugLines.push(line);
|
|
447
|
+
};
|
|
448
|
+
pushDebug(`request=${urlString}`);
|
|
449
|
+
let lastFailure = null;
|
|
450
|
+
let lastError = null;
|
|
451
|
+
const abortSignal = init?.signal ?? undefined;
|
|
452
|
+
// Helper to check if request was aborted
|
|
453
|
+
const checkAborted = () => {
|
|
454
|
+
if (abortSignal?.aborted) {
|
|
455
|
+
throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("Aborted");
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
// Helper to show toast without blocking on abort
|
|
459
|
+
const showToast = async (message, variant) => {
|
|
460
|
+
if (abortSignal?.aborted)
|
|
461
|
+
return;
|
|
316
462
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
463
|
+
await client.tui.showToast({
|
|
464
|
+
body: { message, variant },
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// TUI may not be available
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
// Use while(true) loop to handle rate limits with backoff
|
|
472
|
+
// This ensures we wait and retry when all accounts are rate-limited
|
|
473
|
+
const quietMode = process.env.OPENCODE_ANTIGRAVITY_QUIET === "1";
|
|
474
|
+
while (true) {
|
|
475
|
+
// Check for abort at the start of each iteration
|
|
476
|
+
checkAborted();
|
|
477
|
+
const accountCount = accountManager.getAccountCount();
|
|
478
|
+
if (accountCount === 0) {
|
|
479
|
+
throw new Error("No Antigravity accounts available. Run `opencode auth login`.");
|
|
480
|
+
}
|
|
481
|
+
const account = accountManager.getCurrentOrNextForFamily(family);
|
|
482
|
+
if (!account) {
|
|
483
|
+
// All accounts are rate-limited - wait and retry
|
|
484
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(family) || 60_000;
|
|
485
|
+
const waitSec = Math.max(1, Math.ceil(waitMs / 1000));
|
|
486
|
+
pushDebug(`all-rate-limited family=${family} accounts=${accountCount}`);
|
|
487
|
+
if (isDebugEnabled()) {
|
|
488
|
+
logAccountContext("All accounts rate-limited", {
|
|
489
|
+
index: -1,
|
|
490
|
+
family,
|
|
491
|
+
totalAccounts: accountCount,
|
|
492
|
+
});
|
|
493
|
+
logRateLimitSnapshot(family, accountManager.getAccountsSnapshot());
|
|
329
494
|
}
|
|
495
|
+
await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Waiting ${waitSec}s...`, "warning");
|
|
496
|
+
// Wait for the cooldown to expire
|
|
497
|
+
await sleep(waitMs, abortSignal);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
pushDebug(`selected idx=${account.index} email=${account.email ?? ""} family=${family} accounts=${accountCount}`);
|
|
501
|
+
if (isDebugEnabled()) {
|
|
502
|
+
logAccountContext("Selected", {
|
|
503
|
+
index: account.index,
|
|
504
|
+
email: account.email,
|
|
505
|
+
family,
|
|
506
|
+
totalAccounts: accountCount,
|
|
507
|
+
rateLimitState: account.rateLimitResetTimes,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// Show toast when switching to a different account (debounced, respects quiet mode)
|
|
511
|
+
if (!quietMode && accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
|
|
512
|
+
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
513
|
+
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
514
|
+
accountManager.markToastShown(account.index);
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
await accountManager.saveToDisk();
|
|
330
518
|
}
|
|
331
519
|
catch (error) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
520
|
+
console.error("[opencode-antigravity-auth] Failed to persist rotation state:", error);
|
|
521
|
+
}
|
|
522
|
+
let authRecord = accountManager.toAuthDetails(account);
|
|
523
|
+
if (accessTokenExpired(authRecord)) {
|
|
524
|
+
try {
|
|
525
|
+
const refreshed = await refreshAccessToken(authRecord, client, providerId);
|
|
526
|
+
if (!refreshed) {
|
|
527
|
+
const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
|
|
528
|
+
lastError = new Error("Antigravity token refresh failed");
|
|
529
|
+
if (shouldCooldown) {
|
|
530
|
+
accountManager.markRateLimited(account, cooldownMs, family, "antigravity");
|
|
531
|
+
pushDebug(`token-refresh-failed: cooldown ${cooldownMs}ms after ${failures} failures`);
|
|
341
532
|
}
|
|
533
|
+
continue;
|
|
342
534
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
535
|
+
resetAccountFailureState(account.index);
|
|
536
|
+
accountManager.updateFromAuth(account, refreshed);
|
|
537
|
+
authRecord = refreshed;
|
|
538
|
+
try {
|
|
539
|
+
await accountManager.saveToDisk();
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
console.error("[opencode-antigravity-auth] Failed to persist refreshed auth:", error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (error instanceof AntigravityTokenRefreshError && error.code === "invalid_grant") {
|
|
547
|
+
const removed = accountManager.removeAccount(account);
|
|
548
|
+
if (removed) {
|
|
549
|
+
console.warn("[opencode-antigravity-auth] Removed revoked account from pool. Reauthenticate it via `opencode auth login` to add it back.");
|
|
550
|
+
try {
|
|
551
|
+
await accountManager.saveToDisk();
|
|
552
|
+
}
|
|
553
|
+
catch (persistError) {
|
|
554
|
+
console.error("[opencode-antigravity-auth] Failed to persist revoked account removal:", persistError);
|
|
555
|
+
}
|
|
349
556
|
}
|
|
350
|
-
|
|
351
|
-
|
|
557
|
+
if (accountManager.getAccountCount() === 0) {
|
|
558
|
+
try {
|
|
559
|
+
await client.auth.set({
|
|
560
|
+
path: { id: providerId },
|
|
561
|
+
body: { type: "oauth", refresh: "", access: "", expires: 0 },
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch (storeError) {
|
|
565
|
+
console.error("Failed to clear stored Antigravity OAuth credentials:", storeError);
|
|
566
|
+
}
|
|
567
|
+
throw new Error("All Antigravity accounts have invalid refresh tokens. Run `opencode auth login` and reauthenticate.");
|
|
352
568
|
}
|
|
353
|
-
|
|
569
|
+
lastError = error;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
|
|
573
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
574
|
+
if (shouldCooldown) {
|
|
575
|
+
accountManager.markRateLimited(account, cooldownMs, family, "antigravity");
|
|
576
|
+
pushDebug(`token-refresh-error: cooldown ${cooldownMs}ms after ${failures} failures`);
|
|
354
577
|
}
|
|
355
|
-
lastError = error;
|
|
356
578
|
continue;
|
|
357
579
|
}
|
|
358
|
-
|
|
580
|
+
}
|
|
581
|
+
const accessToken = authRecord.access;
|
|
582
|
+
if (!accessToken) {
|
|
583
|
+
lastError = new Error("Missing access token");
|
|
359
584
|
continue;
|
|
360
585
|
}
|
|
361
|
-
|
|
362
|
-
const accessToken = authRecord.access;
|
|
363
|
-
if (!accessToken) {
|
|
364
|
-
lastError = new Error("Missing access token");
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
let projectContext;
|
|
368
|
-
try {
|
|
369
|
-
projectContext = await ensureProjectContext(authRecord);
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (projectContext.auth !== authRecord) {
|
|
376
|
-
accountManager.updateFromAuth(account, projectContext.auth);
|
|
377
|
-
authRecord = projectContext.auth;
|
|
586
|
+
let projectContext;
|
|
378
587
|
try {
|
|
379
|
-
await
|
|
588
|
+
projectContext = await ensureProjectContext(authRecord);
|
|
589
|
+
resetAccountFailureState(account.index);
|
|
380
590
|
}
|
|
381
591
|
catch (error) {
|
|
382
|
-
|
|
592
|
+
const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
|
|
593
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
594
|
+
if (shouldCooldown) {
|
|
595
|
+
accountManager.markRateLimited(account, cooldownMs, family, "antigravity");
|
|
596
|
+
pushDebug(`project-context-error: cooldown ${cooldownMs}ms after ${failures} failures`);
|
|
597
|
+
}
|
|
598
|
+
continue;
|
|
383
599
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
600
|
+
if (projectContext.auth !== authRecord) {
|
|
601
|
+
accountManager.updateFromAuth(account, projectContext.auth);
|
|
602
|
+
authRecord = projectContext.auth;
|
|
603
|
+
try {
|
|
604
|
+
await accountManager.saveToDisk();
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
console.error("[opencode-antigravity-auth] Failed to persist project context:", error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const runThinkingWarmup = async (prepared, projectId) => {
|
|
611
|
+
if (!prepared.needsSignedThinkingWarmup || !prepared.sessionId) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (!trackWarmupSession(prepared.sessionId)) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const warmupBody = buildThinkingWarmupBody(typeof prepared.init.body === "string" ? prepared.init.body : undefined, Boolean(prepared.effectiveModel?.toLowerCase().includes("claude") && prepared.effectiveModel?.toLowerCase().includes("thinking")));
|
|
618
|
+
if (!warmupBody) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const warmupUrl = toWarmupStreamUrl(prepared.request);
|
|
622
|
+
const warmupHeaders = new Headers(prepared.init.headers ?? {});
|
|
623
|
+
warmupHeaders.set("accept", "text/event-stream");
|
|
624
|
+
const warmupInit = {
|
|
625
|
+
...prepared.init,
|
|
626
|
+
method: prepared.init.method ?? "POST",
|
|
627
|
+
headers: warmupHeaders,
|
|
628
|
+
body: warmupBody,
|
|
629
|
+
};
|
|
630
|
+
const warmupDebugContext = startAntigravityDebugRequest({
|
|
631
|
+
originalUrl: warmupUrl,
|
|
632
|
+
resolvedUrl: warmupUrl,
|
|
633
|
+
method: warmupInit.method,
|
|
634
|
+
headers: warmupHeaders,
|
|
635
|
+
body: warmupBody,
|
|
636
|
+
streaming: true,
|
|
637
|
+
projectId,
|
|
401
638
|
});
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
639
|
+
try {
|
|
640
|
+
pushDebug("thinking-warmup: start");
|
|
641
|
+
const warmupResponse = await fetch(warmupUrl, warmupInit);
|
|
642
|
+
const transformed = await transformAntigravityResponse(warmupResponse, true, warmupDebugContext, prepared.requestedModel, projectId, warmupUrl, prepared.effectiveModel, prepared.sessionId);
|
|
643
|
+
await transformed.text();
|
|
644
|
+
pushDebug("thinking-warmup: done");
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
untrackWarmupSession(prepared.sessionId);
|
|
648
|
+
pushDebug(`thinking-warmup: failed ${error instanceof Error ? error.message : String(error)}`);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
// Try endpoint fallbacks with header style fallback for Gemini
|
|
652
|
+
let shouldSwitchAccount = false;
|
|
653
|
+
// For Gemini models, we can try both header styles (antigravity first, then gemini-cli)
|
|
654
|
+
// For Claude models, only antigravity headers work
|
|
655
|
+
const headerStyles = family === "gemini"
|
|
656
|
+
? ["antigravity", "gemini-cli"]
|
|
657
|
+
: ["antigravity"];
|
|
658
|
+
let currentHeaderStyleIndex = 0;
|
|
659
|
+
// Find first non-rate-limited header style for this account
|
|
660
|
+
while (currentHeaderStyleIndex < headerStyles.length) {
|
|
661
|
+
const hs = headerStyles[currentHeaderStyleIndex];
|
|
662
|
+
if (hs && !accountManager.isRateLimitedForHeaderStyle(account, family, hs)) {
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
currentHeaderStyleIndex++;
|
|
666
|
+
}
|
|
667
|
+
// If all header styles are rate-limited for this account, switch account
|
|
668
|
+
if (currentHeaderStyleIndex >= headerStyles.length) {
|
|
669
|
+
shouldSwitchAccount = true;
|
|
670
|
+
}
|
|
671
|
+
headerStyleLoop: while (!shouldSwitchAccount && currentHeaderStyleIndex < headerStyles.length) {
|
|
672
|
+
const currentHeaderStyle = headerStyles[currentHeaderStyleIndex];
|
|
673
|
+
pushDebug(`headerStyle=${currentHeaderStyle}`);
|
|
674
|
+
for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
|
|
675
|
+
const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
|
|
407
676
|
try {
|
|
408
|
-
|
|
677
|
+
const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint, currentHeaderStyle);
|
|
678
|
+
const originalUrl = toUrlString(input);
|
|
679
|
+
const resolvedUrl = toUrlString(prepared.request);
|
|
680
|
+
pushDebug(`endpoint=${currentEndpoint}`);
|
|
681
|
+
pushDebug(`resolved=${resolvedUrl}`);
|
|
682
|
+
const debugContext = startAntigravityDebugRequest({
|
|
683
|
+
originalUrl,
|
|
684
|
+
resolvedUrl,
|
|
685
|
+
method: prepared.init.method,
|
|
686
|
+
headers: prepared.init.headers,
|
|
687
|
+
body: prepared.init.body,
|
|
688
|
+
streaming: prepared.streaming,
|
|
689
|
+
projectId: projectContext.effectiveProjectId,
|
|
690
|
+
});
|
|
691
|
+
await runThinkingWarmup(prepared, projectContext.effectiveProjectId);
|
|
692
|
+
const response = await fetch(prepared.request, prepared.init);
|
|
693
|
+
pushDebug(`status=${response.status} ${response.statusText}`);
|
|
694
|
+
// Handle 429 rate limit with improved logic
|
|
695
|
+
if (response.status === 429) {
|
|
696
|
+
const headerRetryMs = retryAfterMsFromResponse(response);
|
|
697
|
+
const bodyInfo = await extractRetryInfoFromBody(response);
|
|
698
|
+
const serverRetryMs = bodyInfo.retryDelayMs ?? headerRetryMs;
|
|
699
|
+
const { attempt, delayMs } = getRateLimitBackoff(account.index, serverRetryMs);
|
|
700
|
+
const waitTimeFormatted = formatWaitTime(delayMs);
|
|
701
|
+
const isCapacityExhausted = bodyInfo.reason === "MODEL_CAPACITY_EXHAUSTED" ||
|
|
702
|
+
(typeof bodyInfo.message === "string" && bodyInfo.message.toLowerCase().includes("no capacity"));
|
|
703
|
+
pushDebug(`429 idx=${account.index} email=${account.email ?? ""} family=${family} delayMs=${delayMs} attempt=${attempt}`);
|
|
704
|
+
if (bodyInfo.message) {
|
|
705
|
+
pushDebug(`429 message=${bodyInfo.message}`);
|
|
706
|
+
}
|
|
707
|
+
if (bodyInfo.quotaResetTime) {
|
|
708
|
+
pushDebug(`429 quotaResetTime=${bodyInfo.quotaResetTime}`);
|
|
709
|
+
}
|
|
710
|
+
if (bodyInfo.reason) {
|
|
711
|
+
pushDebug(`429 reason=${bodyInfo.reason}`);
|
|
712
|
+
}
|
|
713
|
+
logRateLimitEvent(account.index, account.email, family, response.status, delayMs, bodyInfo);
|
|
714
|
+
await logResponseBody(debugContext, response, 429);
|
|
715
|
+
if (isCapacityExhausted) {
|
|
716
|
+
accountManager.markRateLimited(account, delayMs, family, currentHeaderStyle);
|
|
717
|
+
await showToast(`Model capacity exhausted for ${family}. Retrying in ${waitTimeFormatted} (attempt ${attempt})...`, "warning");
|
|
718
|
+
await sleep(delayMs, abortSignal);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
722
|
+
// Short retry: if delay is small, just wait and retry same account
|
|
723
|
+
if (delayMs <= SHORT_RETRY_THRESHOLD_MS) {
|
|
724
|
+
await showToast(`Rate limited. Retrying in ${waitTimeFormatted} (attempt ${attempt})...`, "warning");
|
|
725
|
+
await sleep(delayMs, abortSignal);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
// Mark this header style as rate-limited for this account
|
|
729
|
+
accountManager.markRateLimited(account, delayMs, family, currentHeaderStyle);
|
|
730
|
+
try {
|
|
731
|
+
await accountManager.saveToDisk();
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
console.error("[opencode-antigravity-auth] Failed to persist rate-limit state:", error);
|
|
735
|
+
}
|
|
736
|
+
// For Gemini, try next header style before switching accounts
|
|
737
|
+
if (family === "gemini" && currentHeaderStyleIndex < headerStyles.length - 1) {
|
|
738
|
+
const nextHeaderStyle = headerStyles[currentHeaderStyleIndex + 1];
|
|
739
|
+
await showToast(`Rate limited on ${currentHeaderStyle} quota. Trying ${nextHeaderStyle} quota...`, "warning");
|
|
740
|
+
currentHeaderStyleIndex++;
|
|
741
|
+
continue headerStyleLoop;
|
|
742
|
+
}
|
|
743
|
+
if (accountCount > 1) {
|
|
744
|
+
const quotaMsg = bodyInfo.quotaResetTime
|
|
745
|
+
? ` (quota resets ${bodyInfo.quotaResetTime})`
|
|
746
|
+
: ` (retry in ${waitTimeFormatted})`;
|
|
747
|
+
await showToast(`Rate limited on ${accountLabel}${quotaMsg}. Switching...`, "warning");
|
|
748
|
+
lastFailure = {
|
|
749
|
+
response,
|
|
750
|
+
streaming: prepared.streaming,
|
|
751
|
+
debugContext,
|
|
752
|
+
requestedModel: prepared.requestedModel,
|
|
753
|
+
projectId: prepared.projectId,
|
|
754
|
+
endpoint: prepared.endpoint,
|
|
755
|
+
effectiveModel: prepared.effectiveModel,
|
|
756
|
+
sessionId: prepared.sessionId,
|
|
757
|
+
toolDebugMissing: prepared.toolDebugMissing,
|
|
758
|
+
toolDebugSummary: prepared.toolDebugSummary,
|
|
759
|
+
toolDebugPayload: prepared.toolDebugPayload,
|
|
760
|
+
};
|
|
761
|
+
shouldSwitchAccount = true;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const quotaMsg = bodyInfo.quotaResetTime
|
|
766
|
+
? `Quota resets ${bodyInfo.quotaResetTime}`
|
|
767
|
+
: `Waiting ${waitTimeFormatted}`;
|
|
768
|
+
await showToast(`Rate limited. ${quotaMsg} (attempt ${attempt})...`, "warning");
|
|
769
|
+
lastFailure = {
|
|
770
|
+
response,
|
|
771
|
+
streaming: prepared.streaming,
|
|
772
|
+
debugContext,
|
|
773
|
+
requestedModel: prepared.requestedModel,
|
|
774
|
+
projectId: prepared.projectId,
|
|
775
|
+
endpoint: prepared.endpoint,
|
|
776
|
+
effectiveModel: prepared.effectiveModel,
|
|
777
|
+
sessionId: prepared.sessionId,
|
|
778
|
+
toolDebugMissing: prepared.toolDebugMissing,
|
|
779
|
+
toolDebugSummary: prepared.toolDebugSummary,
|
|
780
|
+
toolDebugPayload: prepared.toolDebugPayload,
|
|
781
|
+
};
|
|
782
|
+
await sleep(delayMs, abortSignal);
|
|
783
|
+
shouldSwitchAccount = true;
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// Success - reset rate limit backoff state
|
|
788
|
+
resetRateLimitState(account.index);
|
|
789
|
+
resetAccountFailureState(account.index);
|
|
790
|
+
const shouldRetryEndpoint = (response.status === 403 ||
|
|
791
|
+
response.status === 404 ||
|
|
792
|
+
response.status >= 500);
|
|
793
|
+
if (shouldRetryEndpoint) {
|
|
794
|
+
await logResponseBody(debugContext, response, response.status);
|
|
795
|
+
}
|
|
796
|
+
if (shouldRetryEndpoint && i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
|
|
797
|
+
lastFailure = {
|
|
798
|
+
response,
|
|
799
|
+
streaming: prepared.streaming,
|
|
800
|
+
debugContext,
|
|
801
|
+
requestedModel: prepared.requestedModel,
|
|
802
|
+
projectId: prepared.projectId,
|
|
803
|
+
endpoint: prepared.endpoint,
|
|
804
|
+
effectiveModel: prepared.effectiveModel,
|
|
805
|
+
sessionId: prepared.sessionId,
|
|
806
|
+
toolDebugMissing: prepared.toolDebugMissing,
|
|
807
|
+
toolDebugSummary: prepared.toolDebugSummary,
|
|
808
|
+
toolDebugPayload: prepared.toolDebugPayload,
|
|
809
|
+
};
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// Success or non-retryable error - return the response
|
|
813
|
+
logAntigravityDebugResponse(debugContext, response, {
|
|
814
|
+
note: response.ok ? "Success" : `Error ${response.status}`,
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
await logResponseBody(debugContext, response, response.status);
|
|
818
|
+
}
|
|
819
|
+
return transformAntigravityResponse(response, prepared.streaming, debugContext, prepared.requestedModel, prepared.projectId, prepared.endpoint, prepared.effectiveModel, prepared.sessionId, prepared.toolDebugMissing, prepared.toolDebugSummary, prepared.toolDebugPayload, debugLines);
|
|
409
820
|
}
|
|
410
821
|
catch (error) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
projectId: prepared.projectId,
|
|
423
|
-
endpoint: prepared.endpoint,
|
|
424
|
-
effectiveModel: prepared.effectiveModel,
|
|
425
|
-
sessionId: prepared.sessionId,
|
|
426
|
-
toolDebugMissing: prepared.toolDebugMissing,
|
|
427
|
-
toolDebugSummary: prepared.toolDebugSummary,
|
|
428
|
-
toolDebugPayload: prepared.toolDebugPayload,
|
|
429
|
-
};
|
|
822
|
+
if (i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
|
|
823
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
// All endpoints failed for this account - track failure and try next account
|
|
827
|
+
const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
|
|
828
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
829
|
+
if (shouldCooldown) {
|
|
830
|
+
accountManager.markRateLimited(account, cooldownMs, family, currentHeaderStyle);
|
|
831
|
+
pushDebug(`endpoint-error: cooldown ${cooldownMs}ms after ${failures} failures`);
|
|
832
|
+
}
|
|
430
833
|
shouldSwitchAccount = true;
|
|
431
834
|
break;
|
|
432
835
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
836
|
+
}
|
|
837
|
+
} // end headerStyleLoop
|
|
838
|
+
if (shouldSwitchAccount) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
// If we get here without returning, something went wrong
|
|
842
|
+
if (lastFailure) {
|
|
843
|
+
return transformAntigravityResponse(lastFailure.response, lastFailure.streaming, lastFailure.debugContext, lastFailure.requestedModel, lastFailure.projectId, lastFailure.endpoint, lastFailure.effectiveModel, lastFailure.sessionId, lastFailure.toolDebugMissing, lastFailure.toolDebugSummary, lastFailure.toolDebugPayload, debugLines);
|
|
844
|
+
}
|
|
845
|
+
throw lastError || new Error("All Antigravity accounts failed");
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
methods: [
|
|
851
|
+
{
|
|
852
|
+
label: "OAuth with Google (Antigravity)",
|
|
853
|
+
type: "oauth",
|
|
854
|
+
authorize: async (inputs) => {
|
|
855
|
+
const isHeadless = !!(process.env.SSH_CONNECTION ||
|
|
856
|
+
process.env.SSH_CLIENT ||
|
|
857
|
+
process.env.SSH_TTY ||
|
|
858
|
+
process.env.OPENCODE_HEADLESS);
|
|
859
|
+
// CLI flow (`opencode auth login`) passes an inputs object.
|
|
860
|
+
if (inputs) {
|
|
861
|
+
const accounts = [];
|
|
862
|
+
// Check for existing accounts and prompt user for login mode
|
|
863
|
+
let startFresh = true;
|
|
864
|
+
const existingStorage = await loadAccounts();
|
|
865
|
+
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
866
|
+
const existingAccounts = existingStorage.accounts.map((acc, idx) => ({
|
|
867
|
+
email: acc.email,
|
|
868
|
+
index: idx,
|
|
869
|
+
}));
|
|
870
|
+
const loginMode = await promptLoginMode(existingAccounts);
|
|
871
|
+
startFresh = loginMode === "fresh";
|
|
872
|
+
if (startFresh) {
|
|
873
|
+
console.log("\nStarting fresh - existing accounts will be replaced.\n");
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
console.log("\nAdding to existing accounts.\n");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
while (accounts.length < MAX_OAUTH_ACCOUNTS) {
|
|
880
|
+
console.log(`\n=== Antigravity OAuth (Account ${accounts.length + 1}) ===`);
|
|
881
|
+
const projectId = await promptProjectId();
|
|
882
|
+
const result = await (async () => {
|
|
883
|
+
let listener = null;
|
|
884
|
+
if (!isHeadless) {
|
|
885
|
+
try {
|
|
886
|
+
listener = await startOAuthListener();
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
listener = null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const authorization = await authorizeAntigravity(projectId);
|
|
893
|
+
const fallbackState = getStateFromAuthorizationUrl(authorization.url);
|
|
894
|
+
console.log("\nOAuth URL:\n" + authorization.url + "\n");
|
|
895
|
+
if (!isHeadless) {
|
|
896
|
+
await openBrowser(authorization.url);
|
|
897
|
+
}
|
|
898
|
+
if (listener) {
|
|
899
|
+
try {
|
|
900
|
+
const callbackUrl = await listener.waitForCallback();
|
|
901
|
+
const params = extractOAuthCallbackParams(callbackUrl);
|
|
902
|
+
if (!params) {
|
|
903
|
+
return { type: "failed", error: "Missing code or state in callback URL" };
|
|
904
|
+
}
|
|
905
|
+
return exchangeAntigravity(params.code, params.state);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
return {
|
|
909
|
+
type: "failed",
|
|
910
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
finally {
|
|
914
|
+
try {
|
|
915
|
+
await listener.close();
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
// ignore
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
console.log("1. Open the URL below in your browser and complete Google sign-in.");
|
|
923
|
+
console.log("2. After approving, copy the full redirected localhost URL from the address bar.");
|
|
924
|
+
console.log("3. Paste it back here.");
|
|
925
|
+
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
|
|
926
|
+
const params = parseOAuthCallbackInput(callbackInput, fallbackState);
|
|
927
|
+
if ("error" in params) {
|
|
928
|
+
return { type: "failed", error: params.error };
|
|
929
|
+
}
|
|
930
|
+
return exchangeAntigravity(params.code, params.state);
|
|
931
|
+
})();
|
|
932
|
+
if (result.type === "failed") {
|
|
933
|
+
if (accounts.length === 0) {
|
|
934
|
+
return {
|
|
935
|
+
url: "",
|
|
936
|
+
instructions: `Authentication failed: ${result.error}`,
|
|
937
|
+
method: "auto",
|
|
938
|
+
callback: async () => result,
|
|
449
939
|
};
|
|
450
|
-
// Wait and let the outer loop retry
|
|
451
|
-
await sleep(retryAfterMs, abortSignal);
|
|
452
|
-
shouldSwitchAccount = true;
|
|
453
|
-
break;
|
|
454
940
|
}
|
|
941
|
+
console.warn(`[opencode-antigravity-auth] Skipping failed account ${accounts.length + 1}: ${result.error}`);
|
|
942
|
+
break;
|
|
455
943
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
projectId: prepared.projectId,
|
|
466
|
-
endpoint: prepared.endpoint,
|
|
467
|
-
effectiveModel: prepared.effectiveModel,
|
|
468
|
-
sessionId: prepared.sessionId,
|
|
469
|
-
toolDebugMissing: prepared.toolDebugMissing,
|
|
470
|
-
toolDebugSummary: prepared.toolDebugSummary,
|
|
471
|
-
toolDebugPayload: prepared.toolDebugPayload,
|
|
472
|
-
};
|
|
473
|
-
continue;
|
|
944
|
+
accounts.push(result);
|
|
945
|
+
// Show toast for successful account authentication
|
|
946
|
+
try {
|
|
947
|
+
await client.tui.showToast({
|
|
948
|
+
body: {
|
|
949
|
+
message: `Account ${accounts.length} authenticated${result.email ? ` (${result.email})` : ""}`,
|
|
950
|
+
variant: "success",
|
|
951
|
+
},
|
|
952
|
+
});
|
|
474
953
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
954
|
+
catch {
|
|
955
|
+
// TUI may not be available in CLI mode
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
// Use startFresh only on first account, subsequent accounts always append
|
|
959
|
+
const isFirstAccount = accounts.length === 1;
|
|
960
|
+
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// ignore
|
|
964
|
+
}
|
|
965
|
+
if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
const addAnother = await promptAddAnotherAccount(accounts.length);
|
|
969
|
+
if (!addAnother) {
|
|
970
|
+
break;
|
|
482
971
|
}
|
|
483
|
-
// All endpoints failed for this account - try next account
|
|
484
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
485
|
-
shouldSwitchAccount = true;
|
|
486
|
-
break;
|
|
487
972
|
}
|
|
973
|
+
const primary = accounts[0];
|
|
974
|
+
if (!primary) {
|
|
975
|
+
return {
|
|
976
|
+
url: "",
|
|
977
|
+
instructions: "Authentication cancelled",
|
|
978
|
+
method: "auto",
|
|
979
|
+
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
url: "",
|
|
984
|
+
instructions: `Multi-account setup complete (${accounts.length} account(s)).`,
|
|
985
|
+
method: "auto",
|
|
986
|
+
callback: async () => primary,
|
|
987
|
+
};
|
|
488
988
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return transformAntigravityResponse(lastFailure.response, lastFailure.streaming, lastFailure.debugContext, lastFailure.requestedModel, lastFailure.projectId, lastFailure.endpoint, lastFailure.effectiveModel, lastFailure.sessionId, lastFailure.toolDebugMissing, lastFailure.toolDebugSummary, lastFailure.toolDebugPayload);
|
|
495
|
-
}
|
|
496
|
-
throw lastError || new Error("All Antigravity accounts failed");
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
},
|
|
501
|
-
methods: [
|
|
502
|
-
{
|
|
503
|
-
label: "OAuth with Google (Antigravity)",
|
|
504
|
-
type: "oauth",
|
|
505
|
-
authorize: async (inputs) => {
|
|
506
|
-
const isHeadless = !!(process.env.SSH_CONNECTION ||
|
|
507
|
-
process.env.SSH_CLIENT ||
|
|
508
|
-
process.env.SSH_TTY ||
|
|
509
|
-
process.env.OPENCODE_HEADLESS);
|
|
510
|
-
// CLI flow (`opencode auth login`) passes an inputs object.
|
|
511
|
-
if (inputs) {
|
|
512
|
-
const accounts = [];
|
|
513
|
-
// Check for existing accounts and prompt user for login mode
|
|
514
|
-
let startFresh = true;
|
|
989
|
+
// TUI flow (`/connect`) does not support per-account prompts.
|
|
990
|
+
// Default to adding new accounts (non-destructive).
|
|
991
|
+
// Users can run `opencode auth logout` first if they want a fresh start.
|
|
992
|
+
const projectId = "";
|
|
993
|
+
// Check existing accounts count for toast message
|
|
515
994
|
const existingStorage = await loadAccounts();
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const loginMode = await promptLoginMode(existingAccounts);
|
|
522
|
-
startFresh = loginMode === "fresh";
|
|
523
|
-
if (startFresh) {
|
|
524
|
-
console.log("\nStarting fresh - existing accounts will be replaced.\n");
|
|
995
|
+
const existingCount = existingStorage?.accounts.length ?? 0;
|
|
996
|
+
let listener = null;
|
|
997
|
+
if (!isHeadless) {
|
|
998
|
+
try {
|
|
999
|
+
listener = await startOAuthListener();
|
|
525
1000
|
}
|
|
526
|
-
|
|
527
|
-
|
|
1001
|
+
catch {
|
|
1002
|
+
listener = null;
|
|
528
1003
|
}
|
|
529
1004
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
const authorization = await authorizeAntigravity(projectId);
|
|
544
|
-
const fallbackState = getStateFromAuthorizationUrl(authorization.url);
|
|
545
|
-
console.log("\nOAuth URL:\n" + authorization.url + "\n");
|
|
546
|
-
if (!isHeadless) {
|
|
547
|
-
await openBrowser(authorization.url);
|
|
548
|
-
}
|
|
549
|
-
if (listener) {
|
|
1005
|
+
const authorization = await authorizeAntigravity(projectId);
|
|
1006
|
+
const fallbackState = getStateFromAuthorizationUrl(authorization.url);
|
|
1007
|
+
if (!isHeadless) {
|
|
1008
|
+
await openBrowser(authorization.url);
|
|
1009
|
+
}
|
|
1010
|
+
if (listener) {
|
|
1011
|
+
return {
|
|
1012
|
+
url: authorization.url,
|
|
1013
|
+
instructions: "Complete sign-in in your browser. We'll automatically detect the redirect back to localhost.",
|
|
1014
|
+
method: "auto",
|
|
1015
|
+
callback: async () => {
|
|
550
1016
|
try {
|
|
551
1017
|
const callbackUrl = await listener.waitForCallback();
|
|
552
1018
|
const params = extractOAuthCallbackParams(callbackUrl);
|
|
553
1019
|
if (!params) {
|
|
554
1020
|
return { type: "failed", error: "Missing code or state in callback URL" };
|
|
555
1021
|
}
|
|
556
|
-
|
|
1022
|
+
const result = await exchangeAntigravity(params.code, params.state);
|
|
1023
|
+
if (result.type === "success") {
|
|
1024
|
+
try {
|
|
1025
|
+
// TUI flow adds to existing accounts (non-destructive)
|
|
1026
|
+
await persistAccountPool([result], false);
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
// ignore
|
|
1030
|
+
}
|
|
1031
|
+
// Show appropriate toast message
|
|
1032
|
+
const newTotal = existingCount + 1;
|
|
1033
|
+
const toastMessage = existingCount > 0
|
|
1034
|
+
? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
|
|
1035
|
+
: `Authenticated${result.email ? ` (${result.email})` : ""}`;
|
|
1036
|
+
try {
|
|
1037
|
+
await client.tui.showToast({
|
|
1038
|
+
body: {
|
|
1039
|
+
message: toastMessage,
|
|
1040
|
+
variant: "success",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
// TUI may not be available
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return result;
|
|
557
1049
|
}
|
|
558
1050
|
catch (error) {
|
|
559
1051
|
return {
|
|
@@ -569,199 +1061,57 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
|
|
|
569
1061
|
// ignore
|
|
570
1062
|
}
|
|
571
1063
|
}
|
|
572
|
-
}
|
|
573
|
-
console.log("1. Open the URL below in your browser and complete Google sign-in.");
|
|
574
|
-
console.log("2. After approving, copy the full redirected localhost URL from the address bar.");
|
|
575
|
-
console.log("3. Paste it back here.");
|
|
576
|
-
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
|
|
577
|
-
const params = parseOAuthCallbackInput(callbackInput, fallbackState);
|
|
578
|
-
if ("error" in params) {
|
|
579
|
-
return { type: "failed", error: params.error };
|
|
580
|
-
}
|
|
581
|
-
return exchangeAntigravity(params.code, params.state);
|
|
582
|
-
})();
|
|
583
|
-
if (result.type === "failed") {
|
|
584
|
-
if (accounts.length === 0) {
|
|
585
|
-
return {
|
|
586
|
-
url: "",
|
|
587
|
-
instructions: `Authentication failed: ${result.error}`,
|
|
588
|
-
method: "auto",
|
|
589
|
-
callback: async () => result,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
console.warn(`[opencode-antigravity-auth] Skipping failed account ${accounts.length + 1}: ${result.error}`);
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
accounts.push(result);
|
|
596
|
-
// Show toast for successful account authentication
|
|
597
|
-
try {
|
|
598
|
-
await client.tui.showToast({
|
|
599
|
-
body: {
|
|
600
|
-
message: `Account ${accounts.length} authenticated${result.email ? ` (${result.email})` : ""}`,
|
|
601
|
-
variant: "success",
|
|
602
|
-
},
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
// TUI may not be available in CLI mode
|
|
607
|
-
}
|
|
608
|
-
try {
|
|
609
|
-
// Use startFresh only on first account, subsequent accounts always append
|
|
610
|
-
const isFirstAccount = accounts.length === 1;
|
|
611
|
-
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
612
|
-
}
|
|
613
|
-
catch {
|
|
614
|
-
// ignore
|
|
615
|
-
}
|
|
616
|
-
if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
const addAnother = await promptAddAnotherAccount(accounts.length);
|
|
620
|
-
if (!addAnother) {
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const primary = accounts[0];
|
|
625
|
-
if (!primary) {
|
|
626
|
-
return {
|
|
627
|
-
url: "",
|
|
628
|
-
instructions: "Authentication cancelled",
|
|
629
|
-
method: "auto",
|
|
630
|
-
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
1064
|
+
},
|
|
631
1065
|
};
|
|
632
1066
|
}
|
|
633
|
-
return {
|
|
634
|
-
url: "",
|
|
635
|
-
instructions: `Multi-account setup complete (${accounts.length} account(s)).`,
|
|
636
|
-
method: "auto",
|
|
637
|
-
callback: async () => primary,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
// TUI flow (`/connect`) does not support per-account prompts.
|
|
641
|
-
// Default to adding new accounts (non-destructive).
|
|
642
|
-
// Users can run `opencode auth logout` first if they want a fresh start.
|
|
643
|
-
const projectId = "";
|
|
644
|
-
// Check existing accounts count for toast message
|
|
645
|
-
const existingStorage = await loadAccounts();
|
|
646
|
-
const existingCount = existingStorage?.accounts.length ?? 0;
|
|
647
|
-
let listener = null;
|
|
648
|
-
if (!isHeadless) {
|
|
649
|
-
try {
|
|
650
|
-
listener = await startOAuthListener();
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
listener = null;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
const authorization = await authorizeAntigravity(projectId);
|
|
657
|
-
const fallbackState = getStateFromAuthorizationUrl(authorization.url);
|
|
658
|
-
if (!isHeadless) {
|
|
659
|
-
await openBrowser(authorization.url);
|
|
660
|
-
}
|
|
661
|
-
if (listener) {
|
|
662
1067
|
return {
|
|
663
1068
|
url: authorization.url,
|
|
664
|
-
instructions: "
|
|
665
|
-
method: "
|
|
666
|
-
callback: async () => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (!params) {
|
|
671
|
-
return { type: "failed", error: "Missing code or state in callback URL" };
|
|
672
|
-
}
|
|
673
|
-
const result = await exchangeAntigravity(params.code, params.state);
|
|
674
|
-
if (result.type === "success") {
|
|
675
|
-
try {
|
|
676
|
-
// TUI flow adds to existing accounts (non-destructive)
|
|
677
|
-
await persistAccountPool([result], false);
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
// ignore
|
|
681
|
-
}
|
|
682
|
-
// Show appropriate toast message
|
|
683
|
-
const newTotal = existingCount + 1;
|
|
684
|
-
const toastMessage = existingCount > 0
|
|
685
|
-
? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
|
|
686
|
-
: `Authenticated${result.email ? ` (${result.email})` : ""}`;
|
|
687
|
-
try {
|
|
688
|
-
await client.tui.showToast({
|
|
689
|
-
body: {
|
|
690
|
-
message: toastMessage,
|
|
691
|
-
variant: "success",
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
catch {
|
|
696
|
-
// TUI may not be available
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
return result;
|
|
700
|
-
}
|
|
701
|
-
catch (error) {
|
|
702
|
-
return {
|
|
703
|
-
type: "failed",
|
|
704
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
705
|
-
};
|
|
1069
|
+
instructions: "Visit the URL above, complete OAuth, then paste either the full redirect URL or the authorization code.",
|
|
1070
|
+
method: "code",
|
|
1071
|
+
callback: async (codeInput) => {
|
|
1072
|
+
const params = parseOAuthCallbackInput(codeInput, fallbackState);
|
|
1073
|
+
if ("error" in params) {
|
|
1074
|
+
return { type: "failed", error: params.error };
|
|
706
1075
|
}
|
|
707
|
-
|
|
1076
|
+
const result = await exchangeAntigravity(params.code, params.state);
|
|
1077
|
+
if (result.type === "success") {
|
|
708
1078
|
try {
|
|
709
|
-
|
|
1079
|
+
// TUI flow adds to existing accounts (non-destructive)
|
|
1080
|
+
await persistAccountPool([result], false);
|
|
710
1081
|
}
|
|
711
1082
|
catch {
|
|
712
1083
|
// ignore
|
|
713
1084
|
}
|
|
1085
|
+
// Show appropriate toast message
|
|
1086
|
+
const newTotal = existingCount + 1;
|
|
1087
|
+
const toastMessage = existingCount > 0
|
|
1088
|
+
? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
|
|
1089
|
+
: `Authenticated${result.email ? ` (${result.email})` : ""}`;
|
|
1090
|
+
try {
|
|
1091
|
+
await client.tui.showToast({
|
|
1092
|
+
body: {
|
|
1093
|
+
message: toastMessage,
|
|
1094
|
+
variant: "success",
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
// TUI may not be available
|
|
1100
|
+
}
|
|
714
1101
|
}
|
|
1102
|
+
return result;
|
|
715
1103
|
},
|
|
716
1104
|
};
|
|
717
|
-
}
|
|
718
|
-
return {
|
|
719
|
-
url: authorization.url,
|
|
720
|
-
instructions: "Visit the URL above, complete OAuth, then paste either the full redirect URL or the authorization code.",
|
|
721
|
-
method: "code",
|
|
722
|
-
callback: async (codeInput) => {
|
|
723
|
-
const params = parseOAuthCallbackInput(codeInput, fallbackState);
|
|
724
|
-
if ("error" in params) {
|
|
725
|
-
return { type: "failed", error: params.error };
|
|
726
|
-
}
|
|
727
|
-
const result = await exchangeAntigravity(params.code, params.state);
|
|
728
|
-
if (result.type === "success") {
|
|
729
|
-
try {
|
|
730
|
-
// TUI flow adds to existing accounts (non-destructive)
|
|
731
|
-
await persistAccountPool([result], false);
|
|
732
|
-
}
|
|
733
|
-
catch {
|
|
734
|
-
// ignore
|
|
735
|
-
}
|
|
736
|
-
// Show appropriate toast message
|
|
737
|
-
const newTotal = existingCount + 1;
|
|
738
|
-
const toastMessage = existingCount > 0
|
|
739
|
-
? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
|
|
740
|
-
: `Authenticated${result.email ? ` (${result.email})` : ""}`;
|
|
741
|
-
try {
|
|
742
|
-
await client.tui.showToast({
|
|
743
|
-
body: {
|
|
744
|
-
message: toastMessage,
|
|
745
|
-
variant: "success",
|
|
746
|
-
},
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
catch {
|
|
750
|
-
// TUI may not be available
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
return result;
|
|
754
|
-
},
|
|
755
|
-
};
|
|
1105
|
+
},
|
|
756
1106
|
},
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
}
|
|
1107
|
+
{
|
|
1108
|
+
label: "Manually enter API Key",
|
|
1109
|
+
type: "api",
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
};
|
|
1114
|
+
};
|
|
765
1115
|
export const AntigravityCLIOAuthPlugin = createAntigravityPlugin(ANTIGRAVITY_PROVIDER_ID);
|
|
766
1116
|
export const GoogleOAuthPlugin = AntigravityCLIOAuthPlugin;
|
|
767
1117
|
function toUrlString(value) {
|
|
@@ -774,4 +1124,33 @@ function toUrlString(value) {
|
|
|
774
1124
|
}
|
|
775
1125
|
return value.toString();
|
|
776
1126
|
}
|
|
1127
|
+
function toWarmupStreamUrl(value) {
|
|
1128
|
+
const urlString = toUrlString(value);
|
|
1129
|
+
try {
|
|
1130
|
+
const url = new URL(urlString);
|
|
1131
|
+
if (!url.pathname.includes(":streamGenerateContent")) {
|
|
1132
|
+
url.pathname = url.pathname.replace(":generateContent", ":streamGenerateContent");
|
|
1133
|
+
}
|
|
1134
|
+
url.searchParams.set("alt", "sse");
|
|
1135
|
+
return url.toString();
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
return urlString;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function extractModelFromUrl(urlString) {
|
|
1142
|
+
const match = urlString.match(/\/models\/([^:\/?]+)(?::\w+)?/);
|
|
1143
|
+
return match?.[1] ?? null;
|
|
1144
|
+
}
|
|
1145
|
+
function getModelFamilyFromUrl(urlString) {
|
|
1146
|
+
const model = extractModelFromUrl(urlString);
|
|
1147
|
+
let family = "gemini";
|
|
1148
|
+
if (model && model.includes("claude")) {
|
|
1149
|
+
family = "claude";
|
|
1150
|
+
}
|
|
1151
|
+
if (isDebugEnabled()) {
|
|
1152
|
+
logModelFamily(urlString, model, family);
|
|
1153
|
+
}
|
|
1154
|
+
return family;
|
|
1155
|
+
}
|
|
777
1156
|
//# sourceMappingURL=plugin.js.map
|