antigravity-auth 1.6.0 → 1.7.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 (258) hide show
  1. package/README.md +79 -41
  2. package/dist/cli.js +17854 -0
  3. package/dist/handler.js +25147 -0
  4. package/dist/index.js +25138 -5
  5. package/package.json +66 -54
  6. package/dist/antigravity/oauth.d.ts +0 -30
  7. package/dist/antigravity/oauth.js +0 -170
  8. package/dist/claude/login.d.ts +0 -7
  9. package/dist/claude/login.js +0 -480
  10. package/dist/claude/menu-helpers.d.ts +0 -22
  11. package/dist/claude/menu-helpers.js +0 -281
  12. package/dist/claude/proxy-manager.d.ts +0 -11
  13. package/dist/claude/proxy-manager.js +0 -129
  14. package/dist/claude/proxy.d.ts +0 -1
  15. package/dist/claude/proxy.js +0 -733
  16. package/dist/constants.d.ts +0 -138
  17. package/dist/constants.js +0 -216
  18. package/dist/hooks/auto-update-checker/cache.d.ts +0 -2
  19. package/dist/hooks/auto-update-checker/cache.js +0 -70
  20. package/dist/hooks/auto-update-checker/checker.d.ts +0 -15
  21. package/dist/hooks/auto-update-checker/checker.js +0 -233
  22. package/dist/hooks/auto-update-checker/constants.d.ts +0 -8
  23. package/dist/hooks/auto-update-checker/constants.js +0 -22
  24. package/dist/hooks/auto-update-checker/index.d.ts +0 -33
  25. package/dist/hooks/auto-update-checker/index.js +0 -121
  26. package/dist/hooks/auto-update-checker/logging.d.ts +0 -2
  27. package/dist/hooks/auto-update-checker/logging.js +0 -8
  28. package/dist/hooks/auto-update-checker/types.d.ts +0 -24
  29. package/dist/hooks/auto-update-checker/types.js +0 -1
  30. package/dist/index.d.ts +0 -6
  31. package/dist/opencode/hooks/auto-update-checker/cache.d.ts +0 -2
  32. package/dist/opencode/hooks/auto-update-checker/cache.js +0 -70
  33. package/dist/opencode/hooks/auto-update-checker/checker.d.ts +0 -15
  34. package/dist/opencode/hooks/auto-update-checker/checker.js +0 -233
  35. package/dist/opencode/hooks/auto-update-checker/constants.d.ts +0 -8
  36. package/dist/opencode/hooks/auto-update-checker/constants.js +0 -22
  37. package/dist/opencode/hooks/auto-update-checker/index.d.ts +0 -33
  38. package/dist/opencode/hooks/auto-update-checker/index.js +0 -121
  39. package/dist/opencode/hooks/auto-update-checker/logging.d.ts +0 -2
  40. package/dist/opencode/hooks/auto-update-checker/logging.js +0 -8
  41. package/dist/opencode/hooks/auto-update-checker/types.d.ts +0 -24
  42. package/dist/opencode/hooks/auto-update-checker/types.js +0 -1
  43. package/dist/opencode/plugin.d.ts +0 -29
  44. package/dist/opencode/plugin.js +0 -2954
  45. package/dist/plugin/accounts.d.ts +0 -173
  46. package/dist/plugin/accounts.js +0 -966
  47. package/dist/plugin/auth.d.ts +0 -20
  48. package/dist/plugin/auth.js +0 -44
  49. package/dist/plugin/cache/index.d.ts +0 -4
  50. package/dist/plugin/cache/index.js +0 -4
  51. package/dist/plugin/cache/signature-cache.d.ts +0 -110
  52. package/dist/plugin/cache/signature-cache.js +0 -347
  53. package/dist/plugin/cache.d.ts +0 -43
  54. package/dist/plugin/cache.js +0 -180
  55. package/dist/plugin/cli.d.ts +0 -26
  56. package/dist/plugin/cli.js +0 -126
  57. package/dist/plugin/config/index.d.ts +0 -15
  58. package/dist/plugin/config/index.js +0 -15
  59. package/dist/plugin/config/loader.d.ts +0 -38
  60. package/dist/plugin/config/loader.js +0 -150
  61. package/dist/plugin/config/models.d.ts +0 -26
  62. package/dist/plugin/config/models.js +0 -95
  63. package/dist/plugin/config/schema.d.ts +0 -144
  64. package/dist/plugin/config/schema.js +0 -458
  65. package/dist/plugin/config/updater.d.ts +0 -76
  66. package/dist/plugin/config/updater.js +0 -205
  67. package/dist/plugin/core/streaming/index.d.ts +0 -2
  68. package/dist/plugin/core/streaming/index.js +0 -2
  69. package/dist/plugin/core/streaming/transformer.d.ts +0 -9
  70. package/dist/plugin/core/streaming/transformer.js +0 -301
  71. package/dist/plugin/core/streaming/types.d.ts +0 -28
  72. package/dist/plugin/core/streaming/types.js +0 -1
  73. package/dist/plugin/debug.d.ts +0 -93
  74. package/dist/plugin/debug.js +0 -375
  75. package/dist/plugin/errors.d.ts +0 -27
  76. package/dist/plugin/errors.js +0 -41
  77. package/dist/plugin/fingerprint.d.ts +0 -69
  78. package/dist/plugin/fingerprint.js +0 -137
  79. package/dist/plugin/image-saver.d.ts +0 -24
  80. package/dist/plugin/image-saver.js +0 -78
  81. package/dist/plugin/logger.d.ts +0 -35
  82. package/dist/plugin/logger.js +0 -67
  83. package/dist/plugin/logging-utils.d.ts +0 -22
  84. package/dist/plugin/logging-utils.js +0 -91
  85. package/dist/plugin/project.d.ts +0 -32
  86. package/dist/plugin/project.js +0 -229
  87. package/dist/plugin/quota.d.ts +0 -34
  88. package/dist/plugin/quota.js +0 -261
  89. package/dist/plugin/recovery/constants.d.ts +0 -21
  90. package/dist/plugin/recovery/constants.js +0 -42
  91. package/dist/plugin/recovery/index.d.ts +0 -11
  92. package/dist/plugin/recovery/index.js +0 -11
  93. package/dist/plugin/recovery/storage.d.ts +0 -23
  94. package/dist/plugin/recovery/storage.js +0 -340
  95. package/dist/plugin/recovery/types.d.ts +0 -115
  96. package/dist/plugin/recovery/types.js +0 -6
  97. package/dist/plugin/recovery.d.ts +0 -60
  98. package/dist/plugin/recovery.js +0 -360
  99. package/dist/plugin/refresh-queue.d.ts +0 -99
  100. package/dist/plugin/refresh-queue.js +0 -235
  101. package/dist/plugin/request-helpers.d.ts +0 -281
  102. package/dist/plugin/request-helpers.js +0 -2200
  103. package/dist/plugin/request.d.ts +0 -110
  104. package/dist/plugin/request.js +0 -1489
  105. package/dist/plugin/rotation.d.ts +0 -182
  106. package/dist/plugin/rotation.js +0 -364
  107. package/dist/plugin/search.d.ts +0 -31
  108. package/dist/plugin/search.js +0 -185
  109. package/dist/plugin/server.d.ts +0 -22
  110. package/dist/plugin/server.js +0 -306
  111. package/dist/plugin/storage.d.ts +0 -136
  112. package/dist/plugin/storage.js +0 -599
  113. package/dist/plugin/stores/signature-store.d.ts +0 -4
  114. package/dist/plugin/stores/signature-store.js +0 -24
  115. package/dist/plugin/thinking-recovery.d.ts +0 -89
  116. package/dist/plugin/thinking-recovery.js +0 -289
  117. package/dist/plugin/token.d.ts +0 -18
  118. package/dist/plugin/token.js +0 -127
  119. package/dist/plugin/transform/claude.d.ts +0 -79
  120. package/dist/plugin/transform/claude.js +0 -256
  121. package/dist/plugin/transform/cross-model-sanitizer.d.ts +0 -34
  122. package/dist/plugin/transform/cross-model-sanitizer.js +0 -224
  123. package/dist/plugin/transform/gemini.d.ts +0 -132
  124. package/dist/plugin/transform/gemini.js +0 -659
  125. package/dist/plugin/transform/index.d.ts +0 -14
  126. package/dist/plugin/transform/index.js +0 -9
  127. package/dist/plugin/transform/model-resolver.d.ts +0 -98
  128. package/dist/plugin/transform/model-resolver.js +0 -320
  129. package/dist/plugin/transform/types.d.ts +0 -110
  130. package/dist/plugin/transform/types.js +0 -1
  131. package/dist/plugin/types.d.ts +0 -95
  132. package/dist/plugin/types.js +0 -1
  133. package/dist/plugin/ui/ansi.d.ts +0 -31
  134. package/dist/plugin/ui/ansi.js +0 -45
  135. package/dist/plugin/ui/auth-menu.d.ts +0 -47
  136. package/dist/plugin/ui/auth-menu.js +0 -199
  137. package/dist/plugin/ui/confirm.d.ts +0 -1
  138. package/dist/plugin/ui/confirm.js +0 -14
  139. package/dist/plugin/ui/select.d.ts +0 -22
  140. package/dist/plugin/ui/select.js +0 -243
  141. package/dist/plugin/version.d.ts +0 -18
  142. package/dist/plugin/version.js +0 -79
  143. package/dist/src/antigravity/oauth.d.ts +0 -30
  144. package/dist/src/antigravity/oauth.js +0 -170
  145. package/dist/src/constants.d.ts +0 -138
  146. package/dist/src/constants.js +0 -216
  147. package/dist/src/hooks/auto-update-checker/cache.d.ts +0 -2
  148. package/dist/src/hooks/auto-update-checker/cache.js +0 -70
  149. package/dist/src/hooks/auto-update-checker/checker.d.ts +0 -15
  150. package/dist/src/hooks/auto-update-checker/checker.js +0 -233
  151. package/dist/src/hooks/auto-update-checker/constants.d.ts +0 -8
  152. package/dist/src/hooks/auto-update-checker/constants.js +0 -22
  153. package/dist/src/hooks/auto-update-checker/index.d.ts +0 -33
  154. package/dist/src/hooks/auto-update-checker/index.js +0 -121
  155. package/dist/src/hooks/auto-update-checker/logging.d.ts +0 -2
  156. package/dist/src/hooks/auto-update-checker/logging.js +0 -8
  157. package/dist/src/hooks/auto-update-checker/types.d.ts +0 -24
  158. package/dist/src/hooks/auto-update-checker/types.js +0 -1
  159. package/dist/src/index.d.ts +0 -6
  160. package/dist/src/index.js +0 -5
  161. package/dist/src/plugin/accounts.d.ts +0 -173
  162. package/dist/src/plugin/accounts.js +0 -966
  163. package/dist/src/plugin/auth.d.ts +0 -20
  164. package/dist/src/plugin/auth.js +0 -44
  165. package/dist/src/plugin/cache/index.d.ts +0 -4
  166. package/dist/src/plugin/cache/index.js +0 -4
  167. package/dist/src/plugin/cache/signature-cache.d.ts +0 -110
  168. package/dist/src/plugin/cache/signature-cache.js +0 -347
  169. package/dist/src/plugin/cache.d.ts +0 -43
  170. package/dist/src/plugin/cache.js +0 -180
  171. package/dist/src/plugin/cli.d.ts +0 -26
  172. package/dist/src/plugin/cli.js +0 -126
  173. package/dist/src/plugin/config/index.d.ts +0 -15
  174. package/dist/src/plugin/config/index.js +0 -15
  175. package/dist/src/plugin/config/loader.d.ts +0 -38
  176. package/dist/src/plugin/config/loader.js +0 -150
  177. package/dist/src/plugin/config/models.d.ts +0 -26
  178. package/dist/src/plugin/config/models.js +0 -95
  179. package/dist/src/plugin/config/schema.d.ts +0 -144
  180. package/dist/src/plugin/config/schema.js +0 -458
  181. package/dist/src/plugin/config/updater.d.ts +0 -76
  182. package/dist/src/plugin/config/updater.js +0 -205
  183. package/dist/src/plugin/core/streaming/index.d.ts +0 -2
  184. package/dist/src/plugin/core/streaming/index.js +0 -2
  185. package/dist/src/plugin/core/streaming/transformer.d.ts +0 -9
  186. package/dist/src/plugin/core/streaming/transformer.js +0 -301
  187. package/dist/src/plugin/core/streaming/types.d.ts +0 -28
  188. package/dist/src/plugin/core/streaming/types.js +0 -1
  189. package/dist/src/plugin/debug.d.ts +0 -93
  190. package/dist/src/plugin/debug.js +0 -375
  191. package/dist/src/plugin/errors.d.ts +0 -27
  192. package/dist/src/plugin/errors.js +0 -41
  193. package/dist/src/plugin/fingerprint.d.ts +0 -69
  194. package/dist/src/plugin/fingerprint.js +0 -137
  195. package/dist/src/plugin/image-saver.d.ts +0 -24
  196. package/dist/src/plugin/image-saver.js +0 -78
  197. package/dist/src/plugin/logger.d.ts +0 -35
  198. package/dist/src/plugin/logger.js +0 -67
  199. package/dist/src/plugin/logging-utils.d.ts +0 -22
  200. package/dist/src/plugin/logging-utils.js +0 -91
  201. package/dist/src/plugin/project.d.ts +0 -32
  202. package/dist/src/plugin/project.js +0 -229
  203. package/dist/src/plugin/quota.d.ts +0 -34
  204. package/dist/src/plugin/quota.js +0 -261
  205. package/dist/src/plugin/recovery/constants.d.ts +0 -21
  206. package/dist/src/plugin/recovery/constants.js +0 -42
  207. package/dist/src/plugin/recovery/index.d.ts +0 -11
  208. package/dist/src/plugin/recovery/index.js +0 -11
  209. package/dist/src/plugin/recovery/storage.d.ts +0 -23
  210. package/dist/src/plugin/recovery/storage.js +0 -340
  211. package/dist/src/plugin/recovery/types.d.ts +0 -115
  212. package/dist/src/plugin/recovery/types.js +0 -6
  213. package/dist/src/plugin/recovery.d.ts +0 -60
  214. package/dist/src/plugin/recovery.js +0 -360
  215. package/dist/src/plugin/refresh-queue.d.ts +0 -99
  216. package/dist/src/plugin/refresh-queue.js +0 -235
  217. package/dist/src/plugin/request-helpers.d.ts +0 -281
  218. package/dist/src/plugin/request-helpers.js +0 -2200
  219. package/dist/src/plugin/request.d.ts +0 -110
  220. package/dist/src/plugin/request.js +0 -1489
  221. package/dist/src/plugin/rotation.d.ts +0 -182
  222. package/dist/src/plugin/rotation.js +0 -364
  223. package/dist/src/plugin/search.d.ts +0 -31
  224. package/dist/src/plugin/search.js +0 -185
  225. package/dist/src/plugin/server.d.ts +0 -22
  226. package/dist/src/plugin/server.js +0 -306
  227. package/dist/src/plugin/storage.d.ts +0 -136
  228. package/dist/src/plugin/storage.js +0 -599
  229. package/dist/src/plugin/stores/signature-store.d.ts +0 -4
  230. package/dist/src/plugin/stores/signature-store.js +0 -24
  231. package/dist/src/plugin/thinking-recovery.d.ts +0 -89
  232. package/dist/src/plugin/thinking-recovery.js +0 -289
  233. package/dist/src/plugin/token.d.ts +0 -18
  234. package/dist/src/plugin/token.js +0 -127
  235. package/dist/src/plugin/transform/claude.d.ts +0 -79
  236. package/dist/src/plugin/transform/claude.js +0 -256
  237. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts +0 -34
  238. package/dist/src/plugin/transform/cross-model-sanitizer.js +0 -224
  239. package/dist/src/plugin/transform/gemini.d.ts +0 -132
  240. package/dist/src/plugin/transform/gemini.js +0 -659
  241. package/dist/src/plugin/transform/index.d.ts +0 -14
  242. package/dist/src/plugin/transform/index.js +0 -9
  243. package/dist/src/plugin/transform/model-resolver.d.ts +0 -98
  244. package/dist/src/plugin/transform/model-resolver.js +0 -320
  245. package/dist/src/plugin/transform/types.d.ts +0 -110
  246. package/dist/src/plugin/transform/types.js +0 -1
  247. package/dist/src/plugin/types.d.ts +0 -95
  248. package/dist/src/plugin/types.js +0 -1
  249. package/dist/src/plugin/ui/ansi.d.ts +0 -31
  250. package/dist/src/plugin/ui/ansi.js +0 -45
  251. package/dist/src/plugin/ui/auth-menu.d.ts +0 -47
  252. package/dist/src/plugin/ui/auth-menu.js +0 -199
  253. package/dist/src/plugin/ui/confirm.d.ts +0 -1
  254. package/dist/src/plugin/ui/confirm.js +0 -14
  255. package/dist/src/plugin/ui/select.d.ts +0 -22
  256. package/dist/src/plugin/ui/select.js +0 -243
  257. package/dist/src/plugin/version.d.ts +0 -18
  258. package/dist/src/plugin/version.js +0 -79
