opencode-antigravity-auth 1.3.1-beta.2 → 1.3.1
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 +21 -15
- package/dist/src/antigravity/oauth.d.ts.map +1 -1
- package/dist/src/antigravity/oauth.js +4 -10
- package/dist/src/antigravity/oauth.js.map +1 -1
- package/dist/src/constants.d.ts +3 -24
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +3 -39
- package/dist/src/constants.js.map +1 -1
- package/dist/src/plugin/accounts.d.ts +3 -39
- package/dist/src/plugin/accounts.d.ts.map +1 -1
- package/dist/src/plugin/accounts.js +16 -152
- package/dist/src/plugin/accounts.js.map +1 -1
- package/dist/src/plugin/cli.d.ts +12 -15
- package/dist/src/plugin/cli.d.ts.map +1 -1
- package/dist/src/plugin/cli.js +13 -40
- package/dist/src/plugin/cli.js.map +1 -1
- package/dist/src/plugin/config/loader.d.ts.map +1 -1
- package/dist/src/plugin/config/loader.js +13 -0
- package/dist/src/plugin/config/loader.js.map +1 -1
- package/dist/src/plugin/config/schema.d.ts +319 -37
- package/dist/src/plugin/config/schema.d.ts.map +1 -1
- package/dist/src/plugin/config/schema.js +27 -57
- package/dist/src/plugin/config/schema.js.map +1 -1
- package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
- package/dist/src/plugin/core/streaming/transformer.js +6 -16
- package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
- package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
- package/dist/src/plugin/request-helpers.d.ts.map +1 -1
- package/dist/src/plugin/request-helpers.js +23 -61
- package/dist/src/plugin/request-helpers.js.map +1 -1
- package/dist/src/plugin/request.d.ts +1 -4
- package/dist/src/plugin/request.d.ts.map +1 -1
- package/dist/src/plugin/request.js +11 -72
- package/dist/src/plugin/request.js.map +1 -1
- package/dist/src/plugin/rotation.d.ts +4 -5
- package/dist/src/plugin/rotation.d.ts.map +1 -1
- package/dist/src/plugin/rotation.js +9 -35
- package/dist/src/plugin/rotation.js.map +1 -1
- package/dist/src/plugin/storage.d.ts +0 -2
- package/dist/src/plugin/storage.d.ts.map +1 -1
- package/dist/src/plugin/storage.js +2 -15
- package/dist/src/plugin/storage.js.map +1 -1
- package/dist/src/plugin/transform/gemini.d.ts +13 -1
- package/dist/src/plugin/transform/gemini.d.ts.map +1 -1
- package/dist/src/plugin/transform/gemini.js +12 -49
- package/dist/src/plugin/transform/gemini.js.map +1 -1
- package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
- package/dist/src/plugin/transform/model-resolver.js +2 -4
- package/dist/src/plugin/transform/model-resolver.js.map +1 -1
- package/dist/src/plugin/transform/types.d.ts +0 -5
- package/dist/src/plugin/transform/types.d.ts.map +1 -1
- package/dist/src/plugin/types.d.ts +0 -1
- package/dist/src/plugin/types.d.ts.map +1 -1
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +76 -294
- package/dist/src/plugin.js.map +1 -1
- package/package.json +5 -5
- package/dist/src/plugin/fingerprint.d.ts +0 -69
- package/dist/src/plugin/fingerprint.d.ts.map +0 -1
- package/dist/src/plugin/fingerprint.js +0 -153
- package/dist/src/plugin/fingerprint.js.map +0 -1
- package/dist/src/plugin/search.d.ts +0 -32
- package/dist/src/plugin/search.d.ts.map +0 -1
- package/dist/src/plugin/search.js +0 -197
- package/dist/src/plugin/search.js.map +0 -1
- package/dist/src/plugin/ui/ansi.d.ts +0 -32
- package/dist/src/plugin/ui/ansi.d.ts.map +0 -1
- package/dist/src/plugin/ui/ansi.js +0 -52
- package/dist/src/plugin/ui/ansi.js.map +0 -1
- package/dist/src/plugin/ui/auth-menu.d.ts +0 -24
- package/dist/src/plugin/ui/auth-menu.d.ts.map +0 -1
- package/dist/src/plugin/ui/auth-menu.js +0 -92
- package/dist/src/plugin/ui/auth-menu.js.map +0 -1
- package/dist/src/plugin/ui/confirm.d.ts +0 -2
- package/dist/src/plugin/ui/confirm.d.ts.map +0 -1
- package/dist/src/plugin/ui/confirm.js +0 -15
- package/dist/src/plugin/ui/confirm.js.map +0 -1
- package/dist/src/plugin/ui/select.d.ts +0 -14
- package/dist/src/plugin/ui/select.d.ts.map +0 -1
- package/dist/src/plugin/ui/select.js +0 -174
- package/dist/src/plugin/ui/select.js.map +0 -1
package/dist/src/plugin.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
-
import { tool } from "@opencode-ai/plugin";
|
|
3
2
|
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID } from "./constants";
|
|
4
3
|
import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
|
|
5
4
|
import { accessTokenExpired, isOAuthAuth, parseRefreshParts } from "./plugin/auth";
|
|
@@ -21,7 +20,6 @@ import { initDiskSignatureCache } from "./plugin/cache";
|
|
|
21
20
|
import { createProactiveRefreshQueue } from "./plugin/refresh-queue";
|
|
22
21
|
import { initLogger, createLogger } from "./plugin/logger";
|
|
23
22
|
import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker } from "./plugin/rotation";
|
|
24
|
-
import { executeSearch } from "./plugin/search";
|
|
25
23
|
const MAX_OAUTH_ACCOUNTS = 10;
|
|
26
24
|
const MAX_WARMUP_SESSIONS = 1000;
|
|
27
25
|
const MAX_WARMUP_RETRIES = 2;
|
|
@@ -285,7 +283,7 @@ async function persistAccountPool(results, replaceAll = false) {
|
|
|
285
283
|
},
|
|
286
284
|
});
|
|
287
285
|
}
|
|
288
|
-
function retryAfterMsFromResponse(response
|
|
286
|
+
function retryAfterMsFromResponse(response) {
|
|
289
287
|
const retryAfterMsHeader = response.headers.get("retry-after-ms");
|
|
290
288
|
if (retryAfterMsHeader) {
|
|
291
289
|
const parsed = Number.parseInt(retryAfterMsHeader, 10);
|
|
@@ -300,54 +298,20 @@ function retryAfterMsFromResponse(response, defaultRetryMs = 60_000) {
|
|
|
300
298
|
return parsed * 1000;
|
|
301
299
|
}
|
|
302
300
|
}
|
|
303
|
-
return
|
|
301
|
+
return 60_000;
|
|
304
302
|
}
|
|
305
|
-
/**
|
|
306
|
-
* Parse Go-style duration strings to milliseconds.
|
|
307
|
-
* Supports compound durations: "1h16m0.667s", "1.5s", "200ms", "5m30s"
|
|
308
|
-
*
|
|
309
|
-
* @param duration - Duration string in Go format
|
|
310
|
-
* @returns Duration in milliseconds, or null if parsing fails
|
|
311
|
-
*/
|
|
312
303
|
function parseDurationToMs(duration) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
default: return value * 1000;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
// Parse compound Go-style durations: "1h16m0.667s", "5m30s", etc.
|
|
327
|
-
const compoundRegex = /(\d+(?:\.\d+)?)(h|m(?!s)|s|ms)/gi;
|
|
328
|
-
let totalMs = 0;
|
|
329
|
-
let matchFound = false;
|
|
330
|
-
let match;
|
|
331
|
-
while ((match = compoundRegex.exec(duration)) !== null) {
|
|
332
|
-
matchFound = true;
|
|
333
|
-
const value = parseFloat(match[1]);
|
|
334
|
-
const unit = match[2].toLowerCase();
|
|
335
|
-
switch (unit) {
|
|
336
|
-
case "h":
|
|
337
|
-
totalMs += value * 3600 * 1000;
|
|
338
|
-
break;
|
|
339
|
-
case "m":
|
|
340
|
-
totalMs += value * 60 * 1000;
|
|
341
|
-
break;
|
|
342
|
-
case "s":
|
|
343
|
-
totalMs += value * 1000;
|
|
344
|
-
break;
|
|
345
|
-
case "ms":
|
|
346
|
-
totalMs += value;
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
304
|
+
const match = duration.match(/^(\d+(?:\.\d+)?)(s|m|h)?$/i);
|
|
305
|
+
if (!match)
|
|
306
|
+
return null;
|
|
307
|
+
const value = parseFloat(match[1]);
|
|
308
|
+
const unit = (match[2] || "s").toLowerCase();
|
|
309
|
+
switch (unit) {
|
|
310
|
+
case "h": return value * 3600 * 1000;
|
|
311
|
+
case "m": return value * 60 * 1000;
|
|
312
|
+
case "s": return value * 1000;
|
|
313
|
+
default: return value * 1000;
|
|
349
314
|
}
|
|
350
|
-
return matchFound ? totalMs : null;
|
|
351
315
|
}
|
|
352
316
|
function extractRateLimitBodyInfo(body) {
|
|
353
317
|
if (!body || typeof body !== "object") {
|
|
@@ -471,10 +435,9 @@ const emptyResponseAttempts = new Map();
|
|
|
471
435
|
* @param accountIndex - The account index
|
|
472
436
|
* @param quotaKey - The quota key (e.g., "gemini-cli", "gemini-antigravity", "claude")
|
|
473
437
|
* @param serverRetryAfterMs - Server-provided retry delay (if any)
|
|
474
|
-
* @param maxBackoffMs - Maximum backoff delay in milliseconds (default 60000)
|
|
475
438
|
* @returns { attempt, delayMs, isDuplicate } - isDuplicate=true if within dedup window
|
|
476
439
|
*/
|
|
477
|
-
function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs
|
|
440
|
+
function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs) {
|
|
478
441
|
const now = Date.now();
|
|
479
442
|
const stateKey = `${accountIndex}:${quotaKey}`;
|
|
480
443
|
const previous = rateLimitStateByAccountQuota.get(stateKey);
|
|
@@ -482,7 +445,7 @@ function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs, maxBack
|
|
|
482
445
|
if (previous && (now - previous.lastAt < RATE_LIMIT_DEDUP_WINDOW_MS)) {
|
|
483
446
|
// Same rate limit event from concurrent request - don't increment
|
|
484
447
|
const baseDelay = serverRetryAfterMs ?? 1000;
|
|
485
|
-
const backoffDelay = Math.min(baseDelay * Math.pow(2, previous.consecutive429 - 1),
|
|
448
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, previous.consecutive429 - 1), 60_000);
|
|
486
449
|
return {
|
|
487
450
|
attempt: previous.consecutive429,
|
|
488
451
|
delayMs: Math.max(baseDelay, backoffDelay),
|
|
@@ -499,7 +462,7 @@ function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs, maxBack
|
|
|
499
462
|
quotaKey
|
|
500
463
|
});
|
|
501
464
|
const baseDelay = serverRetryAfterMs ?? 1000;
|
|
502
|
-
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1),
|
|
465
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), 60_000);
|
|
503
466
|
return { attempt, delayMs: Math.max(baseDelay, backoffDelay), isDuplicate: false };
|
|
504
467
|
}
|
|
505
468
|
/**
|
|
@@ -577,8 +540,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
577
540
|
// Load configuration from files and environment variables
|
|
578
541
|
const config = loadConfig(directory);
|
|
579
542
|
initRuntimeConfig(config);
|
|
580
|
-
// Cached getAuth function for tool access
|
|
581
|
-
let cachedGetAuth = null;
|
|
582
543
|
// Initialize debug with config
|
|
583
544
|
initializeDebug(config);
|
|
584
545
|
// Initialize structured logger for TUI integration
|
|
@@ -655,55 +616,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
655
616
|
}
|
|
656
617
|
}
|
|
657
618
|
};
|
|
658
|
-
// Create google_search tool with access to auth context
|
|
659
|
-
const googleSearchTool = tool({
|
|
660
|
-
description: "Search the web using Google Search and analyze URLs. Returns real-time information from the internet with source citations. Use this when you need up-to-date information about current events, recent developments, or any topic that may have changed. You can also provide specific URLs to analyze. IMPORTANT: If the user mentions or provides any URLs in their query, you MUST extract those URLs and pass them in the 'urls' parameter for direct analysis.",
|
|
661
|
-
args: {
|
|
662
|
-
query: tool.schema.string().describe("The search query or question to answer using web search"),
|
|
663
|
-
urls: tool.schema.array(tool.schema.string()).optional().describe("List of specific URLs to fetch and analyze. IMPORTANT: Always extract and include any URLs mentioned by the user in their query here."),
|
|
664
|
-
thinking: tool.schema.boolean().optional().default(true).describe("Enable deep thinking for more thorough analysis (default: true)"),
|
|
665
|
-
},
|
|
666
|
-
async execute(args, ctx) {
|
|
667
|
-
log.debug("Google Search tool called", { query: args.query, urlCount: args.urls?.length ?? 0 });
|
|
668
|
-
// Get current auth context
|
|
669
|
-
const auth = cachedGetAuth ? await cachedGetAuth() : null;
|
|
670
|
-
if (!auth || !isOAuthAuth(auth)) {
|
|
671
|
-
return "Error: Not authenticated with Antigravity. Please run `opencode auth login` to authenticate.";
|
|
672
|
-
}
|
|
673
|
-
// Get access token and project ID
|
|
674
|
-
const parts = parseRefreshParts(auth.refresh);
|
|
675
|
-
const projectId = parts.managedProjectId || parts.projectId || "unknown";
|
|
676
|
-
// Ensure we have a valid access token
|
|
677
|
-
let accessToken = auth.access;
|
|
678
|
-
if (!accessToken || accessTokenExpired(auth)) {
|
|
679
|
-
try {
|
|
680
|
-
const refreshed = await refreshAccessToken(auth, client, providerId);
|
|
681
|
-
accessToken = refreshed?.access;
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
return `Error: Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (!accessToken) {
|
|
688
|
-
return "Error: No valid access token available. Please run `opencode auth login` to re-authenticate.";
|
|
689
|
-
}
|
|
690
|
-
return executeSearch({
|
|
691
|
-
query: args.query,
|
|
692
|
-
urls: args.urls,
|
|
693
|
-
thinking: args.thinking,
|
|
694
|
-
}, accessToken, projectId, ctx.abort);
|
|
695
|
-
},
|
|
696
|
-
});
|
|
697
619
|
return {
|
|
698
620
|
event: eventHandler,
|
|
699
|
-
tool: {
|
|
700
|
-
google_search: googleSearchTool,
|
|
701
|
-
},
|
|
702
621
|
auth: {
|
|
703
622
|
provider: providerId,
|
|
704
623
|
loader: async (getAuth, provider) => {
|
|
705
|
-
// Cache getAuth for tool access
|
|
706
|
-
cachedGetAuth = getAuth;
|
|
707
624
|
const auth = await getAuth();
|
|
708
625
|
// If OpenCode has no valid OAuth auth, clear any stale account storage
|
|
709
626
|
if (!isOAuthAuth(auth)) {
|
|
@@ -787,13 +704,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
787
704
|
throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("Aborted");
|
|
788
705
|
}
|
|
789
706
|
};
|
|
790
|
-
//
|
|
791
|
-
// This ensures we wait and retry when all accounts are rate-limited
|
|
792
|
-
const quietMode = config.quiet_mode;
|
|
793
|
-
// Helper to show toast without blocking on abort (respects quiet_mode)
|
|
707
|
+
// Helper to show toast without blocking on abort
|
|
794
708
|
const showToast = async (message, variant) => {
|
|
795
|
-
if (quietMode)
|
|
796
|
-
return;
|
|
797
709
|
if (abortSignal?.aborted)
|
|
798
710
|
return;
|
|
799
711
|
try {
|
|
@@ -805,6 +717,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
805
717
|
// TUI may not be available
|
|
806
718
|
}
|
|
807
719
|
};
|
|
720
|
+
// Use while(true) loop to handle rate limits with backoff
|
|
721
|
+
// This ensures we wait and retry when all accounts are rate-limited
|
|
722
|
+
const quietMode = config.quiet_mode;
|
|
808
723
|
const hasOtherAccountWithAntigravity = (currentAccount) => {
|
|
809
724
|
if (family !== "gemini")
|
|
810
725
|
return false;
|
|
@@ -820,10 +735,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
820
735
|
}
|
|
821
736
|
const account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, 'antigravity', config.pid_offset_enabled);
|
|
822
737
|
if (!account) {
|
|
823
|
-
const headerStyle = getHeaderStyleFromUrl(urlString, family);
|
|
824
|
-
const explicitQuota = isExplicitQuotaFromUrl(urlString);
|
|
825
738
|
// All accounts are rate-limited - wait and retry
|
|
826
|
-
const waitMs = accountManager.getMinWaitTimeForFamily(family, model
|
|
739
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(family, model) || 60_000;
|
|
827
740
|
const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000));
|
|
828
741
|
pushDebug(`all-rate-limited family=${family} accounts=${accountCount} waitMs=${waitMs}`);
|
|
829
742
|
if (isDebugEnabled()) {
|
|
@@ -860,8 +773,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
860
773
|
rateLimitState: account.rateLimitResetTimes,
|
|
861
774
|
});
|
|
862
775
|
}
|
|
863
|
-
// Show toast when switching to a different account (debounced,
|
|
864
|
-
if (accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
|
|
776
|
+
// Show toast when switching to a different account (debounced, respects quiet mode)
|
|
777
|
+
if (!quietMode && accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
|
|
865
778
|
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
866
779
|
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
867
780
|
accountManager.markToastShown(account.index);
|
|
@@ -1015,9 +928,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1015
928
|
let headerStyle = getHeaderStyleFromUrl(urlString, family);
|
|
1016
929
|
const explicitQuota = isExplicitQuotaFromUrl(urlString);
|
|
1017
930
|
pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`);
|
|
1018
|
-
if (account.fingerprint) {
|
|
1019
|
-
pushDebug(`fingerprint: quotaUser=${account.fingerprint.quotaUser} deviceId=${account.fingerprint.deviceId.slice(0, 8)}...`);
|
|
1020
|
-
}
|
|
1021
931
|
// Check if this header style is rate-limited for this account
|
|
1022
932
|
if (accountManager.isRateLimitedForHeaderStyle(account, family, headerStyle, model)) {
|
|
1023
933
|
// Quota fallback: try alternate quota on same account (if enabled and not explicit)
|
|
@@ -1026,7 +936,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1026
936
|
if (alternateStyle && alternateStyle !== headerStyle) {
|
|
1027
937
|
const quotaName = headerStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
|
|
1028
938
|
const altQuotaName = alternateStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
|
|
1029
|
-
|
|
939
|
+
if (!quietMode) {
|
|
940
|
+
await showToast(`${quotaName} quota exhausted, using ${altQuotaName} quota`, "warning");
|
|
941
|
+
}
|
|
1030
942
|
headerStyle = alternateStyle;
|
|
1031
943
|
pushDebug(`quota fallback: ${headerStyle}`);
|
|
1032
944
|
}
|
|
@@ -1043,20 +955,15 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1043
955
|
let forceThinkingRecovery = false;
|
|
1044
956
|
// Track if token was consumed (for hybrid strategy refund on error)
|
|
1045
957
|
let tokenConsumed = false;
|
|
1046
|
-
// Track capacity retries per endpoint to prevent infinite loops
|
|
1047
|
-
let capacityRetryCount = 0;
|
|
1048
|
-
let lastEndpointIndex = -1;
|
|
1049
958
|
for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
|
|
1050
|
-
// Reset capacity retry counter when switching to a new endpoint
|
|
1051
|
-
if (i !== lastEndpointIndex) {
|
|
1052
|
-
capacityRetryCount = 0;
|
|
1053
|
-
lastEndpointIndex = i;
|
|
1054
|
-
}
|
|
1055
959
|
const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
|
|
1056
960
|
try {
|
|
1057
961
|
const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint, headerStyle, forceThinkingRecovery, {
|
|
1058
962
|
claudeToolHardening: config.claude_tool_hardening,
|
|
1059
|
-
|
|
963
|
+
googleSearch: config.web_search ? {
|
|
964
|
+
mode: config.web_search.default_mode,
|
|
965
|
+
threshold: config.web_search.grounding_threshold
|
|
966
|
+
} : undefined,
|
|
1060
967
|
});
|
|
1061
968
|
const originalUrl = toUrlString(input);
|
|
1062
969
|
const resolvedUrl = toUrlString(prepared.request);
|
|
@@ -1079,58 +986,22 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1079
986
|
}
|
|
1080
987
|
const response = await fetch(prepared.request, prepared.init);
|
|
1081
988
|
pushDebug(`status=${response.status} ${response.statusText}`);
|
|
1082
|
-
// Handle 429 rate limit
|
|
1083
|
-
if (response.status === 429
|
|
989
|
+
// Handle 429 rate limit with improved logic
|
|
990
|
+
if (response.status === 429) {
|
|
1084
991
|
// Refund token on rate limit
|
|
1085
992
|
if (tokenConsumed) {
|
|
1086
993
|
getTokenTracker().refund(account.index);
|
|
1087
994
|
tokenConsumed = false;
|
|
1088
995
|
}
|
|
1089
|
-
const
|
|
1090
|
-
const maxBackoffMs = (config.max_backoff_seconds ?? 60) * 1000;
|
|
1091
|
-
const headerRetryMs = retryAfterMsFromResponse(response, defaultRetryMs);
|
|
996
|
+
const headerRetryMs = retryAfterMsFromResponse(response);
|
|
1092
997
|
const bodyInfo = await extractRetryInfoFromBody(response);
|
|
1093
998
|
const serverRetryMs = bodyInfo.retryDelayMs ?? headerRetryMs;
|
|
1094
|
-
// [Enhanced Parsing] Pass status to handling logic
|
|
1095
|
-
const rateLimitReason = parseRateLimitReason(bodyInfo.reason, bodyInfo.message, response.status);
|
|
1096
|
-
// STRATEGY 1: CAPACITY / SERVER ERROR (Transient)
|
|
1097
|
-
// Goal: Wait and Retry SAME Account. DO NOT LOCK.
|
|
1098
|
-
// We handle this FIRST to avoid calling getRateLimitBackoff() and polluting the global rate limit state for transient errors.
|
|
1099
|
-
if (rateLimitReason === "MODEL_CAPACITY_EXHAUSTED" || rateLimitReason === "SERVER_ERROR") {
|
|
1100
|
-
// Exponential backoff with jitter for capacity errors: 1s → 2s → 4s → 8s (max)
|
|
1101
|
-
// Matches Antigravity-Manager's ExponentialBackoff(1s, 8s)
|
|
1102
|
-
const baseDelayMs = 1000;
|
|
1103
|
-
const maxDelayMs = 8000;
|
|
1104
|
-
const exponentialDelay = Math.min(baseDelayMs * Math.pow(2, capacityRetryCount), maxDelayMs);
|
|
1105
|
-
// Add ±10% jitter to prevent thundering herd
|
|
1106
|
-
const jitter = exponentialDelay * (0.9 + Math.random() * 0.2);
|
|
1107
|
-
const waitMs = Math.round(jitter);
|
|
1108
|
-
const waitSec = Math.round(waitMs / 1000);
|
|
1109
|
-
pushDebug(`Server busy (${rateLimitReason}) on account ${account.index}, exponential backoff ${waitMs}ms (attempt ${capacityRetryCount + 1})`);
|
|
1110
|
-
await showToast(`⏳ Server busy (${response.status}). Retrying in ${waitSec}s...`, "warning");
|
|
1111
|
-
await sleep(waitMs, abortSignal);
|
|
1112
|
-
// CRITICAL FIX: Decrement i so that the loop 'continue' retries the SAME endpoint index
|
|
1113
|
-
// (i++ in the loop will bring it back to the current index)
|
|
1114
|
-
// But limit retries to prevent infinite loops (Greptile feedback)
|
|
1115
|
-
if (capacityRetryCount < 3) {
|
|
1116
|
-
capacityRetryCount++;
|
|
1117
|
-
i -= 1;
|
|
1118
|
-
continue;
|
|
1119
|
-
}
|
|
1120
|
-
else {
|
|
1121
|
-
pushDebug(`Max capacity retries (3) exhausted for endpoint ${currentEndpoint}, trying next endpoint...`);
|
|
1122
|
-
// Do not decrement i, loop will advance to next endpoint
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
// STRATEGY 2: RATE LIMIT EXCEEDED (RPM) / QUOTA EXHAUSTED / UNKNOWN
|
|
1127
|
-
// Goal: Lock and Rotate (Standard Logic)
|
|
1128
|
-
// Only now do we call getRateLimitBackoff, which increments the global failure tracker
|
|
1129
999
|
const quotaKey = headerStyleToQuotaKey(headerStyle, family);
|
|
1130
1000
|
const { attempt, delayMs, isDuplicate } = getRateLimitBackoff(account.index, quotaKey, serverRetryMs);
|
|
1131
|
-
|
|
1001
|
+
const rateLimitReason = parseRateLimitReason(bodyInfo.reason, bodyInfo.message);
|
|
1132
1002
|
const smartBackoffMs = calculateBackoffMs(rateLimitReason, account.consecutiveFailures ?? 0, serverRetryMs);
|
|
1133
1003
|
const effectiveDelayMs = Math.max(delayMs, smartBackoffMs);
|
|
1004
|
+
const isCapacityExhausted = rateLimitReason === "MODEL_CAPACITY_EXHAUSTED";
|
|
1134
1005
|
pushDebug(`429 idx=${account.index} email=${account.email ?? ""} family=${family} delayMs=${effectiveDelayMs} attempt=${attempt} reason=${rateLimitReason}`);
|
|
1135
1006
|
if (bodyInfo.message) {
|
|
1136
1007
|
pushDebug(`429 message=${bodyInfo.message}`);
|
|
@@ -1144,37 +1015,36 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1144
1015
|
logRateLimitEvent(account.index, account.email, family, response.status, effectiveDelayMs, bodyInfo);
|
|
1145
1016
|
await logResponseBody(debugContext, response, 429);
|
|
1146
1017
|
getHealthTracker().recordRateLimit(account.index);
|
|
1018
|
+
if (isCapacityExhausted) {
|
|
1019
|
+
const capacityBackoffMs = calculateBackoffMs(rateLimitReason, account.consecutiveFailures ?? 0, serverRetryMs);
|
|
1020
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1021
|
+
const backoffFormatted = formatWaitTime(capacityBackoffMs);
|
|
1022
|
+
const failures = account.consecutiveFailures ?? 0;
|
|
1023
|
+
pushDebug(`capacity exhausted on account ${account.index}, backoff=${capacityBackoffMs}ms (failure #${failures})`);
|
|
1024
|
+
// Check if we can switch to another account (respects switch_on_first_rate_limit config)
|
|
1025
|
+
if (config.switch_on_first_rate_limit && accountCount > 1) {
|
|
1026
|
+
await showToast(`Server at capacity. Switching account in 1s...`, "warning");
|
|
1027
|
+
await sleep(FIRST_RETRY_DELAY_MS, abortSignal);
|
|
1028
|
+
shouldSwitchAccount = true;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
// No other accounts available or config disabled - wait the backoff
|
|
1032
|
+
await showToast(`Server at capacity. Waiting ${backoffFormatted}... (attempt ${failures})`, "warning");
|
|
1033
|
+
await sleep(capacityBackoffMs, abortSignal);
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1147
1036
|
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
1148
|
-
|
|
1149
|
-
if (attempt === 1 && rateLimitReason !== "QUOTA_EXHAUSTED") {
|
|
1037
|
+
if (attempt === 1) {
|
|
1150
1038
|
await showToast(`Rate limited. Quick retry in 1s...`, "warning");
|
|
1151
1039
|
await sleep(FIRST_RETRY_DELAY_MS, abortSignal);
|
|
1152
|
-
// CacheFirst mode: wait for same account if within threshold (preserves prompt cache)
|
|
1153
|
-
if (config.scheduling_mode === 'cache_first') {
|
|
1154
|
-
const maxCacheFirstWaitMs = config.max_cache_first_wait_seconds * 1000;
|
|
1155
|
-
// effectiveDelayMs is the backoff calculated for this account
|
|
1156
|
-
if (effectiveDelayMs <= maxCacheFirstWaitMs) {
|
|
1157
|
-
pushDebug(`cache_first: waiting ${effectiveDelayMs}ms for same account to recover`);
|
|
1158
|
-
await showToast(`⏳ Waiting ${Math.ceil(effectiveDelayMs / 1000)}s for same account (prompt cache preserved)...`, "info");
|
|
1159
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1160
|
-
await sleep(effectiveDelayMs, abortSignal);
|
|
1161
|
-
// Retry same endpoint after wait
|
|
1162
|
-
i -= 1;
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
// Wait time exceeds threshold, fall through to switch
|
|
1166
|
-
pushDebug(`cache_first: wait ${effectiveDelayMs}ms exceeds max ${maxCacheFirstWaitMs}ms, switching account`);
|
|
1167
|
-
}
|
|
1168
1040
|
if (config.switch_on_first_rate_limit && accountCount > 1) {
|
|
1169
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs
|
|
1041
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1170
1042
|
shouldSwitchAccount = true;
|
|
1171
1043
|
break;
|
|
1172
1044
|
}
|
|
1173
|
-
// Same endpoint retry for first RPM hit
|
|
1174
|
-
i -= 1;
|
|
1175
1045
|
continue;
|
|
1176
1046
|
}
|
|
1177
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs
|
|
1047
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1178
1048
|
accountManager.requestSaveToDisk();
|
|
1179
1049
|
// For Gemini, try prioritized Antigravity across ALL accounts first
|
|
1180
1050
|
if (family === "gemini") {
|
|
@@ -1277,7 +1147,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1277
1147
|
if (response.ok) {
|
|
1278
1148
|
account.consecutiveFailures = 0;
|
|
1279
1149
|
getHealthTracker().recordSuccess(account.index);
|
|
1280
|
-
accountManager.markAccountUsed(account.index);
|
|
1281
1150
|
}
|
|
1282
1151
|
logAntigravityDebugResponse(debugContext, response, {
|
|
1283
1152
|
note: response.ok ? "Success" : `Error ${response.status}`,
|
|
@@ -1289,7 +1158,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1289
1158
|
const cloned = response.clone();
|
|
1290
1159
|
const bodyText = await cloned.text();
|
|
1291
1160
|
if (bodyText.includes("Prompt is too long") || bodyText.includes("prompt_too_long")) {
|
|
1292
|
-
|
|
1161
|
+
if (!quietMode) {
|
|
1162
|
+
await showToast("Context too long - use /compact to reduce size", "warning");
|
|
1163
|
+
}
|
|
1293
1164
|
const errorMessage = `[Antigravity Error] Context is too long for this model.\n\nPlease use /compact to reduce context size, then retry your request.\n\nAlternatively, you can:\n- Use /clear to start fresh\n- Use /undo to remove recent messages\n- Switch to a model with larger context window`;
|
|
1294
1165
|
return createSyntheticErrorResponse(errorMessage, prepared.requestedModel);
|
|
1295
1166
|
}
|
|
@@ -1326,7 +1197,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1326
1197
|
const transformedResponse = await transformAntigravityResponse(response, prepared.streaming, debugContext, prepared.requestedModel, prepared.projectId, prepared.endpoint, prepared.effectiveModel, prepared.sessionId, prepared.toolDebugMissing, prepared.toolDebugSummary, prepared.toolDebugPayload, debugLines);
|
|
1327
1198
|
// Check for context errors and show appropriate toast
|
|
1328
1199
|
const contextError = transformedResponse.headers.get("x-antigravity-context-error");
|
|
1329
|
-
if (contextError) {
|
|
1200
|
+
if (contextError && !quietMode) {
|
|
1330
1201
|
if (contextError === "prompt_too_long") {
|
|
1331
1202
|
await showToast("Context too long - use /compact to reduce size, or trim your request", "warning");
|
|
1332
1203
|
}
|
|
@@ -1418,82 +1289,18 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1418
1289
|
const useManualMode = noBrowser || shouldSkipLocalServer();
|
|
1419
1290
|
// Check for existing accounts and prompt user for login mode
|
|
1420
1291
|
let startFresh = true;
|
|
1421
|
-
let refreshAccountIndex;
|
|
1422
1292
|
const existingStorage = await loadAccounts();
|
|
1423
1293
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
status = 'rate-limited';
|
|
1432
|
-
}
|
|
1433
|
-
else {
|
|
1434
|
-
status = 'active';
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
else {
|
|
1438
|
-
status = 'active';
|
|
1439
|
-
}
|
|
1440
|
-
if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
|
|
1441
|
-
status = 'rate-limited';
|
|
1442
|
-
}
|
|
1443
|
-
return {
|
|
1444
|
-
email: acc.email,
|
|
1445
|
-
index: idx,
|
|
1446
|
-
addedAt: acc.addedAt,
|
|
1447
|
-
lastUsed: acc.lastUsed,
|
|
1448
|
-
status,
|
|
1449
|
-
isCurrentAccount: idx === (existingStorage.activeIndex ?? 0),
|
|
1450
|
-
};
|
|
1451
|
-
});
|
|
1452
|
-
const menuResult = await promptLoginMode(existingAccounts);
|
|
1453
|
-
if (menuResult.mode === "cancel") {
|
|
1454
|
-
return {
|
|
1455
|
-
url: "",
|
|
1456
|
-
instructions: "Authentication cancelled",
|
|
1457
|
-
method: "auto",
|
|
1458
|
-
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
if (menuResult.deleteAccountIndex !== undefined) {
|
|
1462
|
-
const updatedAccounts = existingStorage.accounts.filter((_, idx) => idx !== menuResult.deleteAccountIndex);
|
|
1463
|
-
await saveAccounts({
|
|
1464
|
-
version: 3,
|
|
1465
|
-
accounts: updatedAccounts,
|
|
1466
|
-
activeIndex: 0,
|
|
1467
|
-
activeIndexByFamily: { claude: 0, gemini: 0 },
|
|
1468
|
-
});
|
|
1469
|
-
console.log("\nAccount deleted.\n");
|
|
1470
|
-
if (updatedAccounts.length > 0) {
|
|
1471
|
-
return {
|
|
1472
|
-
url: "",
|
|
1473
|
-
instructions: "Account deleted. Please run `opencode auth login` again to continue.",
|
|
1474
|
-
method: "auto",
|
|
1475
|
-
callback: async () => ({ type: "failed", error: "Account deleted - please re-run auth" }),
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
if (menuResult.refreshAccountIndex !== undefined) {
|
|
1480
|
-
refreshAccountIndex = menuResult.refreshAccountIndex;
|
|
1481
|
-
const refreshEmail = existingStorage.accounts[refreshAccountIndex]?.email;
|
|
1482
|
-
console.log(`\nRe-authenticating ${refreshEmail || 'account'}...\n`);
|
|
1483
|
-
startFresh = false;
|
|
1484
|
-
}
|
|
1485
|
-
if (menuResult.deleteAll) {
|
|
1486
|
-
await clearAccounts();
|
|
1487
|
-
console.log("\nAll accounts deleted.\n");
|
|
1488
|
-
startFresh = true;
|
|
1489
|
-
}
|
|
1490
|
-
else {
|
|
1491
|
-
startFresh = menuResult.mode === "fresh";
|
|
1492
|
-
}
|
|
1493
|
-
if (startFresh && !menuResult.deleteAll) {
|
|
1294
|
+
const existingAccounts = existingStorage.accounts.map((acc, idx) => ({
|
|
1295
|
+
email: acc.email,
|
|
1296
|
+
index: idx,
|
|
1297
|
+
}));
|
|
1298
|
+
const loginMode = await promptLoginMode(existingAccounts);
|
|
1299
|
+
startFresh = loginMode === "fresh";
|
|
1300
|
+
if (startFresh) {
|
|
1494
1301
|
console.log("\nStarting fresh - existing accounts will be replaced.\n");
|
|
1495
1302
|
}
|
|
1496
|
-
else
|
|
1303
|
+
else {
|
|
1497
1304
|
console.log("\nAdding to existing accounts.\n");
|
|
1498
1305
|
}
|
|
1499
1306
|
}
|
|
@@ -1587,6 +1394,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1587
1394
|
break;
|
|
1588
1395
|
}
|
|
1589
1396
|
accounts.push(result);
|
|
1397
|
+
// Show toast for successful account authentication
|
|
1590
1398
|
try {
|
|
1591
1399
|
await client.tui.showToast({
|
|
1592
1400
|
body: {
|
|
@@ -1596,40 +1404,15 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1596
1404
|
});
|
|
1597
1405
|
}
|
|
1598
1406
|
catch {
|
|
1407
|
+
// TUI may not be available in CLI mode
|
|
1599
1408
|
}
|
|
1600
1409
|
try {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
const updatedAccounts = [...currentStorage.accounts];
|
|
1605
|
-
const parts = parseRefreshParts(result.refresh);
|
|
1606
|
-
if (parts.refreshToken) {
|
|
1607
|
-
updatedAccounts[refreshAccountIndex] = {
|
|
1608
|
-
email: result.email ?? updatedAccounts[refreshAccountIndex]?.email,
|
|
1609
|
-
refreshToken: parts.refreshToken,
|
|
1610
|
-
projectId: parts.projectId ?? updatedAccounts[refreshAccountIndex]?.projectId,
|
|
1611
|
-
managedProjectId: parts.managedProjectId ?? updatedAccounts[refreshAccountIndex]?.managedProjectId,
|
|
1612
|
-
addedAt: updatedAccounts[refreshAccountIndex]?.addedAt ?? Date.now(),
|
|
1613
|
-
lastUsed: Date.now(),
|
|
1614
|
-
};
|
|
1615
|
-
await saveAccounts({
|
|
1616
|
-
version: 3,
|
|
1617
|
-
accounts: updatedAccounts,
|
|
1618
|
-
activeIndex: currentStorage.activeIndex,
|
|
1619
|
-
activeIndexByFamily: currentStorage.activeIndexByFamily,
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1625
|
-
const isFirstAccount = accounts.length === 1;
|
|
1626
|
-
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
1627
|
-
}
|
|
1410
|
+
// Use startFresh only on first account, subsequent accounts always append
|
|
1411
|
+
const isFirstAccount = accounts.length === 1;
|
|
1412
|
+
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
1628
1413
|
}
|
|
1629
1414
|
catch {
|
|
1630
|
-
|
|
1631
|
-
if (refreshAccountIndex !== undefined) {
|
|
1632
|
-
break;
|
|
1415
|
+
// ignore
|
|
1633
1416
|
}
|
|
1634
1417
|
if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
|
|
1635
1418
|
break;
|
|
@@ -1659,6 +1442,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1659
1442
|
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
1660
1443
|
};
|
|
1661
1444
|
}
|
|
1445
|
+
// Get the actual deduplicated account count from storage
|
|
1662
1446
|
let actualAccountCount = accounts.length;
|
|
1663
1447
|
try {
|
|
1664
1448
|
const finalStorage = await loadAccounts();
|
|
@@ -1667,13 +1451,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1667
1451
|
}
|
|
1668
1452
|
}
|
|
1669
1453
|
catch {
|
|
1454
|
+
// Fall back to accounts.length if we can't read storage
|
|
1670
1455
|
}
|
|
1671
|
-
const successMessage = refreshAccountIndex !== undefined
|
|
1672
|
-
? `Token refreshed successfully.`
|
|
1673
|
-
: `Multi-account setup complete (${actualAccountCount} account(s)).`;
|
|
1674
1456
|
return {
|
|
1675
1457
|
url: "",
|
|
1676
|
-
instructions:
|
|
1458
|
+
instructions: `Multi-account setup complete (${actualAccountCount} account(s)).`,
|
|
1677
1459
|
method: "auto",
|
|
1678
1460
|
callback: async () => primary,
|
|
1679
1461
|
};
|