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.
Files changed (81) hide show
  1. package/README.md +21 -15
  2. package/dist/src/antigravity/oauth.d.ts.map +1 -1
  3. package/dist/src/antigravity/oauth.js +4 -10
  4. package/dist/src/antigravity/oauth.js.map +1 -1
  5. package/dist/src/constants.d.ts +3 -24
  6. package/dist/src/constants.d.ts.map +1 -1
  7. package/dist/src/constants.js +3 -39
  8. package/dist/src/constants.js.map +1 -1
  9. package/dist/src/plugin/accounts.d.ts +3 -39
  10. package/dist/src/plugin/accounts.d.ts.map +1 -1
  11. package/dist/src/plugin/accounts.js +16 -152
  12. package/dist/src/plugin/accounts.js.map +1 -1
  13. package/dist/src/plugin/cli.d.ts +12 -15
  14. package/dist/src/plugin/cli.d.ts.map +1 -1
  15. package/dist/src/plugin/cli.js +13 -40
  16. package/dist/src/plugin/cli.js.map +1 -1
  17. package/dist/src/plugin/config/loader.d.ts.map +1 -1
  18. package/dist/src/plugin/config/loader.js +13 -0
  19. package/dist/src/plugin/config/loader.js.map +1 -1
  20. package/dist/src/plugin/config/schema.d.ts +319 -37
  21. package/dist/src/plugin/config/schema.d.ts.map +1 -1
  22. package/dist/src/plugin/config/schema.js +27 -57
  23. package/dist/src/plugin/config/schema.js.map +1 -1
  24. package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
  25. package/dist/src/plugin/core/streaming/transformer.js +6 -16
  26. package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
  27. package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
  28. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  29. package/dist/src/plugin/request-helpers.js +23 -61
  30. package/dist/src/plugin/request-helpers.js.map +1 -1
  31. package/dist/src/plugin/request.d.ts +1 -4
  32. package/dist/src/plugin/request.d.ts.map +1 -1
  33. package/dist/src/plugin/request.js +11 -72
  34. package/dist/src/plugin/request.js.map +1 -1
  35. package/dist/src/plugin/rotation.d.ts +4 -5
  36. package/dist/src/plugin/rotation.d.ts.map +1 -1
  37. package/dist/src/plugin/rotation.js +9 -35
  38. package/dist/src/plugin/rotation.js.map +1 -1
  39. package/dist/src/plugin/storage.d.ts +0 -2
  40. package/dist/src/plugin/storage.d.ts.map +1 -1
  41. package/dist/src/plugin/storage.js +2 -15
  42. package/dist/src/plugin/storage.js.map +1 -1
  43. package/dist/src/plugin/transform/gemini.d.ts +13 -1
  44. package/dist/src/plugin/transform/gemini.d.ts.map +1 -1
  45. package/dist/src/plugin/transform/gemini.js +12 -49
  46. package/dist/src/plugin/transform/gemini.js.map +1 -1
  47. package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
  48. package/dist/src/plugin/transform/model-resolver.js +2 -4
  49. package/dist/src/plugin/transform/model-resolver.js.map +1 -1
  50. package/dist/src/plugin/transform/types.d.ts +0 -5
  51. package/dist/src/plugin/transform/types.d.ts.map +1 -1
  52. package/dist/src/plugin/types.d.ts +0 -1
  53. package/dist/src/plugin/types.d.ts.map +1 -1
  54. package/dist/src/plugin.d.ts.map +1 -1
  55. package/dist/src/plugin.js +76 -294
  56. package/dist/src/plugin.js.map +1 -1
  57. package/package.json +5 -5
  58. package/dist/src/plugin/fingerprint.d.ts +0 -69
  59. package/dist/src/plugin/fingerprint.d.ts.map +0 -1
  60. package/dist/src/plugin/fingerprint.js +0 -153
  61. package/dist/src/plugin/fingerprint.js.map +0 -1
  62. package/dist/src/plugin/search.d.ts +0 -32
  63. package/dist/src/plugin/search.d.ts.map +0 -1
  64. package/dist/src/plugin/search.js +0 -197
  65. package/dist/src/plugin/search.js.map +0 -1
  66. package/dist/src/plugin/ui/ansi.d.ts +0 -32
  67. package/dist/src/plugin/ui/ansi.d.ts.map +0 -1
  68. package/dist/src/plugin/ui/ansi.js +0 -52
  69. package/dist/src/plugin/ui/ansi.js.map +0 -1
  70. package/dist/src/plugin/ui/auth-menu.d.ts +0 -24
  71. package/dist/src/plugin/ui/auth-menu.d.ts.map +0 -1
  72. package/dist/src/plugin/ui/auth-menu.js +0 -92
  73. package/dist/src/plugin/ui/auth-menu.js.map +0 -1
  74. package/dist/src/plugin/ui/confirm.d.ts +0 -2
  75. package/dist/src/plugin/ui/confirm.d.ts.map +0 -1
  76. package/dist/src/plugin/ui/confirm.js +0 -15
  77. package/dist/src/plugin/ui/confirm.js.map +0 -1
  78. package/dist/src/plugin/ui/select.d.ts +0 -14
  79. package/dist/src/plugin/ui/select.d.ts.map +0 -1
  80. package/dist/src/plugin/ui/select.js +0 -174
  81. package/dist/src/plugin/ui/select.js.map +0 -1
@@ -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, defaultRetryMs = 60_000) {
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 defaultRetryMs;
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
- // Handle simple formats first for backwards compatibility
314
- const simpleMatch = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i);
315
- if (simpleMatch) {
316
- const value = parseFloat(simpleMatch[1]);
317
- const unit = (simpleMatch[2] || "s").toLowerCase();
318
- switch (unit) {
319
- case "h": return value * 3600 * 1000;
320
- case "m": return value * 60 * 1000;
321
- case "s": return value * 1000;
322
- case "ms": return value;
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, maxBackoffMs = 60_000) {
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), maxBackoffMs);
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), maxBackoffMs);
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
- // Use while(true) loop to handle rate limits with backoff
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, headerStyle, explicitQuota) || 60_000;
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, quiet_mode handled by showToast)
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
- await showToast(`${quotaName} quota exhausted, using ${altQuotaName} quota`, "warning");
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
- fingerprint: account.fingerprint,
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 (or Service Overloaded) with improved logic
1083
- if (response.status === 429 || response.status === 503 || response.status === 529) {
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 defaultRetryMs = (config.default_retry_after_seconds ?? 60) * 1000;
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
- // Calculate potential backoffs
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
- // Progressive retry for standard 429s: 1st 429 → 1s then switch (if enabled) or retry same
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, config.failure_ttl_seconds * 1000);
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, config.failure_ttl_seconds * 1000);
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
- await showToast("Context too long - use /compact to reduce size", "warning");
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 now = Date.now();
1425
- const existingAccounts = existingStorage.accounts.map((acc, idx) => {
1426
- let status = 'unknown';
1427
- const rateLimits = acc.rateLimitResetTimes;
1428
- if (rateLimits) {
1429
- const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === 'number' && resetTime > now);
1430
- if (isRateLimited) {
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 if (!startFresh) {
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
- if (refreshAccountIndex !== undefined) {
1602
- const currentStorage = await loadAccounts();
1603
- if (currentStorage) {
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: successMessage,
1458
+ instructions: `Multi-account setup complete (${actualAccountCount} account(s)).`,
1677
1459
  method: "auto",
1678
1460
  callback: async () => primary,
1679
1461
  };