@@ -1,2954 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { tool } from "@opencode-ai/plugin";
3
- import { ANTIGRAVITY_DEFAULT_PROJECT_ID, ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_ENDPOINT_PROD, ANTIGRAVITY_PROVIDER_ID, getAntigravityHeaders, } from "../src/constants";
4
- import { authorizeAntigravity, exchangeAntigravity } from "../src/antigravity/oauth";
5
- import { accessTokenExpired, isOAuthAuth, parseRefreshParts, formatRefreshParts } from "../src/plugin/auth";
6
- import { promptAddAnotherAccount, promptLoginMode, promptProjectId, showProxyMenu, promptProxyUrl } from "../src/plugin/cli";
7
- import { ensureProjectContext } from "../src/plugin/project";
8
- import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, initializeDebug, } from "../src/plugin/debug";
9
- import { buildThinkingWarmupBody, isGenerativeLanguageRequest, materializeGenerativeLanguageFetchInput, prepareAntigravityRequest, transformAntigravityResponse, } from "../src/plugin/request";
10
- import { resolveModelWithTier } from "../src/plugin/transform/model-resolver";
11
- import { isEmptyResponseBody, createSyntheticErrorResponse, } from "../src/plugin/request-helpers";
12
- import { AntigravityTokenRefreshError, refreshAccessToken } from "../src/plugin/token";
13
- import { startOAuthListener } from "../src/plugin/server";
14
- import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "../src/plugin/storage";
15
- import { AccountManager, parseRateLimitReason, calculateBackoffMs, computeSoftQuotaCacheTtlMs } from "../src/plugin/accounts";
16
- import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
17
- import { loadConfig, initRuntimeConfig } from "../src/plugin/config";
18
- import { createSessionRecoveryHook, getRecoverySuccessToast } from "../src/plugin/recovery";
19
- import { checkAccountsQuota } from "../src/plugin/quota";
20
- import { initDiskSignatureCache } from "../src/plugin/cache";
21
- import { createProactiveRefreshQueue } from "../src/plugin/refresh-queue";
22
- import { initLogger, createLogger } from "../src/plugin/logger";
23
- import { mergeAntigravityGoogleModelsIntoOpencodeConfig } from "../src/plugin/config/updater";
24
- import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker } from "../src/plugin/rotation";
25
- import { initAntigravityVersion } from "../src/plugin/version";
26
- import { executeSearch } from "../src/plugin/search";
27
- const DEFAULT_MODEL_RANKING = [
28
- "antigravity-claude-opus-4-6-thinking",
29
- "antigravity-gemini-3.1-pro",
30
- "antigravity-claude-sonnet-4-6",
31
- "antigravity-gemini-3-flash",
32
- ];
33
- function resolveModelByRanking(startIndex, ranking, accountManager) {
34
- for (let i = startIndex; i < ranking.length; i++) {
35
- const candidate = ranking[i];
36
- if (!candidate)
37
- continue;
38
- const candidateFamily = candidate.includes("claude") ? "claude" : "gemini";
39
- if (accountManager.getMinWaitTimeForFamily(candidateFamily, candidate) === 0) {
40
- return candidate;
41
- }
42
- }
43
- return ranking[startIndex] ?? "antigravity-claude-opus-4-6-thinking";
44
- }
45
- const MAX_OAUTH_ACCOUNTS = 10;
46
- const MAX_WARMUP_SESSIONS = 1000;
47
- const MAX_WARMUP_RETRIES = 2;
48
- const CAPACITY_BACKOFF_TIERS_MS = [5000, 10000, 20000, 30000, 60000];
49
- function getCapacityBackoffDelay(consecutiveFailures) {
50
- const index = Math.min(consecutiveFailures, CAPACITY_BACKOFF_TIERS_MS.length - 1);
51
- return CAPACITY_BACKOFF_TIERS_MS[Math.max(0, index)] ?? 5000;
52
- }
53
- const warmupAttemptedSessionIds = new Set();
54
- const warmupSucceededSessionIds = new Set();
55
- // Used to filter toasts based on toast_scope config
56
- let isChildSession = false;
57
- let childSessionParentID = undefined;
58
- const log = createLogger("plugin");
59
- const rateLimitToastCooldowns = new Map();
60
- const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
61
- const MAX_TOAST_COOLDOWN_ENTRIES = 100;
62
- let softQuotaToastShown = false;
63
- let rateLimitToastShown = false;
64
- let activeAccountManager = null;
65
- function cleanupToastCooldowns() {
66
- if (rateLimitToastCooldowns.size > MAX_TOAST_COOLDOWN_ENTRIES) {
67
- const now = Date.now();
68
- for (const [key, time] of rateLimitToastCooldowns) {
69
- if (now - time > RATE_LIMIT_TOAST_COOLDOWN_MS * 2) {
70
- rateLimitToastCooldowns.delete(key);
71
- }
72
- }
73
- }
74
- }
75
- function shouldShowRateLimitToast(message) {
76
- cleanupToastCooldowns();
77
- const toastKey = message.replace(/\d+/g, "X");
78
- const lastShown = rateLimitToastCooldowns.get(toastKey) ?? 0;
79
- const now = Date.now();
80
- if (now - lastShown < RATE_LIMIT_TOAST_COOLDOWN_MS) {
81
- return false;
82
- }
83
- rateLimitToastCooldowns.set(toastKey, now);
84
- return true;
85
- }
86
- function resetAllAccountsBlockedToasts() {
87
- softQuotaToastShown = false;
88
- rateLimitToastShown = false;
89
- }
90
- const quotaRefreshInProgressByEmail = new Set();
91
- async function triggerAsyncQuotaRefreshForAccount(accountManager, accountIndex, client, providerId, intervalMinutes) {
92
- if (intervalMinutes <= 0)
93
- return;
94
- const accounts = accountManager.getAccounts();
95
- const account = accounts[accountIndex];
96
- if (!account || account.enabled === false)
97
- return;
98
- const accountKey = account.email ?? `idx-${accountIndex}`;
99
- if (quotaRefreshInProgressByEmail.has(accountKey))
100
- return;
101
- const intervalMs = intervalMinutes * 60 * 1000;
102
- const age = account.cachedQuotaUpdatedAt != null
103
- ? Date.now() - account.cachedQuotaUpdatedAt
104
- : Infinity;
105
- if (age < intervalMs)
106
- return;
107
- quotaRefreshInProgressByEmail.add(accountKey);
108
- try {
109
- const accountsForCheck = accountManager.getAccountsForQuotaCheck();
110
- const singleAccount = accountsForCheck[accountIndex];
111
- if (!singleAccount) {
112
- quotaRefreshInProgressByEmail.delete(accountKey);
113
- return;
114
- }
115
- const results = await checkAccountsQuota([singleAccount], client, providerId);
116
- if (results[0]?.status === "ok" && results[0]?.quota?.groups) {
117
- accountManager.updateQuotaCache(accountIndex, results[0].quota.groups);
118
- accountManager.requestSaveToDisk();
119
- }
120
- }
121
- catch (err) {
122
- log.debug(`quota-refresh-failed email=${accountKey}`, { error: String(err) });
123
- }
124
- finally {
125
- quotaRefreshInProgressByEmail.delete(accountKey);
126
- }
127
- }
128
- function trackWarmupAttempt(sessionId) {
129
- if (warmupSucceededSessionIds.has(sessionId)) {
130
- return false;
131
- }
132
- if (warmupAttemptedSessionIds.size >= MAX_WARMUP_SESSIONS) {
133
- const first = warmupAttemptedSessionIds.values().next().value;
134
- if (first) {
135
- warmupAttemptedSessionIds.delete(first);
136
- warmupSucceededSessionIds.delete(first);
137
- }
138
- }
139
- const attempts = getWarmupAttemptCount(sessionId);
140
- if (attempts >= MAX_WARMUP_RETRIES) {
141
- return false;
142
- }
143
- warmupAttemptedSessionIds.add(sessionId);
144
- return true;
145
- }
146
- function getWarmupAttemptCount(sessionId) {
147
- return warmupAttemptedSessionIds.has(sessionId) ? 1 : 0;
148
- }
149
- function markWarmupSuccess(sessionId) {
150
- warmupSucceededSessionIds.add(sessionId);
151
- if (warmupSucceededSessionIds.size >= MAX_WARMUP_SESSIONS) {
152
- const first = warmupSucceededSessionIds.values().next().value;
153
- if (first)
154
- warmupSucceededSessionIds.delete(first);
155
- }
156
- }
157
- function clearWarmupAttempt(sessionId) {
158
- warmupAttemptedSessionIds.delete(sessionId);
159
- }
160
- function isWSL() {
161
- if (process.platform !== "linux")
162
- return false;
163
- try {
164
- const { readFileSync } = require("node:fs");
165
- const release = readFileSync("/proc/version", "utf8").toLowerCase();
166
- return release.includes("microsoft") || release.includes("wsl");
167
- }
168
- catch {
169
- return false;
170
- }
171
- }
172
- function isWSL2() {
173
- if (!isWSL())
174
- return false;
175
- try {
176
- const { readFileSync } = require("node:fs");
177
- const version = readFileSync("/proc/version", "utf8").toLowerCase();
178
- return version.includes("wsl2") || version.includes("microsoft-standard");
179
- }
180
- catch {
181
- return false;
182
- }
183
- }
184
- function isRemoteEnvironment() {
185
- if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
186
- return true;
187
- }
188
- if (process.env.REMOTE_CONTAINERS || process.env.CODESPACES) {
189
- return true;
190
- }
191
- if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY && !isWSL()) {
192
- return true;
193
- }
194
- return false;
195
- }
196
- function shouldSkipLocalServer() {
197
- return isWSL2() || isRemoteEnvironment();
198
- }
199
- async function openBrowser(url) {
200
- try {
201
- if (process.platform === "darwin") {
202
- exec(`open "${url}"`);
203
- return true;
204
- }
205
- if (process.platform === "win32") {
206
- exec(`start "" "${url}"`);
207
- return true;
208
- }
209
- if (isWSL()) {
210
- try {
211
- exec(`wslview "${url}"`);
212
- return true;
213
- }
214
- catch { }
215
- }
216
- if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
217
- return false;
218
- }
219
- exec(`xdg-open "${url}"`);
220
- return true;
221
- }
222
- catch {
223
- return false;
224
- }
225
- }
226
- function decodeEscapedText(input) {
227
- return input
228
- .replace(/&amp;/g, "&")
229
- .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
230
- }
231
- function normalizeGoogleVerificationUrl(rawUrl) {
232
- const normalized = decodeEscapedText(rawUrl).trim();
233
- if (!normalized) {
234
- return undefined;
235
- }
236
- try {
237
- const parsed = new URL(normalized);
238
- if (parsed.hostname !== "accounts.google.com") {
239
- return undefined;
240
- }
241
- return parsed.toString();
242
- }
243
- catch {
244
- return undefined;
245
- }
246
- }
247
- function selectBestVerificationUrl(urls) {
248
- const unique = Array.from(new Set(urls.map((url) => normalizeGoogleVerificationUrl(url)).filter(Boolean)));
249
- if (unique.length === 0) {
250
- return undefined;
251
- }
252
- unique.sort((a, b) => {
253
- const score = (value) => {
254
- let total = 0;
255
- if (value.includes("plt="))
256
- total += 4;
257
- if (value.includes("/signin/continue"))
258
- total += 3;
259
- if (value.includes("continue="))
260
- total += 2;
261
- if (value.includes("service=cloudcode"))
262
- total += 1;
263
- return total;
264
- };
265
- return score(b) - score(a);
266
- });
267
- return unique[0];
268
- }
269
- function extractVerificationErrorDetails(bodyText) {
270
- const decodedBody = decodeEscapedText(bodyText);
271
- const lowerBody = decodedBody.toLowerCase();
272
- let validationRequired = lowerBody.includes("validation_required");
273
- let message;
274
- const verificationUrls = new Set();
275
- const collectUrlsFromText = (text) => {
276
- for (const match of text.matchAll(/https:\/\/accounts\.google\.com\/[^\s"'<>]+/gi)) {
277
- if (match[0]) {
278
- verificationUrls.add(match[0]);
279
- }
280
- }
281
- };
282
- collectUrlsFromText(decodedBody);
283
- const payloads = [];
284
- const trimmed = decodedBody.trim();
285
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
286
- try {
287
- payloads.push(JSON.parse(trimmed));
288
- }
289
- catch {
290
- }
291
- }
292
- for (const rawLine of decodedBody.split("\n")) {
293
- const line = rawLine.trim();
294
- if (!line.startsWith("data:")) {
295
- continue;
296
- }
297
- const payloadText = line.slice(5).trim();
298
- if (!payloadText || payloadText === "[DONE]") {
299
- continue;
300
- }
301
- try {
302
- payloads.push(JSON.parse(payloadText));
303
- }
304
- catch {
305
- collectUrlsFromText(payloadText);
306
- }
307
- }
308
- const visited = new Set();
309
- const walk = (value, key) => {
310
- if (typeof value === "string") {
311
- const normalizedValue = decodeEscapedText(value);
312
- const lowerValue = normalizedValue.toLowerCase();
313
- const lowerKey = key?.toLowerCase() ?? "";
314
- if (lowerValue.includes("validation_required")) {
315
- validationRequired = true;
316
- }
317
- if (!message &&
318
- (lowerKey.includes("message") || lowerKey.includes("detail") || lowerKey.includes("description"))) {
319
- message = normalizedValue;
320
- }
321
- if (lowerKey.includes("validation_url") ||
322
- lowerKey.includes("verify_url") ||
323
- lowerKey.includes("verification_url") ||
324
- lowerKey === "url") {
325
- verificationUrls.add(normalizedValue);
326
- }
327
- collectUrlsFromText(normalizedValue);
328
- return;
329
- }
330
- if (!value || typeof value !== "object" || visited.has(value)) {
331
- return;
332
- }
333
- visited.add(value);
334
- if (Array.isArray(value)) {
335
- for (const item of value) {
336
- walk(item);
337
- }
338
- return;
339
- }
340
- for (const [childKey, childValue] of Object.entries(value)) {
341
- walk(childValue, childKey);
342
- }
343
- };
344
- for (const payload of payloads) {
345
- walk(payload);
346
- }
347
- if (!validationRequired) {
348
- validationRequired =
349
- lowerBody.includes("verification required") ||
350
- lowerBody.includes("verify your account") ||
351
- lowerBody.includes("account verification");
352
- }
353
- if (!message) {
354
- const fallback = decodedBody
355
- .split("\n")
356
- .map((line) => line.trim())
357
- .find((line) => line && !line.startsWith("data:") && /(verify|validation|required)/i.test(line));
358
- if (fallback) {
359
- message = fallback;
360
- }
361
- }
362
- return {
363
- validationRequired,
364
- message,
365
- verifyUrl: selectBestVerificationUrl([...verificationUrls]),
366
- };
367
- }
368
- async function verifyAccountAccess(account, client, providerId) {
369
- const parsed = parseRefreshParts(account.refreshToken);
370
- if (!parsed.refreshToken) {
371
- return { status: "error", message: "Missing refresh token for selected account." };
372
- }
373
- const auth = {
374
- type: "oauth",
375
- refresh: formatRefreshParts({
376
- refreshToken: parsed.refreshToken,
377
- projectId: parsed.projectId ?? account.projectId,
378
- managedProjectId: parsed.managedProjectId ?? account.managedProjectId,
379
- }),
380
- access: "",
381
- expires: 0,
382
- };
383
- let refreshedAuth;
384
- try {
385
- refreshedAuth = await refreshAccessToken(auth, client, providerId);
386
- }
387
- catch (error) {
388
- if (error instanceof AntigravityTokenRefreshError) {
389
- return { status: "error", message: error.message };
390
- }
391
- return { status: "error", message: `Token refresh failed: ${String(error)}` };
392
- }
393
- if (!refreshedAuth?.access) {
394
- return { status: "error", message: "Could not refresh access token for this account." };
395
- }
396
- const projectId = parsed.managedProjectId ??
397
- parsed.projectId ??
398
- account.managedProjectId ??
399
- account.projectId ??
400
- ANTIGRAVITY_DEFAULT_PROJECT_ID;
401
- const headers = {
402
- ...getAntigravityHeaders(),
403
- Authorization: `Bearer ${refreshedAuth.access}`,
404
- "Content-Type": "application/json",
405
- };
406
- if (projectId) {
407
- headers["x-goog-user-project"] = projectId;
408
- }
409
- const requestBody = {
410
- model: "gemini-3-flash",
411
- request: {
412
- model: "gemini-3-flash",
413
- contents: [{ role: "user", parts: [{ text: "ping" }] }],
414
- generationConfig: { maxOutputTokens: 1, temperature: 0 },
415
- },
416
- };
417
- const controller = new AbortController();
418
- const timeoutId = setTimeout(() => controller.abort(), 20000);
419
- let response;
420
- try {
421
- response = await fetch(`${ANTIGRAVITY_ENDPOINT_PROD}/v1internal:streamGenerateContent?alt=sse`, {
422
- method: "POST",
423
- headers,
424
- body: JSON.stringify(requestBody),
425
- signal: controller.signal,
426
- });
427
- }
428
- catch (error) {
429
- if (error instanceof Error && error.name === "AbortError") {
430
- return { status: "error", message: "Verification check timed out." };
431
- }
432
- return { status: "error", message: `Verification check failed: ${String(error)}` };
433
- }
434
- finally {
435
- clearTimeout(timeoutId);
436
- }
437
- let responseBody = "";
438
- try {
439
- responseBody = await response.text();
440
- }
441
- catch {
442
- responseBody = "";
443
- }
444
- if (response.ok) {
445
- return { status: "ok", message: "Account verification check passed." };
446
- }
447
- const extracted = extractVerificationErrorDetails(responseBody);
448
- if (response.status === 403 && extracted.validationRequired) {
449
- return {
450
- status: "blocked",
451
- message: extracted.message ?? "Google requires additional account verification.",
452
- verifyUrl: extracted.verifyUrl,
453
- };
454
- }
455
- const fallbackMessage = extracted.message ?? `Request failed (${response.status} ${response.statusText}).`;
456
- return {
457
- status: "error",
458
- message: fallbackMessage,
459
- };
460
- }
461
- async function promptAccountIndexForVerification(accounts) {
462
- const { createInterface } = await import("node:readline/promises");
463
- const { stdin, stdout } = await import("node:process");
464
- const rl = createInterface({ input: stdin, output: stdout });
465
- try {
466
- console.log("\nSelect an account to verify:");
467
- for (const account of accounts) {
468
- const label = account.email || `Account ${account.index + 1}`;
469
- console.log(` ${account.index + 1}. ${label}`);
470
- }
471
- console.log("");
472
- while (true) {
473
- const answer = (await rl.question("Account number (leave blank to cancel): ")).trim();
474
- if (!answer) {
475
- return undefined;
476
- }
477
- const parsedIndex = Number(answer);
478
- if (!Number.isInteger(parsedIndex)) {
479
- console.log("Please enter a valid account number.");
480
- continue;
481
- }
482
- const normalizedIndex = parsedIndex - 1;
483
- const selected = accounts.find((account) => account.index === normalizedIndex);
484
- if (!selected) {
485
- console.log("Please enter a number from the list above.");
486
- continue;
487
- }
488
- return selected.index;
489
- }
490
- }
491
- finally {
492
- rl.close();
493
- }
494
- }
495
- async function promptOpenVerificationUrl() {
496
- const answer = (await promptOAuthCallbackValue("Open verification URL in your browser now? [Y/n]: ")).trim().toLowerCase();
497
- return answer === "" || answer === "y" || answer === "yes";
498
- }
499
- function markStoredAccountVerificationRequired(account, reason, verifyUrl) {
500
- let changed = false;
501
- const wasVerificationRequired = account.verificationRequired === true;
502
- if (!wasVerificationRequired) {
503
- account.verificationRequired = true;
504
- changed = true;
505
- }
506
- if (!wasVerificationRequired || account.verificationRequiredAt === undefined) {
507
- account.verificationRequiredAt = Date.now();
508
- changed = true;
509
- }
510
- const normalizedReason = reason.trim();
511
- if (account.verificationRequiredReason !== normalizedReason) {
512
- account.verificationRequiredReason = normalizedReason;
513
- changed = true;
514
- }
515
- const normalizedUrl = verifyUrl?.trim();
516
- if (normalizedUrl && account.verificationUrl !== normalizedUrl) {
517
- account.verificationUrl = normalizedUrl;
518
- changed = true;
519
- }
520
- if (account.enabled !== false) {
521
- account.enabled = false;
522
- changed = true;
523
- }
524
- return changed;
525
- }
526
- function clearStoredAccountVerificationRequired(account, enableIfRequired = false) {
527
- const wasVerificationRequired = account.verificationRequired === true;
528
- let changed = false;
529
- if (account.verificationRequired !== false) {
530
- account.verificationRequired = false;
531
- changed = true;
532
- }
533
- if (account.verificationRequiredAt !== undefined) {
534
- account.verificationRequiredAt = undefined;
535
- changed = true;
536
- }
537
- if (account.verificationRequiredReason !== undefined) {
538
- account.verificationRequiredReason = undefined;
539
- changed = true;
540
- }
541
- if (account.verificationUrl !== undefined) {
542
- account.verificationUrl = undefined;
543
- changed = true;
544
- }
545
- if (enableIfRequired && wasVerificationRequired && account.enabled === false) {
546
- account.enabled = true;
547
- changed = true;
548
- }
549
- return { changed, wasVerificationRequired };
550
- }
551
- async function promptOAuthCallbackValue(message) {
552
- const { createInterface } = await import("node:readline/promises");
553
- const { stdin, stdout } = await import("node:process");
554
- const rl = createInterface({ input: stdin, output: stdout });
555
- try {
556
- return (await rl.question(message)).trim();
557
- }
558
- finally {
559
- rl.close();
560
- }
561
- }
562
- function getStateFromAuthorizationUrl(authorizationUrl) {
563
- try {
564
- return new URL(authorizationUrl).searchParams.get("state") ?? "";
565
- }
566
- catch {
567
- return "";
568
- }
569
- }
570
- function extractOAuthCallbackParams(url) {
571
- const code = url.searchParams.get("code");
572
- const state = url.searchParams.get("state");
573
- if (!code || !state) {
574
- return null;
575
- }
576
- return { code, state };
577
- }
578
- function parseOAuthCallbackInput(value, fallbackState) {
579
- const trimmed = value.trim();
580
- if (!trimmed) {
581
- return { error: "Missing authorization code" };
582
- }
583
- try {
584
- const url = new URL(trimmed);
585
- const code = url.searchParams.get("code");
586
- const state = url.searchParams.get("state") ?? fallbackState;
587
- if (!code) {
588
- return { error: "Missing code in callback URL" };
589
- }
590
- if (!state) {
591
- return { error: "Missing state in callback URL" };
592
- }
593
- return { code, state };
594
- }
595
- catch {
596
- if (!fallbackState) {
597
- return { error: "Missing state. Paste the full redirect URL instead of only the code." };
598
- }
599
- return { code: trimmed, state: fallbackState };
600
- }
601
- }
602
- async function promptManualOAuthInput(fallbackState) {
603
- console.log("1. Open the URL above in your browser and complete Google sign-in.");
604
- console.log("2. After approving, copy the full redirected localhost URL from the address bar.");
605
- console.log("3. Paste it back here.\n");
606
- const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
607
- const params = parseOAuthCallbackInput(callbackInput, fallbackState);
608
- if ("error" in params) {
609
- return { type: "failed", error: params.error };
610
- }
611
- return exchangeAntigravity(params.code, params.state);
612
- }
613
- function clampInt(value, min, max) {
614
- if (!Number.isFinite(value)) {
615
- return min;
616
- }
617
- return Math.min(max, Math.max(min, Math.floor(value)));
618
- }
619
- async function persistAccountPool(results, replaceAll = false) {
620
- if (results.length === 0) {
621
- return;
622
- }
623
- const now = Date.now();
624
- const stored = replaceAll ? null : await loadAccounts();
625
- const accounts = stored?.accounts ? [...stored.accounts] : [];
626
- const indexByRefreshToken = new Map();
627
- const indexByEmail = new Map();
628
- for (let i = 0; i < accounts.length; i++) {
629
- const acc = accounts[i];
630
- if (acc?.refreshToken) {
631
- indexByRefreshToken.set(acc.refreshToken, i);
632
- }
633
- if (acc?.email) {
634
- indexByEmail.set(acc.email, i);
635
- }
636
- }
637
- for (const result of results) {
638
- const parts = parseRefreshParts(result.refresh);
639
- if (!parts.refreshToken) {
640
- continue;
641
- }
642
- const existingByEmail = result.email ? indexByEmail.get(result.email) : undefined;
643
- const existingByToken = indexByRefreshToken.get(parts.refreshToken);
644
- const existingIndex = existingByEmail ?? existingByToken;
645
- if (existingIndex === undefined) {
646
- const newIndex = accounts.length;
647
- indexByRefreshToken.set(parts.refreshToken, newIndex);
648
- if (result.email) {
649
- indexByEmail.set(result.email, newIndex);
650
- }
651
- accounts.push({
652
- email: result.email,
653
- refreshToken: parts.refreshToken,
654
- projectId: parts.projectId,
655
- managedProjectId: parts.managedProjectId,
656
- addedAt: now,
657
- lastUsed: now,
658
- enabled: true,
659
- });
660
- continue;
661
- }
662
- const existing = accounts[existingIndex];
663
- if (!existing) {
664
- continue;
665
- }
666
- const oldToken = existing.refreshToken;
667
- accounts[existingIndex] = {
668
- ...existing,
669
- email: result.email ?? existing.email,
670
- refreshToken: parts.refreshToken,
671
- projectId: parts.projectId ?? existing.projectId,
672
- managedProjectId: parts.managedProjectId ?? existing.managedProjectId,
673
- lastUsed: now,
674
- };
675
- if (oldToken !== parts.refreshToken) {
676
- indexByRefreshToken.delete(oldToken);
677
- indexByRefreshToken.set(parts.refreshToken, existingIndex);
678
- }
679
- }
680
- if (accounts.length === 0) {
681
- return;
682
- }
683
- const activeIndex = replaceAll
684
- ? 0
685
- : (typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex) ? stored.activeIndex : 0);
686
- await saveAccounts({
687
- version: 4,
688
- accounts,
689
- activeIndex: clampInt(activeIndex, 0, accounts.length - 1),
690
- activeIndexByFamily: {
691
- claude: clampInt(activeIndex, 0, accounts.length - 1),
692
- gemini: clampInt(activeIndex, 0, accounts.length - 1),
693
- },
694
- });
695
- }
696
- function buildAuthSuccessFromStoredAccount(account) {
697
- const refresh = formatRefreshParts({
698
- refreshToken: account.refreshToken,
699
- projectId: account.projectId,
700
- managedProjectId: account.managedProjectId,
701
- });
702
- return {
703
- type: "success",
704
- refresh,
705
- access: "",
706
- expires: 0,
707
- email: account.email,
708
- projectId: account.projectId ?? "",
709
- };
710
- }
711
- function retryAfterMsFromResponse(response, defaultRetryMs = 60_000) {
712
- const retryAfterMsHeader = response.headers.get("retry-after-ms");
713
- if (retryAfterMsHeader) {
714
- const parsed = Number.parseInt(retryAfterMsHeader, 10);
715
- if (!Number.isNaN(parsed) && parsed > 0) {
716
- return parsed;
717
- }
718
- }
719
- const retryAfterHeader = response.headers.get("retry-after");
720
- if (retryAfterHeader) {
721
- const parsed = Number.parseInt(retryAfterHeader, 10);
722
- if (!Number.isNaN(parsed) && parsed > 0) {
723
- return parsed * 1000;
724
- }
725
- }
726
- return defaultRetryMs;
727
- }
728
- /**
729
- * Parse Go-style duration strings to milliseconds.
730
- * Supports compound durations: "1h16m0.667s", "1.5s", "200ms", "5m30s"
731
- *
732
- * @param duration - Duration string in Go format
733
- * @returns Duration in milliseconds, or null if parsing fails
734
- */
735
- function parseDurationToMs(duration) {
736
- const simpleMatch = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i);
737
- if (simpleMatch) {
738
- const value = parseFloat(simpleMatch[1]);
739
- const unit = (simpleMatch[2] || "s").toLowerCase();
740
- switch (unit) {
741
- case "h": return value * 3600 * 1000;
742
- case "m": return value * 60 * 1000;
743
- case "s": return value * 1000;
744
- case "ms": return value;
745
- default: return value * 1000;
746
- }
747
- }
748
- const compoundRegex = /(\d+(?:\.\d+)?)(h|m(?!s)|s|ms)/gi;
749
- let totalMs = 0;
750
- let matchFound = false;
751
- let match;
752
- while ((match = compoundRegex.exec(duration)) !== null) {
753
- matchFound = true;
754
- const value = parseFloat(match[1]);
755
- const unit = match[2].toLowerCase();
756
- switch (unit) {
757
- case "h":
758
- totalMs += value * 3600 * 1000;
759
- break;
760
- case "m":
761
- totalMs += value * 60 * 1000;
762
- break;
763
- case "s":
764
- totalMs += value * 1000;
765
- break;
766
- case "ms":
767
- totalMs += value;
768
- break;
769
- }
770
- }
771
- return matchFound ? totalMs : null;
772
- }
773
- function extractRateLimitBodyInfo(body) {
774
- if (!body || typeof body !== "object") {
775
- return { retryDelayMs: null };
776
- }
777
- const error = body.error;
778
- const message = error && typeof error === "object"
779
- ? error.message
780
- : undefined;
781
- const details = error && typeof error === "object"
782
- ? error.details
783
- : undefined;
784
- let reason;
785
- if (Array.isArray(details)) {
786
- for (const detail of details) {
787
- if (!detail || typeof detail !== "object")
788
- continue;
789
- const type = detail["@type"];
790
- if (typeof type === "string" && type.includes("google.rpc.ErrorInfo")) {
791
- const detailReason = detail.reason;
792
- if (typeof detailReason === "string") {
793
- reason = detailReason;
794
- break;
795
- }
796
- }
797
- }
798
- for (const detail of details) {
799
- if (!detail || typeof detail !== "object")
800
- continue;
801
- const type = detail["@type"];
802
- if (typeof type === "string" && type.includes("google.rpc.RetryInfo")) {
803
- const retryDelay = detail.retryDelay;
804
- if (typeof retryDelay === "string") {
805
- const retryDelayMs = parseDurationToMs(retryDelay);
806
- if (retryDelayMs !== null) {
807
- return { retryDelayMs, message, reason };
808
- }
809
- }
810
- }
811
- }
812
- for (const detail of details) {
813
- if (!detail || typeof detail !== "object")
814
- continue;
815
- const metadata = detail.metadata;
816
- if (metadata && typeof metadata === "object") {
817
- const quotaResetDelay = metadata.quotaResetDelay;
818
- const quotaResetTime = metadata.quotaResetTimeStamp;
819
- if (typeof quotaResetDelay === "string") {
820
- const quotaResetDelayMs = parseDurationToMs(quotaResetDelay);
821
- if (quotaResetDelayMs !== null) {
822
- return { retryDelayMs: quotaResetDelayMs, message, quotaResetTime, reason };
823
- }
824
- }
825
- }
826
- }
827
- }
828
- if (message) {
829
- const afterMatch = message.match(/reset after\s+([0-9hms.]+)/i);
830
- const rawDuration = afterMatch?.[1];
831
- if (rawDuration) {
832
- const parsed = parseDurationToMs(rawDuration);
833
- if (parsed !== null) {
834
- return { retryDelayMs: parsed, message, reason };
835
- }
836
- }
837
- }
838
- return { retryDelayMs: null, message, reason };
839
- }
840
- async function extractRetryInfoFromBody(response) {
841
- try {
842
- const text = await response.clone().text();
843
- try {
844
- const parsed = JSON.parse(text);
845
- return extractRateLimitBodyInfo(parsed);
846
- }
847
- catch {
848
- return { retryDelayMs: null };
849
- }
850
- }
851
- catch {
852
- return { retryDelayMs: null };
853
- }
854
- }
855
- function formatWaitTime(ms) {
856
- if (ms < 1000)
857
- return `${ms}ms`;
858
- const seconds = Math.ceil(ms / 1000);
859
- if (seconds < 60)
860
- return `${seconds}s`;
861
- const minutes = Math.floor(seconds / 60);
862
- const remainingSeconds = seconds % 60;
863
- if (minutes < 60) {
864
- return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
865
- }
866
- const hours = Math.floor(minutes / 60);
867
- const remainingMinutes = minutes % 60;
868
- return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
869
- }
870
- const FIRST_RETRY_DELAY_MS = 1000; // 1s - first 429 quick retry on same account
871
- const SWITCH_ACCOUNT_DELAY_MS = 5000; // 5s - delay before switching to another account
872
- /**
873
- * Rate limit state tracking with time-window deduplication.
874
- *
875
- * Problem: When multiple subagents hit 429 simultaneously, each would increment
876
- * the consecutive counter, causing incorrect exponential backoff (5 concurrent
877
- * 429s = 2^5 backoff instead of 2^1).
878
- *
879
- * Solution: Track per account+quota with deduplication window. Multiple 429s
880
- * within RATE_LIMIT_DEDUP_WINDOW_MS are treated as a single event.
881
- */
882
- const RATE_LIMIT_DEDUP_WINDOW_MS = 2000; // 2 seconds - concurrent requests within this window are deduplicated
883
- const RATE_LIMIT_STATE_RESET_MS = 120_000; // Reset consecutive counter after 2 minutes of no 429s
884
- // Key format: `${accountIndex}:${quotaKey}` for per-account-per-quota tracking
885
- const rateLimitStateByAccountQuota = new Map();
886
- const emptyResponseAttempts = new Map();
887
- /**
888
- * Get rate limit backoff with time-window deduplication.
889
- *
890
- * @param accountIndex - The account index
891
- * @param quotaKey - The quota key (e.g., "gemini-cli", "gemini-antigravity", "claude")
892
- * @param serverRetryAfterMs - Server-provided retry delay (if any)
893
- * @param maxBackoffMs - Maximum backoff delay in milliseconds (default 60000)
894
- * @returns { attempt, delayMs, isDuplicate } - isDuplicate=true if within dedup window
895
- */
896
- function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs, maxBackoffMs = 60_000) {
897
- const now = Date.now();
898
- const stateKey = `${accountIndex}:${quotaKey}`;
899
- const previous = rateLimitStateByAccountQuota.get(stateKey);
900
- if (previous && (now - previous.lastAt < RATE_LIMIT_DEDUP_WINDOW_MS)) {
901
- const baseDelay = serverRetryAfterMs ?? 1000;
902
- const backoffDelay = Math.min(baseDelay * Math.pow(2, previous.consecutive429 - 1), maxBackoffMs);
903
- return {
904
- attempt: previous.consecutive429,
905
- delayMs: Math.max(baseDelay, backoffDelay),
906
- isDuplicate: true
907
- };
908
- }
909
- const attempt = previous && (now - previous.lastAt < RATE_LIMIT_STATE_RESET_MS)
910
- ? previous.consecutive429 + 1
911
- : 1;
912
- rateLimitStateByAccountQuota.set(stateKey, {
913
- consecutive429: attempt,
914
- lastAt: now,
915
- quotaKey
916
- });
917
- const baseDelay = serverRetryAfterMs ?? 1000;
918
- const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxBackoffMs);
919
- return { attempt, delayMs: Math.max(baseDelay, backoffDelay), isDuplicate: false };
920
- }
921
- /**
922
- * Reset rate limit state for an account+quota combination.
923
- * Only resets the specific quota, not all quotas for the account.
924
- */
925
- function resetRateLimitState(accountIndex, quotaKey) {
926
- const stateKey = `${accountIndex}:${quotaKey}`;
927
- rateLimitStateByAccountQuota.delete(stateKey);
928
- }
929
- /**
930
- * Reset all rate limit state for an account (all quotas).
931
- * Used when account is completely healthy.
932
- */
933
- function resetAllRateLimitStateForAccount(accountIndex) {
934
- for (const key of rateLimitStateByAccountQuota.keys()) {
935
- if (key.startsWith(`${accountIndex}:`)) {
936
- rateLimitStateByAccountQuota.delete(key);
937
- }
938
- }
939
- }
940
- function headerStyleToQuotaKey(headerStyle, family) {
941
- if (family === "claude")
942
- return "claude";
943
- return headerStyle === "antigravity" ? "gemini-antigravity" : "gemini-cli";
944
- }
945
- const accountFailureState = new Map();
946
- const MAX_CONSECUTIVE_FAILURES = 5;
947
- const FAILURE_COOLDOWN_MS = 30_000; // 30 seconds cooldown after max failures
948
- const FAILURE_STATE_RESET_MS = 120_000; // Reset failure count after 2 minutes of no failures
949
- function trackAccountFailure(accountIndex) {
950
- const now = Date.now();
951
- const previous = accountFailureState.get(accountIndex);
952
- const failures = previous && (now - previous.lastFailureAt < FAILURE_STATE_RESET_MS)
953
- ? previous.consecutiveFailures + 1
954
- : 1;
955
- accountFailureState.set(accountIndex, { consecutiveFailures: failures, lastFailureAt: now });
956
- const shouldCooldown = failures >= MAX_CONSECUTIVE_FAILURES;
957
- const cooldownMs = shouldCooldown ? FAILURE_COOLDOWN_MS : 0;
958
- return { failures, shouldCooldown, cooldownMs };
959
- }
960
- function resetAccountFailureState(accountIndex) {
961
- accountFailureState.delete(accountIndex);
962
- }
963
- /**
964
- * Sleep for a given number of milliseconds, respecting an abort signal.
965
- */
966
- function sleep(ms, signal) {
967
- return new Promise((resolve, reject) => {
968
- if (signal?.aborted) {
969
- reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
970
- return;
971
- }
972
- const timeout = setTimeout(() => {
973
- cleanup();
974
- resolve();
975
- }, ms);
976
- const onAbort = () => {
977
- cleanup();
978
- reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
979
- };
980
- const cleanup = () => {
981
- clearTimeout(timeout);
982
- signal?.removeEventListener("abort", onAbort);
983
- };
984
- signal?.addEventListener("abort", onAbort, { once: true });
985
- });
986
- }
987
- /**
988
- * Creates an Antigravity OAuth plugin for a specific provider ID.
989
- */
990
- export const createAntigravityPlugin = (providerId) => async ({ client, directory }) => {
991
- mergeAntigravityGoogleModelsIntoOpencodeConfig().catch((error) => {
992
- log.debug("model-config-autosync-failed", { error: String(error) });
993
- });
994
- const config = loadConfig(directory);
995
- initRuntimeConfig(config);
996
- let cachedGetAuth = null;
997
- initializeDebug(config);
998
- initLogger(client);
999
- await initAntigravityVersion();
1000
- if (config.health_score) {
1001
- initHealthTracker({
1002
- initial: config.health_score.initial,
1003
- successReward: config.health_score.success_reward,
1004
- rateLimitPenalty: config.health_score.rate_limit_penalty,
1005
- failurePenalty: config.health_score.failure_penalty,
1006
- recoveryRatePerHour: config.health_score.recovery_rate_per_hour,
1007
- minUsable: config.health_score.min_usable,
1008
- maxScore: config.health_score.max_score,
1009
- });
1010
- }
1011
- if (config.token_bucket) {
1012
- initTokenTracker({
1013
- maxTokens: config.token_bucket.max_tokens,
1014
- regenerationRatePerMinute: config.token_bucket.regeneration_rate_per_minute,
1015
- initialTokens: config.token_bucket.initial_tokens,
1016
- });
1017
- }
1018
- // Initialize disk signature cache if keep_thinking is enabled
1019
- if (config.keep_thinking) {
1020
- initDiskSignatureCache(config.signature_cache);
1021
- }
1022
- const sessionRecovery = createSessionRecoveryHook({ client, directory }, config);
1023
- const updateChecker = createAutoUpdateCheckerHook(client, directory, {
1024
- showStartupToast: true,
1025
- autoUpdate: config.auto_update,
1026
- });
1027
- const eventHandler = async (input) => {
1028
- await updateChecker.event(input);
1029
- // This is used to filter toasts based on toast_scope config
1030
- if (input.event.type === "session.created") {
1031
- const props = input.event.properties;
1032
- if (props?.info?.parentID) {
1033
- isChildSession = true;
1034
- childSessionParentID = props.info.parentID;
1035
- log.debug("child-session-detected", { parentID: props.info.parentID });
1036
- }
1037
- else {
1038
- isChildSession = false;
1039
- childSessionParentID = undefined;
1040
- log.debug("root-session-detected", {});
1041
- }
1042
- }
1043
- if (sessionRecovery && input.event.type === "session.error") {
1044
- const props = input.event.properties;
1045
- const sessionID = props?.sessionID;
1046
- const messageID = props?.messageID;
1047
- const error = props?.error;
1048
- if (sessionRecovery.isRecoverableError(error)) {
1049
- const messageInfo = {
1050
- id: messageID,
1051
- role: "assistant",
1052
- sessionID,
1053
- error,
1054
- };
1055
- // handleSessionRecovery now does the actual fix (injects tool_result, etc.)
1056
- const recovered = await sessionRecovery.handleSessionRecovery(messageInfo);
1057
- // Only send "continue" AFTER successful tool_result_missing recovery
1058
- // (thinking recoveries already resume inside handleSessionRecovery)
1059
- if (recovered && sessionID && config.auto_resume) {
1060
- // For tool_result_missing, we need to send continue after injecting tool_results
1061
- await client.session.prompt({
1062
- path: { id: sessionID },
1063
- body: { parts: [{ type: "text", text: config.resume_text }] },
1064
- query: { directory },
1065
- }).catch(() => { });
1066
- // Show success toast (respects toast_scope for child sessions)
1067
- const successToast = getRecoverySuccessToast();
1068
- log.debug("recovery-toast", { ...successToast, isChildSession, toastScope: config.toast_scope });
1069
- if (!(config.toast_scope === "root_only" && isChildSession)) {
1070
- await client.tui.showToast({
1071
- body: {
1072
- title: successToast.title,
1073
- message: successToast.message,
1074
- variant: "success",
1075
- },
1076
- }).catch(() => { });
1077
- }
1078
- }
1079
- }
1080
- }
1081
- };
1082
- // Create google_search tool with access to auth context
1083
- const googleSearchTool = tool({
1084
- 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.",
1085
- args: {
1086
- query: tool.schema.string().describe("The search query or question to answer using web search"),
1087
- 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."),
1088
- thinking: tool.schema.boolean().optional().default(true).describe("Enable deep thinking for more thorough analysis (default: true)"),
1089
- },
1090
- async execute(args, ctx) {
1091
- log.debug("Google Search tool called", { query: args.query, urlCount: args.urls?.length ?? 0 });
1092
- const auth = cachedGetAuth ? await cachedGetAuth() : null;
1093
- if (!auth || !isOAuthAuth(auth)) {
1094
- return "Error: Not authenticated with Antigravity. Please run `opencode auth login` to authenticate.";
1095
- }
1096
- const parts = parseRefreshParts(auth.refresh);
1097
- const projectId = parts.managedProjectId || parts.projectId || "unknown";
1098
- let accessToken = auth.access;
1099
- if (!accessToken || accessTokenExpired(auth)) {
1100
- try {
1101
- const refreshed = await refreshAccessToken(auth, client, providerId);
1102
- accessToken = refreshed?.access;
1103
- }
1104
- catch (error) {
1105
- return `Error: Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`;
1106
- }
1107
- }
1108
- if (!accessToken) {
1109
- return "Error: No valid access token available. Please run `opencode auth login` to re-authenticate.";
1110
- }
1111
- return executeSearch({
1112
- query: args.query,
1113
- urls: args.urls,
1114
- thinking: args.thinking,
1115
- }, accessToken, projectId, ctx.abort);
1116
- },
1117
- });
1118
- return {
1119
- event: eventHandler,
1120
- tool: {
1121
- google_search: googleSearchTool,
1122
- },
1123
- auth: {
1124
- provider: providerId,
1125
- loader: async (getAuth, provider) => {
1126
- cachedGetAuth = getAuth;
1127
- // Check initial auth — but do NOT bail with return {} if missing.
1128
- const initialAuth = await getAuth();
1129
- if (!isOAuthAuth(initialAuth)) {
1130
- try {
1131
- await clearAccounts();
1132
- }
1133
- catch { /* ignore */ }
1134
- }
1135
- let accountManager = isOAuthAuth(initialAuth)
1136
- ? await AccountManager.loadFromDisk(initialAuth)
1137
- : null;
1138
- if (accountManager) {
1139
- activeAccountManager = accountManager;
1140
- if (accountManager.getAccountCount() > 0) {
1141
- accountManager.requestSaveToDisk();
1142
- }
1143
- }
1144
- let refreshQueue = null;
1145
- if (accountManager && config.proactive_token_refresh && accountManager.getAccountCount() > 0) {
1146
- refreshQueue = createProactiveRefreshQueue(client, providerId, {
1147
- enabled: config.proactive_token_refresh,
1148
- bufferSeconds: config.proactive_refresh_buffer_seconds,
1149
- checkIntervalSeconds: config.proactive_refresh_check_interval_seconds,
1150
- });
1151
- refreshQueue.setAccountManager(accountManager);
1152
- refreshQueue.start();
1153
- }
1154
- if (isDebugEnabled()) {
1155
- const logPath = getLogFilePath();
1156
- if (logPath) {
1157
- try {
1158
- await client.tui.showToast({
1159
- body: { message: `Debug log: ${logPath}`, variant: "info" },
1160
- });
1161
- }
1162
- catch {
1163
- }
1164
- }
1165
- }
1166
- if (provider.models) {
1167
- for (const model of Object.values(provider.models)) {
1168
- if (model) {
1169
- model.cost = { input: 0, output: 0 };
1170
- }
1171
- }
1172
- }
1173
- return {
1174
- apiKey: "",
1175
- async fetch(input, init) {
1176
- const materialized = await materializeGenerativeLanguageFetchInput(input, init);
1177
- input = materialized.input;
1178
- init = materialized.init;
1179
- if (!isGenerativeLanguageRequest(input)) {
1180
- return fetch(input, init);
1181
- }
1182
- const latestAuth = await getAuth();
1183
- if (!isOAuthAuth(latestAuth)) {
1184
- return fetch(input, init);
1185
- }
1186
- if (!accountManager) {
1187
- accountManager = await AccountManager.loadFromDisk(latestAuth);
1188
- activeAccountManager = accountManager;
1189
- if (accountManager.getAccountCount() > 0)
1190
- accountManager.requestSaveToDisk();
1191
- if (config.proactive_token_refresh && accountManager.getAccountCount() > 0) {
1192
- refreshQueue = createProactiveRefreshQueue(client, providerId, {
1193
- enabled: config.proactive_token_refresh,
1194
- bufferSeconds: config.proactive_refresh_buffer_seconds,
1195
- checkIntervalSeconds: config.proactive_refresh_check_interval_seconds,
1196
- });
1197
- refreshQueue.setAccountManager(accountManager);
1198
- refreshQueue.start();
1199
- }
1200
- }
1201
- if (accountManager.getAccountCount() === 0) {
1202
- return createSyntheticErrorResponse("[Antigravity] No accounts configured. Run `opencode auth login`.", "unknown");
1203
- }
1204
- let urlString = toUrlString(input);
1205
- let family = getModelFamilyFromUrl(urlString);
1206
- let model = extractModelFromUrl(urlString);
1207
- let crossFamilyFallbackApplied = false;
1208
- const ranking = config.model_ranking ?? DEFAULT_MODEL_RANKING;
1209
- const stageMap = {
1210
- "antigravity-auto-best": 0,
1211
- "antigravity-auto-high": 1,
1212
- "antigravity-auto-balanced": 2,
1213
- "antigravity-auto-fastest": 3,
1214
- };
1215
- let resolvedModel = null;
1216
- if (model && stageMap[model] !== undefined) {
1217
- resolvedModel = resolveModelByRanking(stageMap[model], ranking, accountManager);
1218
- }
1219
- else if (config.auto_mode && model === "antigravity-auto") {
1220
- const stageIndex = config.auto_mode_stage
1221
- ? stageMap[`antigravity-auto-${config.auto_mode_stage}`] ?? 0
1222
- : 0;
1223
- resolvedModel = resolveModelByRanking(stageIndex, ranking, accountManager);
1224
- }
1225
- if (resolvedModel) {
1226
- urlString = urlString.replace(/\/models\/[^:\/?]+/, `/models/${resolvedModel}`);
1227
- input = urlString;
1228
- family = getModelFamilyFromUrl(urlString);
1229
- model = resolvedModel;
1230
- }
1231
- const debugLines = [];
1232
- const pushDebug = (line) => {
1233
- if (!isDebugEnabled())
1234
- return;
1235
- debugLines.push(line);
1236
- };
1237
- pushDebug(`request=${urlString}`);
1238
- let lastFailure = null;
1239
- let lastError = null;
1240
- const abortSignal = init?.signal ?? undefined;
1241
- const checkAborted = () => {
1242
- if (abortSignal?.aborted) {
1243
- throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("Aborted");
1244
- }
1245
- };
1246
- const quietMode = config.quiet_mode;
1247
- const toastScope = config.toast_scope;
1248
- // Helper to show toast without blocking on abort (respects quiet_mode and toast_scope)
1249
- const showToast = async (message, variant, forceChild = false) => {
1250
- log.debug("toast", { message, variant, isChildSession, toastScope });
1251
- if (quietMode)
1252
- return;
1253
- if (abortSignal?.aborted)
1254
- return;
1255
- // Filter toasts for child sessions when toast_scope is "root_only"
1256
- if (toastScope === "root_only" && isChildSession && !forceChild) {
1257
- log.debug("toast-suppressed-child-session", { message, variant, parentID: childSessionParentID });
1258
- return;
1259
- }
1260
- if (variant === "warning" && message.toLowerCase().includes("rate")) {
1261
- if (!shouldShowRateLimitToast(message)) {
1262
- return;
1263
- }
1264
- }
1265
- try {
1266
- await client.tui.showToast({
1267
- body: { message, variant },
1268
- });
1269
- }
1270
- catch {
1271
- }
1272
- };
1273
- const hasOtherAccountWithAntigravity = (currentAccount) => {
1274
- if (family !== "gemini")
1275
- return false;
1276
- return accountManager.hasOtherAccountWithAntigravityAvailable(currentAccount.index, family, model);
1277
- };
1278
- while (true) {
1279
- let loopLeasedAccountIndex = null;
1280
- let selectedProxy = undefined;
1281
- try {
1282
- if (abortSignal?.aborted) {
1283
- pushDebug("request-aborted-by-client");
1284
- return createSyntheticErrorResponse("Request was aborted by OpenCode.", model ?? "unknown");
1285
- }
1286
- checkAborted();
1287
- const accountCount = accountManager.getAccountCount();
1288
- const routingDecision = resolveHeaderRoutingDecision(urlString, family, config);
1289
- const { cliFirst, preferredHeaderStyle, explicitQuota, allowQuotaFallback, } = routingDecision;
1290
- if (accountCount === 0) {
1291
- return createSyntheticErrorResponse("[Antigravity] No accounts available. Run `opencode auth login`.", model ?? "unknown");
1292
- }
1293
- const softQuotaCacheTtlMs = computeSoftQuotaCacheTtlMs(config.soft_quota_cache_ttl_minutes, config.quota_refresh_interval_minutes);
1294
- let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1295
- if (!account && allowQuotaFallback) {
1296
- const alternateHeaderStyle = preferredHeaderStyle === "antigravity" ? "gemini-cli" : "antigravity";
1297
- account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1298
- if (account) {
1299
- pushDebug(`selected-by-fallback idx=${account.index} preferred=${preferredHeaderStyle} alternate=${alternateHeaderStyle}`);
1300
- }
1301
- }
1302
- if (!account && config.fallback_enabled) {
1303
- const ranking = config.model_ranking ?? DEFAULT_MODEL_RANKING;
1304
- const currentIndex = ranking.indexOf(model ?? "");
1305
- if (currentIndex !== -1 && currentIndex < ranking.length - 1) {
1306
- const nextModel = ranking[currentIndex + 1];
1307
- if (!nextModel)
1308
- break;
1309
- const nextFamily = nextModel.includes("claude") ? "claude" : "gemini";
1310
- urlString = urlString.replace(/\/models\/[^:\/?]+/, `/models/${nextModel}`);
1311
- input = urlString;
1312
- family = nextFamily;
1313
- model = nextModel;
1314
- await showToast(`Rate-limited. Falling back to ${nextModel}`, "warning");
1315
- crossFamilyFallbackApplied = true;
1316
- continue; // restart loop with new model
1317
- }
1318
- }
1319
- if (!account) {
1320
- if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model)) {
1321
- const threshold = config.soft_quota_threshold_percent;
1322
- const softQuotaWaitMs = accountManager.getMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model);
1323
- const maxWaitMs = (config.max_rate_limit_wait_seconds ?? 300) * 1000;
1324
- if (softQuotaWaitMs === null || (maxWaitMs > 0 && softQuotaWaitMs > maxWaitMs)) {
1325
- const waitTimeFormatted = softQuotaWaitMs ? formatWaitTime(softQuotaWaitMs) : "unknown";
1326
- await showToast(`All accounts over ${threshold}% quota threshold. Resets in ${waitTimeFormatted}.`, "error");
1327
- {
1328
- const errorMessage = `[Antigravity] Quota protection: All ${accountCount} account(s) are over ${threshold}% usage for ${family}. Quota resets in ${waitTimeFormatted}.`;
1329
- return createSyntheticErrorResponse(errorMessage, model ?? "unknown");
1330
- }
1331
- }
1332
- const waitSecValue = Math.max(1, Math.ceil(softQuotaWaitMs / 1000));
1333
- pushDebug(`all-over-soft-quota family=${family} accounts=${accountCount} waitMs=${softQuotaWaitMs}`);
1334
- if (!softQuotaToastShown) {
1335
- await showToast(`All ${accountCount} account(s) over ${threshold}% quota. Waiting ${formatWaitTime(softQuotaWaitMs)}...`, "warning");
1336
- softQuotaToastShown = true;
1337
- }
1338
- await sleep(softQuotaWaitMs, abortSignal);
1339
- continue;
1340
- }
1341
- if (crossFamilyFallbackApplied) {
1342
- const errorMessage = `[Antigravity Error] All accounts are temporarily unavailable.\n\nBoth Claude and Gemini accounts are blocked by rate limits or health/cooldown filters.\nPlease wait a few minutes and try again, or add more accounts with \`opencode auth login\`.`;
1343
- return createSyntheticErrorResponse(errorMessage, model ?? "unknown");
1344
- }
1345
- const strictWait = !allowQuotaFallback;
1346
- const waitMs = accountManager.getMinWaitTimeForFamily(family, model, preferredHeaderStyle, strictWait) || 60_000;
1347
- const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000));
1348
- pushDebug(`all-rate-limited family=${family} accounts=${accountCount} waitMs=${waitMs}`);
1349
- if (isDebugEnabled()) {
1350
- logAccountContext("All accounts rate-limited", {
1351
- index: -1,
1352
- family,
1353
- totalAccounts: accountCount,
1354
- });
1355
- logRateLimitSnapshot(family, accountManager.getAccountsSnapshot());
1356
- }
1357
- // 0 means disabled (wait indefinitely)
1358
- const maxWaitMs = (config.max_rate_limit_wait_seconds ?? 300) * 1000;
1359
- if (maxWaitMs > 0 && waitMs > maxWaitMs) {
1360
- const waitTimeFormatted = formatWaitTime(waitMs);
1361
- await showToast(`Rate limited for ${waitTimeFormatted}. Try again later or add another account.`, "error");
1362
- {
1363
- const errorMessage = `[Antigravity] All ${accountCount} account(s) rate-limited for ${family}. Quota resets in ${waitTimeFormatted}. Add more accounts or wait and retry.`;
1364
- return createSyntheticErrorResponse(errorMessage, model ?? "unknown");
1365
- }
1366
- }
1367
- if (!rateLimitToastShown) {
1368
- await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Waiting ${waitSecValue}s...`, "warning");
1369
- rateLimitToastShown = true;
1370
- }
1371
- await sleep(waitMs, abortSignal);
1372
- continue;
1373
- }
1374
- resetAllAccountsBlockedToasts();
1375
- pushDebug(`selected idx=${account.index} email=${account.email ?? ""} family=${family} accounts=${accountCount} strategy=${config.account_selection_strategy}`);
1376
- if (isDebugEnabled()) {
1377
- logAccountContext("Selected", {
1378
- index: account.index,
1379
- email: account.email,
1380
- family,
1381
- totalAccounts: accountCount,
1382
- rateLimitState: account.rateLimitResetTimes,
1383
- });
1384
- }
1385
- // Show toast when switching to a different account (debounced, quiet_mode handled by showToast)
1386
- if (accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
1387
- const accountLabel = account.email || `Account ${account.index + 1}`;
1388
- const enabledAccounts = accountManager.getEnabledAccounts();
1389
- const enabledPosition = enabledAccounts.findIndex(a => a.index === account.index) + 1;
1390
- let categoryString = "";
1391
- if (resolvedModel !== null) {
1392
- const rankIdx = ranking.indexOf(model ?? "");
1393
- const rankNames = ["Best", "High", "Balanced", "Fastest"];
1394
- const rankName = rankNames[rankIdx] ?? model;
1395
- categoryString = ` (Auto: ${rankName} - ${model})`;
1396
- }
1397
- await showToast(`Using ${accountLabel} (${enabledPosition}/${accountCount})${categoryString}`, "info");
1398
- accountManager.markToastShown(account.index);
1399
- }
1400
- accountManager.requestSaveToDisk();
1401
- let authRecord = accountManager.toAuthDetails(account);
1402
- if (accessTokenExpired(authRecord)) {
1403
- try {
1404
- const refreshed = await refreshAccessToken(authRecord, client, providerId);
1405
- if (!refreshed) {
1406
- const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
1407
- getHealthTracker().recordFailure(account.index);
1408
- lastError = new Error("Antigravity token refresh failed");
1409
- if (shouldCooldown) {
1410
- accountManager.markAccountCoolingDown(account, cooldownMs, "auth-failure");
1411
- accountManager.markRateLimited(account, cooldownMs, family, "antigravity", model);
1412
- pushDebug(`token-refresh-failed: cooldown ${cooldownMs}ms after ${failures} failures`);
1413
- }
1414
- continue;
1415
- }
1416
- resetAccountFailureState(account.index);
1417
- accountManager.updateFromAuth(account, refreshed);
1418
- authRecord = refreshed;
1419
- try {
1420
- await accountManager.saveToDisk();
1421
- }
1422
- catch (error) {
1423
- log.error("Failed to persist refreshed auth", { error: String(error) });
1424
- }
1425
- }
1426
- catch (error) {
1427
- if (error instanceof AntigravityTokenRefreshError && error.code === "invalid_grant") {
1428
- const removed = accountManager.removeAccount(account);
1429
- if (removed) {
1430
- log.warn("Removed revoked account from pool - reauthenticate via `opencode auth login`");
1431
- try {
1432
- await accountManager.saveToDisk();
1433
- }
1434
- catch (persistError) {
1435
- log.error("Failed to persist revoked account removal", { error: String(persistError) });
1436
- }
1437
- }
1438
- if (accountManager.getAccountCount() === 0) {
1439
- try {
1440
- await client.auth.set({
1441
- path: { id: providerId },
1442
- body: { type: "oauth", refresh: "", access: "", expires: 0 },
1443
- });
1444
- }
1445
- catch (storeError) {
1446
- log.error("Failed to clear stored Antigravity OAuth credentials", { error: String(storeError) });
1447
- }
1448
- return createSyntheticErrorResponse("[Antigravity] All accounts have invalid refresh tokens. Run `opencode auth login` and reauthenticate.", model ?? "unknown");
1449
- }
1450
- lastError = error;
1451
- continue;
1452
- }
1453
- const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
1454
- getHealthTracker().recordFailure(account.index);
1455
- lastError = error instanceof Error ? error : new Error(String(error));
1456
- if (shouldCooldown) {
1457
- accountManager.markAccountCoolingDown(account, cooldownMs, "auth-failure");
1458
- accountManager.markRateLimited(account, cooldownMs, family, "antigravity", model);
1459
- pushDebug(`token-refresh-error: cooldown ${cooldownMs}ms after ${failures} failures`);
1460
- }
1461
- continue;
1462
- }
1463
- }
1464
- const accessToken = authRecord.access;
1465
- if (!accessToken) {
1466
- lastError = new Error("Missing access token");
1467
- if (accountCount <= 1) {
1468
- const errorMessage = "[Antigravity] Missing access token. Please run `opencode auth login` to re-authenticate.";
1469
- return createSyntheticErrorResponse(errorMessage, model ?? "unknown");
1470
- }
1471
- continue;
1472
- }
1473
- let projectContext;
1474
- try {
1475
- projectContext = await ensureProjectContext(authRecord);
1476
- resetAccountFailureState(account.index);
1477
- }
1478
- catch (error) {
1479
- const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
1480
- getHealthTracker().recordFailure(account.index);
1481
- lastError = error instanceof Error ? error : new Error(String(error));
1482
- if (shouldCooldown) {
1483
- accountManager.markAccountCoolingDown(account, cooldownMs, "project-error");
1484
- accountManager.markRateLimited(account, cooldownMs, family, "antigravity", model);
1485
- pushDebug(`project-context-error: cooldown ${cooldownMs}ms after ${failures} failures`);
1486
- }
1487
- continue;
1488
- }
1489
- if (projectContext.auth.refresh !== authRecord.refresh ||
1490
- projectContext.auth.access !== authRecord.access) {
1491
- accountManager.updateFromAuth(account, projectContext.auth);
1492
- authRecord = projectContext.auth;
1493
- try {
1494
- await accountManager.saveToDisk();
1495
- }
1496
- catch (error) {
1497
- log.error("Failed to persist project context", { error: String(error) });
1498
- }
1499
- }
1500
- const runThinkingWarmup = async (prepared, projectId) => {
1501
- if (!prepared.needsSignedThinkingWarmup || !prepared.sessionId) {
1502
- return;
1503
- }
1504
- if (!trackWarmupAttempt(prepared.sessionId)) {
1505
- return;
1506
- }
1507
- const warmupBody = buildThinkingWarmupBody(typeof prepared.init.body === "string" ? prepared.init.body : undefined, Boolean(prepared.effectiveModel?.toLowerCase().includes("claude") && prepared.effectiveModel?.toLowerCase().includes("thinking")));
1508
- if (!warmupBody) {
1509
- return;
1510
- }
1511
- const warmupUrl = toWarmupStreamUrl(prepared.request);
1512
- const warmupHeaders = new Headers(prepared.init.headers ?? {});
1513
- warmupHeaders.set("accept", "text/event-stream");
1514
- const warmupInit = {
1515
- ...prepared.init,
1516
- method: prepared.init.method ?? "POST",
1517
- headers: warmupHeaders,
1518
- body: warmupBody,
1519
- };
1520
- const warmupDebugContext = startAntigravityDebugRequest({
1521
- originalUrl: warmupUrl,
1522
- resolvedUrl: warmupUrl,
1523
- method: warmupInit.method,
1524
- headers: warmupHeaders,
1525
- body: warmupBody,
1526
- streaming: true,
1527
- projectId,
1528
- });
1529
- try {
1530
- pushDebug("thinking-warmup: start");
1531
- const warmupResponse = await fetch(warmupUrl, warmupInit);
1532
- const transformed = await transformAntigravityResponse(warmupResponse, true, warmupDebugContext, prepared.requestedModel, projectId, warmupUrl, prepared.effectiveModel, prepared.sessionId);
1533
- await transformed.text();
1534
- markWarmupSuccess(prepared.sessionId);
1535
- pushDebug("thinking-warmup: done");
1536
- }
1537
- catch (error) {
1538
- clearWarmupAttempt(prepared.sessionId);
1539
- pushDebug(`thinking-warmup: failed ${error instanceof Error ? error.message : String(error)}`);
1540
- }
1541
- };
1542
- let shouldSwitchAccount = false;
1543
- // - Models with antigravity- prefix -> use Antigravity quota
1544
- // - Gemini models without explicit prefix -> follow cli_first
1545
- // - Claude models -> always use Antigravity
1546
- let headerStyle = preferredHeaderStyle;
1547
- pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`);
1548
- getLeaseTracker().lease(account.index);
1549
- loopLeasedAccountIndex = account.index;
1550
- if (account.fingerprint) {
1551
- pushDebug(`fingerprint: quotaUser=${account.fingerprint.quotaUser} deviceId=${account.fingerprint.deviceId.slice(0, 8)}...`);
1552
- }
1553
- if (accountManager.isRateLimitedForHeaderStyle(account, family, headerStyle, model)) {
1554
- if (allowQuotaFallback && family === "gemini" && headerStyle === "antigravity") {
1555
- if (accountManager.hasOtherAccountWithAntigravityAvailable(account.index, family, model)) {
1556
- pushDebug(`antigravity rate-limited on account ${account.index}, but available on other accounts. Switching.`);
1557
- shouldSwitchAccount = true;
1558
- }
1559
- else {
1560
- const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1561
- const fallbackStyle = resolveQuotaFallbackHeaderStyle({
1562
- family,
1563
- headerStyle,
1564
- alternateStyle,
1565
- });
1566
- if (fallbackStyle) {
1567
- await showToast(`Antigravity quota exhausted on all accounts. Using Gemini CLI quota.`, "warning");
1568
- headerStyle = fallbackStyle;
1569
- pushDebug(`all-accounts antigravity exhausted, quota fallback: ${headerStyle}`);
1570
- }
1571
- else {
1572
- shouldSwitchAccount = true;
1573
- }
1574
- }
1575
- }
1576
- else if (allowQuotaFallback && family === "gemini") {
1577
- const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1578
- const fallbackStyle = resolveQuotaFallbackHeaderStyle({
1579
- family,
1580
- headerStyle,
1581
- alternateStyle,
1582
- });
1583
- if (fallbackStyle) {
1584
- const quotaName = headerStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
1585
- const altQuotaName = fallbackStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
1586
- await showToast(`${quotaName} quota exhausted, using ${altQuotaName} quota`, "warning");
1587
- headerStyle = fallbackStyle;
1588
- pushDebug(`quota fallback: ${headerStyle}`);
1589
- }
1590
- else {
1591
- shouldSwitchAccount = true;
1592
- }
1593
- }
1594
- else {
1595
- shouldSwitchAccount = true;
1596
- }
1597
- }
1598
- while (!shouldSwitchAccount) {
1599
- let forceThinkingRecovery = false;
1600
- let tokenConsumed = false;
1601
- let capacityRetryCount = 0;
1602
- let lastEndpointIndex = -1;
1603
- for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
1604
- if (i !== lastEndpointIndex) {
1605
- capacityRetryCount = 0;
1606
- lastEndpointIndex = i;
1607
- }
1608
- const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
1609
- if (headerStyle === "gemini-cli" && currentEndpoint !== ANTIGRAVITY_ENDPOINT_PROD) {
1610
- pushDebug(`Skipping sandbox endpoint ${currentEndpoint} for gemini-cli headerStyle`);
1611
- continue;
1612
- }
1613
- try {
1614
- const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint, headerStyle, forceThinkingRecovery, {
1615
- claudeToolHardening: config.claude_tool_hardening,
1616
- claudePromptAutoCaching: config.claude_prompt_auto_caching,
1617
- fingerprint: account.fingerprint,
1618
- debugGeminiPayloads: config.debug_gemini_payloads,
1619
- });
1620
- selectedProxy = getProxyManager().selectBestProxy(account.proxies);
1621
- if (selectedProxy) {
1622
- prepared.init.proxy = selectedProxy;
1623
- }
1624
- const originalUrl = toUrlString(input);
1625
- const resolvedUrl = toUrlString(prepared.request);
1626
- pushDebug(`endpoint=${currentEndpoint}`);
1627
- pushDebug(`resolved=${resolvedUrl}`);
1628
- const debugContext = startAntigravityDebugRequest({
1629
- originalUrl,
1630
- resolvedUrl,
1631
- method: prepared.init.method,
1632
- headers: prepared.init.headers,
1633
- body: prepared.init.body,
1634
- streaming: prepared.streaming,
1635
- projectId: projectContext.effectiveProjectId,
1636
- });
1637
- const createFailureContext = (failureResponse) => ({
1638
- response: failureResponse,
1639
- streaming: prepared.streaming,
1640
- debugContext,
1641
- requestedModel: prepared.requestedModel,
1642
- projectId: prepared.projectId,
1643
- endpoint: prepared.endpoint,
1644
- effectiveModel: prepared.effectiveModel,
1645
- sessionId: prepared.sessionId,
1646
- toolDebugMissing: prepared.toolDebugMissing,
1647
- toolDebugSummary: prepared.toolDebugSummary,
1648
- toolDebugPayload: prepared.toolDebugPayload,
1649
- });
1650
- await runThinkingWarmup(prepared, projectContext.effectiveProjectId);
1651
- if (config.request_jitter_max_ms > 0) {
1652
- const jitterMs = Math.floor(Math.random() * config.request_jitter_max_ms);
1653
- if (jitterMs > 0) {
1654
- await sleep(jitterMs, abortSignal);
1655
- }
1656
- }
1657
- if (config.account_selection_strategy === 'hybrid') {
1658
- tokenConsumed = getTokenTracker().consume(account.index);
1659
- }
1660
- const PROGRESS_TOAST_INTERVAL_MS = isChildSession ? 25000 : 8000;
1661
- const fetchStartTime = Date.now();
1662
- let progressToastCount = 0;
1663
- const progressInterval = setInterval(async () => {
1664
- progressToastCount++;
1665
- const elapsedSec = Math.round((Date.now() - fetchStartTime) / 1000);
1666
- const prefix = isChildSession ? "[Subagent] " : "";
1667
- await showToast(`⏳ ${prefix}Waiting for ${family} response... (${elapsedSec}s)`, "info", true);
1668
- }, PROGRESS_TOAST_INTERVAL_MS);
1669
- let response;
1670
- const fetchController = new AbortController();
1671
- const fetchTimeoutId = setTimeout(() => fetchController.abort(), 300000); // 5 minutes
1672
- let streamFinished = false;
1673
- if (abortSignal) {
1674
- abortSignal.addEventListener("abort", () => {
1675
- fetchController.abort(new Error("Opencode aborted the request"));
1676
- });
1677
- }
1678
- const mergedInit = { ...prepared.init, signal: fetchController.signal };
1679
- try {
1680
- response = await fetch(prepared.request, mergedInit);
1681
- }
1682
- finally {
1683
- clearTimeout(fetchTimeoutId);
1684
- clearInterval(progressInterval);
1685
- }
1686
- if (Date.now() - fetchStartTime > PROGRESS_TOAST_INTERVAL_MS) {
1687
- pushDebug(`fetch-slow: ${Date.now() - fetchStartTime}ms (${progressToastCount} progress toasts)`);
1688
- }
1689
- pushDebug(`status=${response.status} ${response.statusText}`);
1690
- if (response.status === 429 || response.status === 503 || response.status === 529) {
1691
- if (tokenConsumed) {
1692
- getTokenTracker().refund(account.index);
1693
- tokenConsumed = false;
1694
- }
1695
- const defaultRetryMs = (config.default_retry_after_seconds ?? 60) * 1000;
1696
- const maxBackoffMs = (config.max_backoff_seconds ?? 60) * 1000;
1697
- const headerRetryMs = retryAfterMsFromResponse(response, defaultRetryMs);
1698
- const bodyInfo = await extractRetryInfoFromBody(response);
1699
- const serverRetryMs = bodyInfo.retryDelayMs ?? headerRetryMs;
1700
- // [Enhanced Parsing] Pass status to handling logic
1701
- const rateLimitReason = parseRateLimitReason(bodyInfo.reason, bodyInfo.message, response.status);
1702
- if (rateLimitReason === "MODEL_CAPACITY_EXHAUSTED" || rateLimitReason === "SERVER_ERROR") {
1703
- const baseDelayMs = 1000;
1704
- const maxDelayMs = 8000;
1705
- const exponentialDelay = Math.min(baseDelayMs * Math.pow(2, capacityRetryCount), maxDelayMs);
1706
- const jitter = exponentialDelay * (0.9 + Math.random() * 0.2);
1707
- const waitMs = Math.round(jitter);
1708
- const waitSec = Math.round(waitMs / 1000);
1709
- pushDebug(`Server busy (${rateLimitReason}) on account ${account.index}, exponential backoff ${waitMs}ms (attempt ${capacityRetryCount + 1})`);
1710
- await showToast(`⏳ Server busy (${response.status}). Retrying in ${waitSec}s...`, "warning");
1711
- await sleep(waitMs, abortSignal);
1712
- // (i++ in the loop will bring it back to the current index)
1713
- if (capacityRetryCount < 3) {
1714
- capacityRetryCount++;
1715
- i -= 1;
1716
- continue;
1717
- }
1718
- else {
1719
- pushDebug(`Max capacity retries (3) exhausted for endpoint ${currentEndpoint}, regenerating fingerprint...`);
1720
- const newFingerprint = accountManager.regenerateAccountFingerprint(account.index);
1721
- if (newFingerprint) {
1722
- pushDebug(`Fingerprint regenerated for account ${account.index}`);
1723
- }
1724
- continue;
1725
- }
1726
- }
1727
- if (selectedProxy) {
1728
- getProxyManager().markCooldown(selectedProxy, 60000);
1729
- }
1730
- const quotaKey = headerStyleToQuotaKey(headerStyle, family);
1731
- const { attempt, delayMs, isDuplicate } = getRateLimitBackoff(account.index, quotaKey, serverRetryMs);
1732
- const smartBackoffMs = calculateBackoffMs(rateLimitReason, account.consecutiveFailures ?? 0, serverRetryMs);
1733
- const effectiveDelayMs = Math.max(delayMs, smartBackoffMs);
1734
- pushDebug(`429 idx=${account.index} email=${account.email ?? ""} family=${family} delayMs=${effectiveDelayMs} attempt=${attempt} reason=${rateLimitReason}`);
1735
- if (bodyInfo.message) {
1736
- pushDebug(`429 message=${bodyInfo.message}`);
1737
- }
1738
- if (bodyInfo.quotaResetTime) {
1739
- pushDebug(`429 quotaResetTime=${bodyInfo.quotaResetTime}`);
1740
- }
1741
- if (bodyInfo.reason) {
1742
- pushDebug(`429 reason=${bodyInfo.reason}`);
1743
- }
1744
- logRateLimitEvent(account.index, account.email, family, response.status, effectiveDelayMs, bodyInfo);
1745
- await logResponseBody(debugContext, response, 429);
1746
- getHealthTracker().recordRateLimit(account.index);
1747
- const accountLabel = account.email || `Account ${account.index + 1}`;
1748
- if (attempt === 1 && rateLimitReason !== "QUOTA_EXHAUSTED") {
1749
- await showToast(`Rate limited. Quick retry in 1s...`, "warning");
1750
- await sleep(FIRST_RETRY_DELAY_MS, abortSignal);
1751
- if (config.scheduling_mode === 'cache_first') {
1752
- const maxCacheFirstWaitMs = config.max_cache_first_wait_seconds * 1000;
1753
- if (effectiveDelayMs <= maxCacheFirstWaitMs) {
1754
- pushDebug(`cache_first: waiting ${effectiveDelayMs}ms for same account to recover`);
1755
- await showToast(`⏳ Waiting ${Math.ceil(effectiveDelayMs / 1000)}s for same account (prompt cache preserved)...`, "info");
1756
- accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
1757
- await sleep(effectiveDelayMs, abortSignal);
1758
- i -= 1;
1759
- continue;
1760
- }
1761
- pushDebug(`cache_first: wait ${effectiveDelayMs}ms exceeds max ${maxCacheFirstWaitMs}ms, switching account`);
1762
- }
1763
- if (config.switch_on_first_rate_limit && accountCount > 1) {
1764
- accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs, config.failure_ttl_seconds * 1000);
1765
- shouldSwitchAccount = true;
1766
- break;
1767
- }
1768
- i -= 1;
1769
- continue;
1770
- }
1771
- accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs, config.failure_ttl_seconds * 1000);
1772
- accountManager.requestSaveToDisk();
1773
- if (family === "gemini") {
1774
- if (headerStyle === "antigravity") {
1775
- if (hasOtherAccountWithAntigravity(account)) {
1776
- pushDebug(`antigravity exhausted on account ${account.index}, but available on others. Switching account.`);
1777
- await showToast(`Rate limited again. Switching account in 5s...`, "warning");
1778
- await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1779
- shouldSwitchAccount = true;
1780
- break;
1781
- }
1782
- if (allowQuotaFallback) {
1783
- const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1784
- const fallbackStyle = resolveQuotaFallbackHeaderStyle({
1785
- family,
1786
- headerStyle,
1787
- alternateStyle,
1788
- });
1789
- if (fallbackStyle) {
1790
- const safeModelName = model || "this model";
1791
- await showToast(`Antigravity quota exhausted for ${safeModelName}. Switching to Gemini CLI quota...`, "warning");
1792
- headerStyle = fallbackStyle;
1793
- pushDebug(`quota fallback: ${headerStyle}`);
1794
- continue;
1795
- }
1796
- }
1797
- }
1798
- else if (headerStyle === "gemini-cli") {
1799
- if (allowQuotaFallback) {
1800
- const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1801
- const fallbackStyle = resolveQuotaFallbackHeaderStyle({
1802
- family,
1803
- headerStyle,
1804
- alternateStyle,
1805
- });
1806
- if (fallbackStyle) {
1807
- const safeModelName = model || "this model";
1808
- await showToast(`Gemini CLI quota exhausted for ${safeModelName}. Switching to Antigravity quota...`, "warning");
1809
- headerStyle = fallbackStyle;
1810
- pushDebug(`quota fallback: ${headerStyle}`);
1811
- continue;
1812
- }
1813
- }
1814
- }
1815
- }
1816
- const quotaName = headerStyle === "antigravity" ? "Antigravity" : "Gemini CLI";
1817
- if (accountCount > 1) {
1818
- const quotaMsg = bodyInfo.quotaResetTime
1819
- ? ` (quota resets ${bodyInfo.quotaResetTime})`
1820
- : ``;
1821
- await showToast(`Rate limited again. Switching account in 5s...${quotaMsg}`, "warning");
1822
- await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1823
- }
1824
- else {
1825
- const expBackoffMs = Math.min(FIRST_RETRY_DELAY_MS * Math.pow(2, attempt - 1), 60000);
1826
- const expBackoffFormatted = expBackoffMs >= 1000 ? `${Math.round(expBackoffMs / 1000)}s` : `${expBackoffMs}ms`;
1827
- await showToast(`Rate limited. Retrying in ${expBackoffFormatted} (attempt ${attempt})...`, "warning");
1828
- await sleep(expBackoffMs, abortSignal);
1829
- }
1830
- lastFailure = createFailureContext(response);
1831
- shouldSwitchAccount = true;
1832
- break;
1833
- }
1834
- const quotaKey = headerStyleToQuotaKey(headerStyle, family);
1835
- resetRateLimitState(account.index, quotaKey);
1836
- resetAccountFailureState(account.index);
1837
- if (response.status === 403) {
1838
- const errorBodyText = await response.clone().text().catch(() => "");
1839
- const extracted = extractVerificationErrorDetails(errorBodyText);
1840
- if (extracted.validationRequired) {
1841
- const verificationReason = extracted.message ?? "Google requires account verification.";
1842
- const cooldownMs = 10 * 60 * 1000;
1843
- accountManager.markAccountVerificationRequired(account.index, verificationReason, extracted.verifyUrl);
1844
- accountManager.markAccountCoolingDown(account, cooldownMs, "validation-required");
1845
- accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model);
1846
- const label = account.email || `Account ${account.index + 1}`;
1847
- if (accountManager.shouldShowAccountToast(account.index, 60000)) {
1848
- await showToast(`⚠ ${label} needs verification. Run 'opencode auth login' and use Verify accounts.`, "warning");
1849
- accountManager.markToastShown(account.index);
1850
- }
1851
- pushDebug(`verification-required: disabled account ${account.index}`);
1852
- getHealthTracker().recordFailure(account.index);
1853
- lastFailure = createFailureContext(response);
1854
- shouldSwitchAccount = true;
1855
- break;
1856
- }
1857
- }
1858
- const shouldRetryEndpoint = (response.status === 403 ||
1859
- response.status === 404 ||
1860
- response.status >= 500);
1861
- if (shouldRetryEndpoint && i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
1862
- await logResponseBody(debugContext, response, response.status);
1863
- lastFailure = createFailureContext(response);
1864
- continue;
1865
- }
1866
- if (response.ok) {
1867
- account.consecutiveFailures = 0;
1868
- getHealthTracker().recordSuccess(account.index);
1869
- accountManager.markAccountUsed(account.index);
1870
- void triggerAsyncQuotaRefreshForAccount(accountManager, account.index, client, providerId, config.quota_refresh_interval_minutes);
1871
- }
1872
- logAntigravityDebugResponse(debugContext, response, {
1873
- note: response.ok ? "Success" : `Error ${response.status}`,
1874
- });
1875
- if (response.ok && !prepared.streaming) {
1876
- await logResponseBody(debugContext, response, response.status);
1877
- }
1878
- if (!response.ok) {
1879
- await logResponseBody(debugContext, response, response.status);
1880
- if (response.status === 400) {
1881
- const cloned = response.clone();
1882
- const bodyText = await cloned.text();
1883
- pushDebug(`400-error: crossFamilyFallback=${crossFamilyFallbackApplied} body=${bodyText.slice(0, 200)}`);
1884
- if (bodyText.includes("Prompt is too long") || bodyText.includes("prompt_too_long")) {
1885
- await showToast("Context too long - use /compact to reduce size", "warning");
1886
- 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`;
1887
- return createSyntheticErrorResponse(errorMessage, prepared.requestedModel);
1888
- }
1889
- if (crossFamilyFallbackApplied) {
1890
- const snippet = bodyText.slice(0, 300).replace(/\n/g, " ");
1891
- await showToast(`Gemini fallback returned 400. Returning error to avoid loop.`, "error");
1892
- const errorMessage = `[Antigravity Error] Cross-family fallback failed (400 Bad Request).\n\nThe request could not be processed by the fallback model (${prepared.effectiveModel ?? "unknown"}).\nDetails: ${snippet}\n\nPlease retry or use /compact to simplify the context.`;
1893
- return createSyntheticErrorResponse(errorMessage, prepared.requestedModel);
1894
- }
1895
- // Generic 400: unknown error (thought_signature, model rejection, etc.)
1896
- const snippet400 = bodyText.slice(0, 200).replace(/\n/g, " ");
1897
- log.warn("Generic 400 from Antigravity, cooling down account and retrying", {
1898
- model: prepared.effectiveModel,
1899
- body: snippet400,
1900
- });
1901
- if (shouldShowRateLimitToast(`400: ${snippet400}`)) {
1902
- await showToast(`Model returned 400 — retrying with next account`, "warning");
1903
- }
1904
- if (account) {
1905
- accountManager.markAccountCoolingDown(account, 30_000, "project-error");
1906
- }
1907
- shouldSwitchAccount = true;
1908
- break;
1909
- }
1910
- }
1911
- // and retry if so (up to config.empty_response_max_attempts times)
1912
- if (response.ok && !prepared.streaming) {
1913
- const maxAttempts = config.empty_response_max_attempts ?? 4;
1914
- const retryDelayMs = config.empty_response_retry_delay_ms ?? 2000;
1915
- const clonedForCheck = response.clone();
1916
- const bodyText = await clonedForCheck.text();
1917
- if (isEmptyResponseBody(bodyText)) {
1918
- const emptyAttemptKey = `${prepared.sessionId ?? "none"}:${prepared.effectiveModel ?? "unknown"}`;
1919
- const currentAttempts = (emptyResponseAttempts.get(emptyAttemptKey) ?? 0) + 1;
1920
- emptyResponseAttempts.set(emptyAttemptKey, currentAttempts);
1921
- pushDebug(`empty-response: attempt ${currentAttempts}/${maxAttempts}`);
1922
- if (currentAttempts < maxAttempts) {
1923
- await showToast(`Empty response received. Retrying (${currentAttempts}/${maxAttempts})...`, "warning");
1924
- await sleep(retryDelayMs, abortSignal);
1925
- continue; // Retry the endpoint loop
1926
- }
1927
- emptyResponseAttempts.delete(emptyAttemptKey);
1928
- {
1929
- const emptyModel = prepared.effectiveModel ?? "unknown";
1930
- return createSyntheticErrorResponse("[Antigravity] Empty response from " + emptyModel + " after " + currentAttempts + " attempts. Please retry.", prepared.requestedModel);
1931
- }
1932
- }
1933
- const emptyAttemptKeyClean = `${prepared.sessionId ?? "none"}:${prepared.effectiveModel ?? "unknown"}`;
1934
- emptyResponseAttempts.delete(emptyAttemptKeyClean);
1935
- }
1936
- 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, () => { streamFinished = true; }, async () => {
1937
- const watchdogCooldownMs = 120_000; // 2 minute cooldown
1938
- accountManager?.markAccountCoolingDown(account, watchdogCooldownMs, "network-error");
1939
- accountManager?.markRateLimited(account, watchdogCooldownMs, family, headerStyle, model);
1940
- try {
1941
- await accountManager?.saveToDisk();
1942
- }
1943
- catch (e) {
1944
- pushDebug(`watchdog-timeout: failed to save to disk: ${e}`);
1945
- }
1946
- pushDebug(`watchdog-timeout: account ${account.index} penalized for ${watchdogCooldownMs}ms, forcing rotation`);
1947
- showToast(`Stream stalled on ${account.email} - rotating to next account`, "warning").catch(() => { });
1948
- });
1949
- const contextError = transformedResponse.headers.get("x-antigravity-context-error");
1950
- if (contextError) {
1951
- if (contextError === "prompt_too_long") {
1952
- await showToast("Context too long - use /compact to reduce size, or trim your request", "warning");
1953
- }
1954
- else if (contextError === "tool_pairing") {
1955
- await showToast("Tool call/result mismatch - use /compact to fix, or /undo last message", "warning");
1956
- }
1957
- }
1958
- return transformedResponse;
1959
- }
1960
- catch (error) {
1961
- if (selectedProxy) {
1962
- getProxyManager().markCooldown(selectedProxy, 60000);
1963
- }
1964
- if (tokenConsumed) {
1965
- getTokenTracker().refund(account.index);
1966
- tokenConsumed = false;
1967
- }
1968
- if (error instanceof Error && error.name === "AbortError") {
1969
- pushDebug(`fetch-timeout: account ${account.index} timed out after 5 minutes, rotating to next account`);
1970
- accountManager.markAccountCoolingDown(account, 300000, "network-error");
1971
- accountManager.markRateLimited(account, 300000, family, headerStyle, model);
1972
- await showToast(`⏳ Request timed out. Trying next account...`, "warning");
1973
- shouldSwitchAccount = true;
1974
- break; // break out of endpoint loop, go to next account
1975
- }
1976
- if (error instanceof Error && error.message === "THINKING_RECOVERY_NEEDED") {
1977
- if (!forceThinkingRecovery) {
1978
- pushDebug("thinking-recovery: API error detected, retrying with forced recovery");
1979
- forceThinkingRecovery = true;
1980
- i = -1; // Will become 0 after loop increment, restart endpoint loop
1981
- continue;
1982
- }
1983
- const recoveryError = error;
1984
- const originalError = recoveryError.originalError || { error: { message: "Thinking recovery triggered" } };
1985
- const recoveryMessage = `${originalError.error?.message || "Session recovery failed"}\n\n[RECOVERY] Thinking block corruption could not be resolved. Try starting a new session.`;
1986
- return new Response(JSON.stringify({
1987
- type: "error",
1988
- error: {
1989
- type: "unrecoverable_error",
1990
- message: recoveryMessage
1991
- }
1992
- }), {
1993
- status: 400,
1994
- headers: { "Content-Type": "application/json" }
1995
- });
1996
- }
1997
- if (i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
1998
- lastError = error instanceof Error ? error : new Error(String(error));
1999
- continue;
2000
- }
2001
- const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index);
2002
- lastError = error instanceof Error ? error : new Error(String(error));
2003
- if (shouldCooldown) {
2004
- accountManager.markAccountCoolingDown(account, cooldownMs, "network-error");
2005
- accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model);
2006
- pushDebug(`endpoint-error: cooldown ${cooldownMs}ms after ${failures} failures`);
2007
- }
2008
- shouldSwitchAccount = true;
2009
- break;
2010
- }
2011
- }
2012
- } // end headerStyleLoop
2013
- if (shouldSwitchAccount) {
2014
- if (accountCount <= 1) {
2015
- if (lastFailure) {
2016
- 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);
2017
- }
2018
- {
2019
- const msg = (lastError && lastError.message) || "All Antigravity endpoints failed";
2020
- return createSyntheticErrorResponse("[Antigravity] " + msg, model ?? "unknown");
2021
- }
2022
- }
2023
- continue;
2024
- }
2025
- if (lastFailure) {
2026
- 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);
2027
- }
2028
- const msg = lastError?.message ?? "All Antigravity accounts failed";
2029
- return createSyntheticErrorResponse("[Antigravity] " + msg, model ?? "unknown");
2030
- }
2031
- finally {
2032
- if (loopLeasedAccountIndex !== null)
2033
- getLeaseTracker().release(loopLeasedAccountIndex);
2034
- }
2035
- }
2036
- },
2037
- };
2038
- },
2039
- methods: [
2040
- {
2041
- label: "OAuth with Google (Antigravity)",
2042
- type: "oauth",
2043
- authorize: async (inputs) => {
2044
- const isHeadless = !!(process.env.SSH_CONNECTION ||
2045
- process.env.SSH_CLIENT ||
2046
- process.env.SSH_TTY ||
2047
- process.env.OPENCODE_HEADLESS);
2048
- if (inputs) {
2049
- const accounts = [];
2050
- const noBrowser = inputs.noBrowser === "true" || inputs["no-browser"] === "true";
2051
- const useManualMode = noBrowser || shouldSkipLocalServer();
2052
- let startFresh = true;
2053
- let refreshAccountIndex;
2054
- const existingStorage = await loadAccounts();
2055
- if (existingStorage && existingStorage.accounts.length > 0) {
2056
- let menuResult;
2057
- while (true) {
2058
- const now = Date.now();
2059
- const existingAccounts = existingStorage.accounts.map((acc, idx) => {
2060
- let status = 'unknown';
2061
- if (acc.verificationRequired) {
2062
- status = 'verification-required';
2063
- }
2064
- else {
2065
- const rateLimits = acc.rateLimitResetTimes;
2066
- if (rateLimits) {
2067
- const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === 'number' && resetTime > now);
2068
- if (isRateLimited) {
2069
- status = 'rate-limited';
2070
- }
2071
- else {
2072
- status = 'active';
2073
- }
2074
- }
2075
- else {
2076
- status = 'active';
2077
- }
2078
- if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
2079
- status = 'rate-limited';
2080
- }
2081
- }
2082
- return {
2083
- email: acc.email,
2084
- index: idx,
2085
- addedAt: acc.addedAt,
2086
- lastUsed: acc.lastUsed,
2087
- status,
2088
- isCurrentAccount: idx === (existingStorage.activeIndex ?? 0),
2089
- enabled: acc.enabled !== false,
2090
- };
2091
- });
2092
- menuResult = await promptLoginMode(existingAccounts);
2093
- if (menuResult.mode === "check") {
2094
- console.log("\n📊 Checking quotas for all accounts...\n");
2095
- const results = await checkAccountsQuota(existingStorage.accounts, client, providerId);
2096
- let storageUpdated = false;
2097
- for (const res of results) {
2098
- const label = res.email || `Account ${res.index + 1}`;
2099
- const disabledStr = res.disabled ? " (disabled)" : "";
2100
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
2101
- console.log(` ${label}${disabledStr}`);
2102
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
2103
- if (res.status === "error") {
2104
- console.log(` ❌ Error: ${res.error}\n`);
2105
- continue;
2106
- }
2107
- const colors = {
2108
- red: '\x1b[31m',
2109
- orange: '\x1b[33m', // Yellow/orange
2110
- green: '\x1b[32m',
2111
- reset: '\x1b[0m',
2112
- };
2113
- const getColor = (remaining) => {
2114
- if (typeof remaining !== 'number')
2115
- return colors.reset;
2116
- if (remaining < 0.2)
2117
- return colors.red;
2118
- if (remaining < 0.6)
2119
- return colors.orange;
2120
- return colors.green;
2121
- };
2122
- const createProgressBar = (remaining, width = 20) => {
2123
- if (typeof remaining !== 'number')
2124
- return '░'.repeat(width) + ' ???';
2125
- const filled = Math.round(remaining * width);
2126
- const empty = width - filled;
2127
- const color = getColor(remaining);
2128
- const bar = `${color}${'█'.repeat(filled)}${colors.reset}${'░'.repeat(empty)}`;
2129
- const pct = `${color}${Math.round(remaining * 100)}%${colors.reset}`.padStart(4 + color.length + colors.reset.length);
2130
- return `${bar} ${pct}`;
2131
- };
2132
- const formatReset = (resetTime) => {
2133
- if (!resetTime)
2134
- return '';
2135
- const ms = Date.parse(resetTime) - Date.now();
2136
- if (ms <= 0)
2137
- return ' (resetting...)';
2138
- const hours = ms / (1000 * 60 * 60);
2139
- if (hours >= 24) {
2140
- const days = Math.floor(hours / 24);
2141
- const remainingHours = Math.floor(hours % 24);
2142
- if (remainingHours > 0) {
2143
- return ` (resets in ${days}d ${remainingHours}h)`;
2144
- }
2145
- return ` (resets in ${days}d)`;
2146
- }
2147
- return ` (resets in ${formatWaitTime(ms)})`;
2148
- };
2149
- const hasGeminiCli = res.geminiCliQuota && res.geminiCliQuota.models.length > 0;
2150
- console.log(`\n ┌─ Gemini CLI Quota`);
2151
- if (!hasGeminiCli) {
2152
- const errorMsg = res.geminiCliQuota?.error || "No Gemini CLI quota available";
2153
- console.log(` │ └─ ${errorMsg}`);
2154
- }
2155
- else {
2156
- const models = res.geminiCliQuota.models;
2157
- models.forEach((model, idx) => {
2158
- const isLast = idx === models.length - 1;
2159
- const connector = isLast ? "└─" : "├─";
2160
- const bar = createProgressBar(model.remainingFraction);
2161
- const reset = formatReset(model.resetTime);
2162
- const modelName = model.modelId.padEnd(29);
2163
- console.log(` │ ${connector} ${modelName} ${bar}${reset}`);
2164
- });
2165
- }
2166
- const hasAntigravity = res.quota && Object.keys(res.quota.groups).length > 0;
2167
- console.log(` │`);
2168
- console.log(` └─ Antigravity Quota`);
2169
- if (!hasAntigravity) {
2170
- const errorMsg = res.quota?.error || "No quota information available";
2171
- console.log(` └─ ${errorMsg}`);
2172
- }
2173
- else {
2174
- const groups = res.quota.groups;
2175
- const groupEntries = [
2176
- { name: "Claude", data: groups.claude },
2177
- { name: "Gemini 3 Pro", data: groups["gemini-pro"] },
2178
- { name: "Gemini 3 Flash", data: groups["gemini-flash"] },
2179
- ].filter(g => g.data);
2180
- groupEntries.forEach((g, idx) => {
2181
- const isLast = idx === groupEntries.length - 1;
2182
- const connector = isLast ? "└─" : "├─";
2183
- const bar = createProgressBar(g.data.remainingFraction);
2184
- const reset = formatReset(g.data.resetTime);
2185
- const modelName = g.name.padEnd(29);
2186
- console.log(` ${connector} ${modelName} ${bar}${reset}`);
2187
- });
2188
- }
2189
- console.log("");
2190
- if (res.quota?.groups) {
2191
- const acc = existingStorage.accounts[res.index];
2192
- if (acc) {
2193
- acc.cachedQuota = res.quota.groups;
2194
- acc.cachedQuotaUpdatedAt = Date.now();
2195
- storageUpdated = true;
2196
- }
2197
- }
2198
- if (res.updatedAccount) {
2199
- existingStorage.accounts[res.index] = {
2200
- ...res.updatedAccount,
2201
- cachedQuota: res.quota?.groups,
2202
- cachedQuotaUpdatedAt: Date.now(),
2203
- };
2204
- storageUpdated = true;
2205
- }
2206
- }
2207
- if (storageUpdated) {
2208
- await saveAccounts(existingStorage);
2209
- }
2210
- console.log("");
2211
- continue;
2212
- }
2213
- if (menuResult.mode === "manage") {
2214
- if (menuResult.toggleAccountIndex !== undefined) {
2215
- const acc = existingStorage.accounts[menuResult.toggleAccountIndex];
2216
- if (acc) {
2217
- acc.enabled = acc.enabled === false;
2218
- await saveAccounts(existingStorage);
2219
- activeAccountManager?.setAccountEnabled(menuResult.toggleAccountIndex, acc.enabled);
2220
- console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`);
2221
- }
2222
- }
2223
- continue;
2224
- }
2225
- if (menuResult.mode === "proxies") {
2226
- if (menuResult.proxiesAccountIndex !== undefined) {
2227
- const proxyIdx = menuResult.proxiesAccountIndex;
2228
- const acc = existingStorage.accounts[proxyIdx];
2229
- const label = acc?.email || `Account ${proxyIdx + 1}`;
2230
- if (!acc) {
2231
- console.log("\nAccount not found.\n");
2232
- continue;
2233
- }
2234
- while (true) {
2235
- if (!acc.proxies)
2236
- acc.proxies = [];
2237
- const action = await showProxyMenu(label, acc.proxies);
2238
- if (action.action === "back") {
2239
- break;
2240
- }
2241
- else if (action.action === "add") {
2242
- const proxyUrl = await promptProxyUrl();
2243
- if (proxyUrl) {
2244
- if (acc.proxies.includes(proxyUrl)) {
2245
- console.log("\nProxy already exists.\n");
2246
- }
2247
- else {
2248
- acc.proxies.push(proxyUrl);
2249
- await saveAccounts(existingStorage);
2250
- console.log(`\n✓ Added proxy: ${proxyUrl}\n`);
2251
- }
2252
- }
2253
- }
2254
- else if (action.action === "remove") {
2255
- const removed = acc.proxies.splice(action.index, 1)[0];
2256
- await saveAccounts(existingStorage);
2257
- console.log(`\n✓ Removed proxy: ${removed}\n`);
2258
- }
2259
- else if (action.action === "clear") {
2260
- acc.proxies = [];
2261
- await saveAccounts(existingStorage);
2262
- console.log(`\n✓ Cleared all proxies.\n`);
2263
- }
2264
- }
2265
- }
2266
- else {
2267
- const { loadProxyConfig, saveProxyConfig } = await import("../src/plugin/proxy-config");
2268
- const { select } = await import("../src/plugin/ui/select");
2269
- const config = loadProxyConfig();
2270
- const strategy = await select([
2271
- { label: `Automatic${config.strategy === 'automatic' ? ' (current)' : ''}`, value: 'automatic', hint: 'Uses background hot-pool from multiple providers', color: 'cyan' },
2272
- { label: `Manual${config.strategy === 'manual' ? ' (current)' : ''}`, value: 'manual', hint: 'Uses manually added proxies per account', color: 'cyan' },
2273
- { label: `Disabled${config.strategy === 'disabled' ? ' (current)' : ''}`, value: 'disabled', hint: 'Direct connection', color: 'cyan' },
2274
- { label: '', value: 'back', separator: true },
2275
- { label: 'Back', value: 'back', color: 'yellow' }
2276
- ], { message: 'Select Global Proxy Strategy', clearScreen: true });
2277
- if (strategy && strategy !== 'back') {
2278
- config.strategy = strategy;
2279
- saveProxyConfig(config);
2280
- console.log(`\n✓ Proxy strategy updated to: ${strategy}\n`);
2281
- }
2282
- }
2283
- continue;
2284
- }
2285
- if (menuResult.mode === "proxy_providers") {
2286
- const { loadProxyConfig, saveProxyConfig } = await import("../src/plugin/proxy-config");
2287
- const { select } = await import("../src/plugin/ui/select");
2288
- while (true) {
2289
- const config = loadProxyConfig();
2290
- const providerItems = Object.entries(config.providers).map(([name, p]) => ({
2291
- label: name + (p.enabled ? " [\x1b[32mON\x1b[0m]" : " [\x1b[31mOFF\x1b[0m]"),
2292
- hint: p.apiKey ? "(Key Set)" : "(No Key)",
2293
- value: name
2294
- }));
2295
- providerItems.push({ label: "", value: "back", separator: true });
2296
- providerItems.push({ label: "Back", value: "back", color: "yellow" });
2297
- const chosenProvider = await select(providerItems, { message: "Select a Provider to configure", clearScreen: true });
2298
- if (!chosenProvider || chosenProvider === "back")
2299
- break;
2300
- const prov = config.providers[chosenProvider];
2301
- if (prov) {
2302
- const action = await select([
2303
- { label: prov.enabled ? "Disable Provider" : "Enable Provider", value: "toggle" },
2304
- { label: "Set API Key / Proxy List URL", value: "setkey" },
2305
- { label: "Back", value: "back", color: "yellow" }
2306
- ], { message: `Configure ${chosenProvider}`, clearScreen: true });
2307
- if (action === "toggle") {
2308
- prov.enabled = !prov.enabled;
2309
- saveProxyConfig(config);
2310
- }
2311
- else if (action === "setkey") {
2312
- const readline = await import("readline");
2313
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2314
- const newKey = await new Promise(resolve => rl.question('Enter API Key or URL for ' + chosenProvider + ' (leave empty to cancel): ', resolve));
2315
- rl.close();
2316
- if (newKey && newKey.trim()) {
2317
- prov.apiKey = newKey.trim();
2318
- saveProxyConfig(config);
2319
- }
2320
- }
2321
- }
2322
- }
2323
- continue;
2324
- }
2325
- if (menuResult.mode === "verify" || menuResult.mode === "verify-all") {
2326
- const verifyAll = menuResult.mode === "verify-all" || menuResult.verifyAll === true;
2327
- if (verifyAll) {
2328
- if (existingStorage.accounts.length === 0) {
2329
- console.log("\nNo accounts available to verify.\n");
2330
- continue;
2331
- }
2332
- console.log(`\nChecking verification status for ${existingStorage.accounts.length} account(s)...\n`);
2333
- let okCount = 0;
2334
- let blockedCount = 0;
2335
- let errorCount = 0;
2336
- let storageUpdated = false;
2337
- const blockedResults = [];
2338
- for (let i = 0; i < existingStorage.accounts.length; i++) {
2339
- const account = existingStorage.accounts[i];
2340
- if (!account)
2341
- continue;
2342
- const label = account.email || `Account ${i + 1}`;
2343
- process.stdout.write(`- [${i + 1}/${existingStorage.accounts.length}] ${label} ... `);
2344
- const verification = await verifyAccountAccess(account, client, providerId);
2345
- if (verification.status === "ok") {
2346
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2347
- if (changed) {
2348
- storageUpdated = true;
2349
- }
2350
- activeAccountManager?.clearAccountVerificationRequired(i, wasVerificationRequired);
2351
- okCount += 1;
2352
- console.log("ok");
2353
- continue;
2354
- }
2355
- if (verification.status === "blocked") {
2356
- const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2357
- if (changed) {
2358
- storageUpdated = true;
2359
- }
2360
- activeAccountManager?.markAccountVerificationRequired(i, verification.message, verification.verifyUrl);
2361
- blockedCount += 1;
2362
- console.log("needs verification");
2363
- const verifyUrl = verification.verifyUrl ?? account.verificationUrl;
2364
- blockedResults.push({
2365
- label,
2366
- message: verification.message,
2367
- verifyUrl,
2368
- });
2369
- continue;
2370
- }
2371
- errorCount += 1;
2372
- console.log(`error (${verification.message})`);
2373
- }
2374
- if (storageUpdated) {
2375
- await saveAccounts(existingStorage);
2376
- }
2377
- console.log(`\nVerification summary: ${okCount} ready, ${blockedCount} need verification, ${errorCount} errors.`);
2378
- if (blockedResults.length > 0) {
2379
- console.log("\nAccounts needing verification:");
2380
- for (const result of blockedResults) {
2381
- console.log(`\n- ${result.label}`);
2382
- console.log(` ${result.message}`);
2383
- if (result.verifyUrl) {
2384
- console.log(` URL: ${result.verifyUrl}`);
2385
- }
2386
- else {
2387
- console.log(" URL: not provided by API response");
2388
- }
2389
- }
2390
- console.log("");
2391
- }
2392
- else {
2393
- console.log("");
2394
- }
2395
- continue;
2396
- }
2397
- let verifyAccountIndex = menuResult.verifyAccountIndex;
2398
- if (verifyAccountIndex === undefined) {
2399
- verifyAccountIndex = await promptAccountIndexForVerification(existingAccounts);
2400
- }
2401
- if (verifyAccountIndex === undefined) {
2402
- console.log("\nVerification cancelled.\n");
2403
- continue;
2404
- }
2405
- const account = existingStorage.accounts[verifyAccountIndex];
2406
- if (!account) {
2407
- console.log(`\nAccount ${verifyAccountIndex + 1} not found.\n`);
2408
- continue;
2409
- }
2410
- const label = account.email || `Account ${verifyAccountIndex + 1}`;
2411
- console.log(`\nChecking verification status for ${label}...\n`);
2412
- const verification = await verifyAccountAccess(account, client, providerId);
2413
- if (verification.status === "ok") {
2414
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2415
- if (changed) {
2416
- await saveAccounts(existingStorage);
2417
- }
2418
- activeAccountManager?.clearAccountVerificationRequired(verifyAccountIndex, wasVerificationRequired);
2419
- if (wasVerificationRequired) {
2420
- console.log(`✓ ${label} is ready for requests and has been re-enabled.\n`);
2421
- }
2422
- else {
2423
- console.log(`✓ ${label} is ready for requests.\n`);
2424
- }
2425
- continue;
2426
- }
2427
- if (verification.status === "blocked") {
2428
- const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2429
- if (changed) {
2430
- await saveAccounts(existingStorage);
2431
- }
2432
- activeAccountManager?.markAccountVerificationRequired(verifyAccountIndex, verification.message, verification.verifyUrl);
2433
- const verifyUrl = verification.verifyUrl ?? account.verificationUrl;
2434
- console.log(`⚠ ${label} needs Google verification before it can be used.`);
2435
- if (verification.message) {
2436
- console.log(verification.message);
2437
- }
2438
- console.log(`${label} has been disabled until verification is completed.`);
2439
- if (verifyUrl) {
2440
- console.log(`\nVerification URL:\n${verifyUrl}\n`);
2441
- if (await promptOpenVerificationUrl()) {
2442
- const opened = await openBrowser(verifyUrl);
2443
- if (opened) {
2444
- console.log("Opened verification URL in your browser.\n");
2445
- }
2446
- else {
2447
- console.log("Could not open browser automatically. Please open the URL manually.\n");
2448
- }
2449
- }
2450
- }
2451
- else {
2452
- console.log("No verification URL was returned. Try re-authenticating this account.\n");
2453
- }
2454
- continue;
2455
- }
2456
- console.log(`✗ ${label}: ${verification.message}\n`);
2457
- continue;
2458
- }
2459
- break;
2460
- }
2461
- if (menuResult.mode === "cancel") {
2462
- return {
2463
- url: "",
2464
- instructions: "Authentication cancelled",
2465
- method: "auto",
2466
- callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
2467
- };
2468
- }
2469
- if (menuResult.deleteAccountIndex !== undefined) {
2470
- const updatedAccounts = existingStorage.accounts.filter((_, idx) => idx !== menuResult.deleteAccountIndex);
2471
- await saveAccountsReplace({
2472
- version: 4,
2473
- accounts: updatedAccounts,
2474
- activeIndex: 0,
2475
- activeIndexByFamily: { claude: 0, gemini: 0 },
2476
- });
2477
- activeAccountManager?.removeAccountByIndex(menuResult.deleteAccountIndex);
2478
- console.log("\nAccount deleted.\n");
2479
- if (updatedAccounts.length > 0) {
2480
- const fallbackAccount = updatedAccounts[0];
2481
- if (fallbackAccount?.refreshToken) {
2482
- const fallbackResult = buildAuthSuccessFromStoredAccount(fallbackAccount);
2483
- try {
2484
- await client.auth.set({
2485
- path: { id: providerId },
2486
- body: { type: "oauth", refresh: fallbackResult.refresh, access: "", expires: 0 },
2487
- });
2488
- }
2489
- catch (storeError) {
2490
- log.error("Failed to update stored Antigravity OAuth credentials", { error: String(storeError) });
2491
- }
2492
- const label = fallbackAccount.email || `Account ${1}`;
2493
- return {
2494
- url: "",
2495
- instructions: `Account deleted. Using ${label} for future requests.`,
2496
- method: "auto",
2497
- callback: async () => fallbackResult,
2498
- };
2499
- }
2500
- }
2501
- try {
2502
- await client.auth.set({
2503
- path: { id: providerId },
2504
- body: { type: "oauth", refresh: "", access: "", expires: 0 },
2505
- });
2506
- }
2507
- catch (storeError) {
2508
- log.error("Failed to clear stored Antigravity OAuth credentials", { error: String(storeError) });
2509
- }
2510
- return {
2511
- url: "",
2512
- instructions: "All accounts deleted. Run `opencode auth login` to reauthenticate.",
2513
- method: "auto",
2514
- callback: async () => ({
2515
- type: "failed",
2516
- error: "All accounts deleted. Reauthentication required.",
2517
- }),
2518
- };
2519
- }
2520
- if (menuResult.refreshAccountIndex !== undefined) {
2521
- refreshAccountIndex = menuResult.refreshAccountIndex;
2522
- const refreshEmail = existingStorage.accounts[refreshAccountIndex]?.email;
2523
- console.log(`\nRe-authenticating ${refreshEmail || 'account'}...\n`);
2524
- startFresh = false;
2525
- }
2526
- if (menuResult.deleteAll) {
2527
- await clearAccounts();
2528
- console.log("\nAll accounts deleted.\n");
2529
- startFresh = true;
2530
- try {
2531
- await client.auth.set({
2532
- path: { id: providerId },
2533
- body: { type: "oauth", refresh: "", access: "", expires: 0 },
2534
- });
2535
- }
2536
- catch (storeError) {
2537
- log.error("Failed to clear stored Antigravity OAuth credentials", { error: String(storeError) });
2538
- }
2539
- }
2540
- else {
2541
- startFresh = menuResult.mode === "fresh";
2542
- }
2543
- if (startFresh && !menuResult.deleteAll) {
2544
- console.log("\nStarting fresh - existing accounts will be replaced.\n");
2545
- }
2546
- else if (!startFresh) {
2547
- console.log("\nAdding to existing accounts.\n");
2548
- }
2549
- }
2550
- while (accounts.length < MAX_OAUTH_ACCOUNTS) {
2551
- console.log(`\n=== Antigravity OAuth (Account ${accounts.length + 1}) ===`);
2552
- const projectId = await promptProjectId();
2553
- const result = await (async () => {
2554
- const authorization = await authorizeAntigravity(projectId);
2555
- const fallbackState = getStateFromAuthorizationUrl(authorization.url);
2556
- console.log("\nOAuth URL:\n" + authorization.url + "\n");
2557
- if (useManualMode) {
2558
- const browserOpened = await openBrowser(authorization.url);
2559
- if (!browserOpened) {
2560
- console.log("Could not open browser automatically.");
2561
- console.log("Please open the URL above manually in your local browser.\n");
2562
- }
2563
- return promptManualOAuthInput(fallbackState);
2564
- }
2565
- let listener = null;
2566
- if (!isHeadless) {
2567
- try {
2568
- listener = await startOAuthListener();
2569
- }
2570
- catch {
2571
- listener = null;
2572
- }
2573
- }
2574
- if (!isHeadless) {
2575
- await openBrowser(authorization.url);
2576
- }
2577
- if (listener) {
2578
- try {
2579
- const SOFT_TIMEOUT_MS = 30000;
2580
- const callbackPromise = listener.waitForCallback();
2581
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("SOFT_TIMEOUT")), SOFT_TIMEOUT_MS));
2582
- let callbackUrl;
2583
- try {
2584
- callbackUrl = await Promise.race([callbackPromise, timeoutPromise]);
2585
- }
2586
- catch (err) {
2587
- if (err instanceof Error && err.message === "SOFT_TIMEOUT") {
2588
- console.log("\n⏳ Automatic callback not received after 30 seconds.");
2589
- console.log("You can paste the redirect URL manually.\n");
2590
- console.log("OAuth URL (in case you need it again):");
2591
- console.log(authorization.url + "\n");
2592
- try {
2593
- await listener.close();
2594
- }
2595
- catch { }
2596
- return promptManualOAuthInput(fallbackState);
2597
- }
2598
- throw err;
2599
- }
2600
- const params = extractOAuthCallbackParams(callbackUrl);
2601
- if (!params) {
2602
- return { type: "failed", error: "Missing code or state in callback URL" };
2603
- }
2604
- return exchangeAntigravity(params.code, params.state);
2605
- }
2606
- catch (error) {
2607
- if (error instanceof Error && error.message !== "SOFT_TIMEOUT") {
2608
- return {
2609
- type: "failed",
2610
- error: error.message,
2611
- };
2612
- }
2613
- return {
2614
- type: "failed",
2615
- error: error instanceof Error ? error.message : "Unknown error",
2616
- };
2617
- }
2618
- finally {
2619
- try {
2620
- await listener.close();
2621
- }
2622
- catch { }
2623
- }
2624
- }
2625
- return promptManualOAuthInput(fallbackState);
2626
- })();
2627
- if (result.type === "failed") {
2628
- if (accounts.length === 0) {
2629
- return {
2630
- url: "",
2631
- instructions: `Authentication failed: ${result.error}`,
2632
- method: "auto",
2633
- callback: async () => result,
2634
- };
2635
- }
2636
- console.warn(`[opencode-antigravity-auth] Skipping failed account ${accounts.length + 1}: ${result.error}`);
2637
- break;
2638
- }
2639
- accounts.push(result);
2640
- try {
2641
- await client.tui.showToast({
2642
- body: {
2643
- message: `Account ${accounts.length} authenticated${result.email ? ` (${result.email})` : ""}`,
2644
- variant: "success",
2645
- },
2646
- });
2647
- }
2648
- catch {
2649
- }
2650
- try {
2651
- if (refreshAccountIndex !== undefined) {
2652
- const currentStorage = await loadAccounts();
2653
- if (currentStorage) {
2654
- const updatedAccounts = [...currentStorage.accounts];
2655
- const parts = parseRefreshParts(result.refresh);
2656
- if (parts.refreshToken) {
2657
- updatedAccounts[refreshAccountIndex] = {
2658
- email: result.email ?? updatedAccounts[refreshAccountIndex]?.email,
2659
- refreshToken: parts.refreshToken,
2660
- projectId: parts.projectId ?? updatedAccounts[refreshAccountIndex]?.projectId,
2661
- managedProjectId: parts.managedProjectId ?? updatedAccounts[refreshAccountIndex]?.managedProjectId,
2662
- addedAt: updatedAccounts[refreshAccountIndex]?.addedAt ?? Date.now(),
2663
- lastUsed: Date.now(),
2664
- };
2665
- await saveAccounts({
2666
- version: 4,
2667
- accounts: updatedAccounts,
2668
- activeIndex: currentStorage.activeIndex,
2669
- activeIndexByFamily: currentStorage.activeIndexByFamily,
2670
- });
2671
- }
2672
- }
2673
- }
2674
- else {
2675
- const isFirstAccount = accounts.length === 1;
2676
- await persistAccountPool([result], isFirstAccount && startFresh);
2677
- }
2678
- }
2679
- catch {
2680
- }
2681
- if (refreshAccountIndex !== undefined) {
2682
- break;
2683
- }
2684
- if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
2685
- break;
2686
- }
2687
- let currentAccountCount = accounts.length;
2688
- try {
2689
- const currentStorage = await loadAccounts();
2690
- if (currentStorage) {
2691
- currentAccountCount = currentStorage.accounts.length;
2692
- }
2693
- }
2694
- catch {
2695
- }
2696
- const addAnother = await promptAddAnotherAccount(currentAccountCount);
2697
- if (!addAnother) {
2698
- break;
2699
- }
2700
- }
2701
- const primary = accounts[0];
2702
- if (!primary) {
2703
- return {
2704
- url: "",
2705
- instructions: "Authentication cancelled",
2706
- method: "auto",
2707
- callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
2708
- };
2709
- }
2710
- let actualAccountCount = accounts.length;
2711
- try {
2712
- const finalStorage = await loadAccounts();
2713
- if (finalStorage) {
2714
- actualAccountCount = finalStorage.accounts.length;
2715
- }
2716
- }
2717
- catch {
2718
- }
2719
- const successMessage = refreshAccountIndex !== undefined
2720
- ? `Token refreshed successfully.`
2721
- : `Multi-account setup complete (${actualAccountCount} account(s)).`;
2722
- return {
2723
- url: "",
2724
- instructions: successMessage,
2725
- method: "auto",
2726
- callback: async () => primary,
2727
- };
2728
- }
2729
- const projectId = "";
2730
- const existingStorage = await loadAccounts();
2731
- const existingCount = existingStorage?.accounts.length ?? 0;
2732
- const useManualFlow = isHeadless || shouldSkipLocalServer();
2733
- let listener = null;
2734
- if (!useManualFlow) {
2735
- try {
2736
- listener = await startOAuthListener();
2737
- }
2738
- catch {
2739
- listener = null;
2740
- }
2741
- }
2742
- const authorization = await authorizeAntigravity(projectId);
2743
- const fallbackState = getStateFromAuthorizationUrl(authorization.url);
2744
- if (!useManualFlow) {
2745
- const browserOpened = await openBrowser(authorization.url);
2746
- if (!browserOpened) {
2747
- listener?.close().catch(() => { });
2748
- listener = null;
2749
- }
2750
- }
2751
- if (listener) {
2752
- return {
2753
- url: authorization.url,
2754
- instructions: "Complete sign-in in your browser. We'll automatically detect the redirect back to localhost.",
2755
- method: "auto",
2756
- callback: async () => {
2757
- const CALLBACK_TIMEOUT_MS = 30000;
2758
- try {
2759
- const callbackPromise = listener.waitForCallback();
2760
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("CALLBACK_TIMEOUT")), CALLBACK_TIMEOUT_MS));
2761
- let callbackUrl;
2762
- try {
2763
- callbackUrl = await Promise.race([callbackPromise, timeoutPromise]);
2764
- }
2765
- catch (err) {
2766
- if (err instanceof Error && err.message === "CALLBACK_TIMEOUT") {
2767
- return {
2768
- type: "failed",
2769
- error: "Callback timeout - please use CLI with --no-browser flag for manual input",
2770
- };
2771
- }
2772
- throw err;
2773
- }
2774
- const params = extractOAuthCallbackParams(callbackUrl);
2775
- if (!params) {
2776
- return { type: "failed", error: "Missing code or state in callback URL" };
2777
- }
2778
- const result = await exchangeAntigravity(params.code, params.state);
2779
- if (result.type === "success") {
2780
- try {
2781
- await persistAccountPool([result], false);
2782
- }
2783
- catch {
2784
- }
2785
- const newTotal = existingCount + 1;
2786
- const toastMessage = existingCount > 0
2787
- ? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
2788
- : `Authenticated${result.email ? ` (${result.email})` : ""}`;
2789
- try {
2790
- await client.tui.showToast({
2791
- body: {
2792
- message: toastMessage,
2793
- variant: "success",
2794
- },
2795
- });
2796
- }
2797
- catch {
2798
- }
2799
- }
2800
- return result;
2801
- }
2802
- catch (error) {
2803
- return {
2804
- type: "failed",
2805
- error: error instanceof Error ? error.message : "Unknown error",
2806
- };
2807
- }
2808
- finally {
2809
- try {
2810
- await listener.close();
2811
- }
2812
- catch {
2813
- }
2814
- }
2815
- },
2816
- };
2817
- }
2818
- return {
2819
- url: authorization.url,
2820
- instructions: "Visit the URL above, complete OAuth, then paste either the full redirect URL or the authorization code.",
2821
- method: "code",
2822
- callback: async (codeInput) => {
2823
- const params = parseOAuthCallbackInput(codeInput, fallbackState);
2824
- if ("error" in params) {
2825
- return { type: "failed", error: params.error };
2826
- }
2827
- const result = await exchangeAntigravity(params.code, params.state);
2828
- if (result.type === "success") {
2829
- try {
2830
- await persistAccountPool([result], false);
2831
- }
2832
- catch {
2833
- }
2834
- const newTotal = existingCount + 1;
2835
- const toastMessage = existingCount > 0
2836
- ? `Added account${result.email ? ` (${result.email})` : ""} - ${newTotal} total`
2837
- : `Authenticated${result.email ? ` (${result.email})` : ""}`;
2838
- try {
2839
- await client.tui.showToast({
2840
- body: {
2841
- message: toastMessage,
2842
- variant: "success",
2843
- },
2844
- });
2845
- }
2846
- catch {
2847
- }
2848
- }
2849
- return result;
2850
- },
2851
- };
2852
- },
2853
- },
2854
- {
2855
- label: "Manually enter API Key",
2856
- type: "api",
2857
- },
2858
- ],
2859
- },
2860
- };
2861
- };
2862
- export const AntigravityCLIOAuthPlugin = createAntigravityPlugin(ANTIGRAVITY_PROVIDER_ID);
2863
- export const GoogleOAuthPlugin = AntigravityCLIOAuthPlugin;
2864
- function toUrlString(value) {
2865
- if (typeof value === "string") {
2866
- return value;
2867
- }
2868
- const candidate = value.url;
2869
- if (candidate) {
2870
- return candidate;
2871
- }
2872
- return value.toString();
2873
- }
2874
- function toWarmupStreamUrl(value) {
2875
- const urlString = toUrlString(value);
2876
- try {
2877
- const url = new URL(urlString);
2878
- if (!url.pathname.includes(":streamGenerateContent")) {
2879
- url.pathname = url.pathname.replace(":generateContent", ":streamGenerateContent");
2880
- }
2881
- url.searchParams.set("alt", "sse");
2882
- return url.toString();
2883
- }
2884
- catch {
2885
- return urlString;
2886
- }
2887
- }
2888
- function extractModelFromUrl(urlString) {
2889
- const match = urlString.match(/\/models\/([^:\/?]+)(?::\w+)?/);
2890
- return match?.[1] ?? null;
2891
- }
2892
- function extractModelFromUrlWithSuffix(urlString) {
2893
- const match = urlString.match(/\/models\/([^:\/\?]+)/);
2894
- return match?.[1] ?? null;
2895
- }
2896
- function getModelFamilyFromUrl(urlString) {
2897
- const model = extractModelFromUrl(urlString);
2898
- let family = "gemini";
2899
- if (model && model.includes("claude")) {
2900
- family = "claude";
2901
- }
2902
- if (isDebugEnabled()) {
2903
- logModelFamily(urlString, model, family);
2904
- }
2905
- return family;
2906
- }
2907
- function resolveQuotaFallbackHeaderStyle(input) {
2908
- if (input.family !== "gemini") {
2909
- return null;
2910
- }
2911
- if (!input.alternateStyle || input.alternateStyle === input.headerStyle) {
2912
- return null;
2913
- }
2914
- return input.alternateStyle;
2915
- }
2916
- function resolveHeaderRoutingDecision(urlString, family, config) {
2917
- const cliFirst = getCliFirst(config);
2918
- const preferredHeaderStyle = getHeaderStyleFromUrl(urlString, family, cliFirst);
2919
- const explicitQuota = isExplicitQuotaFromUrl(urlString);
2920
- return {
2921
- cliFirst,
2922
- preferredHeaderStyle,
2923
- explicitQuota,
2924
- allowQuotaFallback: family === "gemini",
2925
- };
2926
- }
2927
- function getCliFirst(config) {
2928
- return config.cli_first ?? false;
2929
- }
2930
- function getHeaderStyleFromUrl(urlString, family, cliFirst = false) {
2931
- if (family === "claude") {
2932
- return "antigravity";
2933
- }
2934
- const modelWithSuffix = extractModelFromUrlWithSuffix(urlString);
2935
- if (!modelWithSuffix) {
2936
- return cliFirst ? "gemini-cli" : "antigravity";
2937
- }
2938
- const { quotaPreference } = resolveModelWithTier(modelWithSuffix, { cli_first: cliFirst });
2939
- return quotaPreference ?? "antigravity";
2940
- }
2941
- function isExplicitQuotaFromUrl(urlString) {
2942
- const modelWithSuffix = extractModelFromUrlWithSuffix(urlString);
2943
- if (!modelWithSuffix) {
2944
- return false;
2945
- }
2946
- const { explicitQuota } = resolveModelWithTier(modelWithSuffix);
2947
- return explicitQuota ?? false;
2948
- }
2949
- export const __testExports = {
2950
- getHeaderStyleFromUrl,
2951
- resolveHeaderRoutingDecision,
2952
- resolveQuotaFallbackHeaderStyle,
2953
- };
2954
- import { getLeaseTracker, getProxyManager } from "../src/plugin/rotation";