antigravity-auth 1.6.0 → 1.7.0

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 +2868 -0
  3. package/dist/handler.js +25119 -0
  4. package/dist/index.js +25110 -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,966 +0,0 @@
1
- import { formatRefreshParts, parseRefreshParts } from "./auth";
2
- import { loadAccounts, saveAccounts } from "./storage";
3
- import { getHealthTracker, getTokenTracker, selectHybridAccount } from "./rotation";
4
- import { generateFingerprint, updateFingerprintVersion, MAX_FINGERPRINT_HISTORY } from "./fingerprint";
5
- import { getModelFamily } from "./transform/model-resolver";
6
- import { debugLogToFile } from "./debug";
7
- import { formatAccountLabel } from "./logging-utils";
8
- const QUOTA_EXHAUSTED_BACKOFFS = [60_000, 300_000, 1_800_000, 7_200_000];
9
- const RATE_LIMIT_EXCEEDED_BACKOFF = 30_000;
10
- const MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF = 45_000;
11
- const MODEL_CAPACITY_EXHAUSTED_JITTER_MAX = 30_000; // ±15s jitter range
12
- const SERVER_ERROR_BACKOFF = 20_000;
13
- const UNKNOWN_BACKOFF = 60_000;
14
- const MIN_BACKOFF_MS = 2_000;
15
- /**
16
- * Generate a random jitter value for backoff timing.
17
- * Helps prevent thundering herd problem when multiple clients retry simultaneously.
18
- */
19
- function generateJitter(maxJitterMs) {
20
- return Math.random() * maxJitterMs - (maxJitterMs / 2);
21
- }
22
- export function parseRateLimitReason(reason, message, status) {
23
- // 1. Status Code Checks (Rust parity)
24
- // 529 = Site Overloaded, 503 = Service Unavailable -> Capacity issues
25
- if (status === 529 || status === 503)
26
- return "MODEL_CAPACITY_EXHAUSTED";
27
- // 500 = Internal Server Error -> Treat as Server Error (soft wait)
28
- if (status === 500)
29
- return "SERVER_ERROR";
30
- // 2. Explicit Reason String
31
- if (reason) {
32
- switch (reason.toUpperCase()) {
33
- case "QUOTA_EXHAUSTED": return "QUOTA_EXHAUSTED";
34
- case "RATE_LIMIT_EXCEEDED": return "RATE_LIMIT_EXCEEDED";
35
- case "MODEL_CAPACITY_EXHAUSTED": return "MODEL_CAPACITY_EXHAUSTED";
36
- }
37
- }
38
- // 3. Message Text Scanning (Rust Regex parity)
39
- if (message) {
40
- const lower = message.toLowerCase();
41
- if (lower.includes("capacity") || lower.includes("overloaded") || lower.includes("resource exhausted")) {
42
- return "MODEL_CAPACITY_EXHAUSTED";
43
- }
44
- // "per minute", "rate limit", "too many requests"
45
- // "presque" (French: almost) - retained for i18n parity with Rust reference
46
- if (lower.includes("per minute") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("presque")) {
47
- return "RATE_LIMIT_EXCEEDED";
48
- }
49
- if (lower.includes("exhausted") || lower.includes("quota")) {
50
- return "QUOTA_EXHAUSTED";
51
- }
52
- }
53
- if (status === 429) {
54
- return "UNKNOWN";
55
- }
56
- return "UNKNOWN";
57
- }
58
- export function calculateBackoffMs(reason, consecutiveFailures, retryAfterMs) {
59
- if (retryAfterMs && retryAfterMs > 0) {
60
- return Math.max(retryAfterMs, MIN_BACKOFF_MS);
61
- }
62
- let baseBackoff = UNKNOWN_BACKOFF;
63
- switch (reason) {
64
- case "QUOTA_EXHAUSTED": {
65
- const index = Math.min(consecutiveFailures, QUOTA_EXHAUSTED_BACKOFFS.length - 1);
66
- return QUOTA_EXHAUSTED_BACKOFFS[index] ?? UNKNOWN_BACKOFF;
67
- }
68
- case "RATE_LIMIT_EXCEEDED":
69
- baseBackoff = 45_000; // Increased base default wait time
70
- break;
71
- case "MODEL_CAPACITY_EXHAUSTED":
72
- baseBackoff = MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF + generateJitter(MODEL_CAPACITY_EXHAUSTED_JITTER_MAX);
73
- break;
74
- case "SERVER_ERROR":
75
- baseBackoff = 30_000; // Increased base default
76
- break;
77
- case "UNKNOWN":
78
- default:
79
- baseBackoff = 90_000; // Increased base default
80
- break;
81
- }
82
- const MAX_EXPONENTIAL_BACKOFF = 60 * 60 * 1000;
83
- const multiplier = Math.pow(1.5, consecutiveFailures);
84
- return Math.min(Math.round(baseBackoff * multiplier), MAX_EXPONENTIAL_BACKOFF);
85
- }
86
- function nowMs() {
87
- return Date.now();
88
- }
89
- function clampNonNegativeInt(value, fallback) {
90
- if (typeof value !== "number" || !Number.isFinite(value)) {
91
- return fallback;
92
- }
93
- return value < 0 ? 0 : Math.floor(value);
94
- }
95
- function getQuotaKey(family, headerStyle, model) {
96
- if (family === "claude") {
97
- return "claude";
98
- }
99
- const base = headerStyle === "gemini-cli" ? "gemini-cli" : "gemini-antigravity";
100
- if (model) {
101
- return `${base}:${model}`;
102
- }
103
- return base;
104
- }
105
- function isRateLimitedForQuotaKey(account, key) {
106
- const resetTime = account.rateLimitResetTimes[key];
107
- return resetTime !== undefined && nowMs() < resetTime;
108
- }
109
- function isRateLimitedForFamily(account, family, model) {
110
- if (family === "claude") {
111
- return isRateLimitedForQuotaKey(account, "claude");
112
- }
113
- const antigravityIsLimited = isRateLimitedForHeaderStyle(account, family, "antigravity", model);
114
- const cliIsLimited = isRateLimitedForHeaderStyle(account, family, "gemini-cli", model);
115
- return antigravityIsLimited && cliIsLimited;
116
- }
117
- function isRateLimitedForHeaderStyle(account, family, headerStyle, model) {
118
- clearExpiredRateLimits(account);
119
- if (family === "claude") {
120
- return isRateLimitedForQuotaKey(account, "claude");
121
- }
122
- if (model) {
123
- const modelKey = getQuotaKey(family, headerStyle, model);
124
- if (isRateLimitedForQuotaKey(account, modelKey)) {
125
- return true;
126
- }
127
- }
128
- const baseKey = getQuotaKey(family, headerStyle);
129
- return isRateLimitedForQuotaKey(account, baseKey);
130
- }
131
- function clearExpiredRateLimits(account) {
132
- const now = nowMs();
133
- const keys = Object.keys(account.rateLimitResetTimes);
134
- for (const key of keys) {
135
- const resetTime = account.rateLimitResetTimes[key];
136
- if (resetTime !== undefined && now >= resetTime) {
137
- delete account.rateLimitResetTimes[key];
138
- }
139
- }
140
- }
141
- /**
142
- * Resolve the quota group for soft quota checks.
143
- *
144
- * When a model string is available, we can precisely determine the quota group.
145
- * When model is null/undefined, we fall back based on family:
146
- * - Claude → "claude" quota group
147
- * - Gemini → "gemini-pro" (conservative fallback; may misclassify flash models)
148
- *
149
- * @param family - The model family ("claude" | "gemini")
150
- * @param model - Optional model string for precise resolution
151
- * @returns The QuotaGroup to use for soft quota checks
152
- */
153
- export function resolveQuotaGroup(family, model) {
154
- if (model) {
155
- return getModelFamily(model);
156
- }
157
- return family === "claude" ? "claude" : "gemini-pro";
158
- }
159
- function isOverSoftQuotaThreshold(account, family, thresholdPercent, cacheTtlMs, model) {
160
- if (thresholdPercent >= 100)
161
- return false;
162
- if (!account.cachedQuota)
163
- return false;
164
- if (account.cachedQuotaUpdatedAt == null)
165
- return false;
166
- const age = nowMs() - account.cachedQuotaUpdatedAt;
167
- if (age > cacheTtlMs)
168
- return false;
169
- const quotaGroup = resolveQuotaGroup(family, model);
170
- const groupData = account.cachedQuota[quotaGroup];
171
- if (groupData?.remainingFraction == null)
172
- return false;
173
- const remainingFraction = Math.max(0, Math.min(1, groupData.remainingFraction));
174
- const usedPercent = (1 - remainingFraction) * 100;
175
- const isOverThreshold = usedPercent >= thresholdPercent;
176
- if (isOverThreshold) {
177
- const accountLabel = formatAccountLabel(account.email, account.index);
178
- const resetSuffix = groupData.resetTime ? ` (resets: ${groupData.resetTime})` : "";
179
- const message = `[SoftQuota] Skipping ${accountLabel}: ${quotaGroup} usage ${usedPercent.toFixed(1)}% >= threshold ${thresholdPercent}%${resetSuffix}`;
180
- debugLogToFile(message);
181
- }
182
- return isOverThreshold;
183
- }
184
- export function computeSoftQuotaCacheTtlMs(ttlConfig, refreshIntervalMinutes) {
185
- if (ttlConfig === "auto") {
186
- return Math.max(2 * refreshIntervalMinutes, 10) * 60 * 1000;
187
- }
188
- return ttlConfig * 60 * 1000;
189
- }
190
- /**
191
- * In-memory multi-account manager with sticky account selection.
192
- *
193
- * Uses the same account until it hits a rate limit (429), then switches.
194
- * Rate limits are tracked per-model-family (claude/gemini) so an account
195
- * rate-limited for Claude can still be used for Gemini.
196
- *
197
- * Source of truth for the pool is `antigravity-accounts.json`.
198
- */
199
- export class AccountManager {
200
- accounts = [];
201
- cursor = 0;
202
- currentAccountIndexByFamily = {
203
- claude: -1,
204
- gemini: -1,
205
- };
206
- sessionOffsetApplied = {
207
- claude: false,
208
- gemini: false,
209
- };
210
- lastToastAccountIndex = -1;
211
- lastToastTime = 0;
212
- savePending = false;
213
- saveTimeout = null;
214
- savePromiseResolvers = [];
215
- static async loadFromDisk(authFallback) {
216
- const stored = await loadAccounts();
217
- return new AccountManager(authFallback, stored);
218
- }
219
- constructor(authFallback, stored) {
220
- const authParts = authFallback ? parseRefreshParts(authFallback.refresh) : null;
221
- if (stored && stored.accounts.length === 0) {
222
- this.accounts = [];
223
- this.cursor = 0;
224
- return;
225
- }
226
- if (stored && stored.accounts.length > 0) {
227
- const baseNow = nowMs();
228
- this.accounts = stored.accounts
229
- .map((acc, index) => {
230
- if (!acc.refreshToken || typeof acc.refreshToken !== "string") {
231
- return null;
232
- }
233
- const matchesFallback = !!(authFallback &&
234
- authParts &&
235
- authParts.refreshToken &&
236
- acc.refreshToken === authParts.refreshToken);
237
- return {
238
- index,
239
- email: acc.email,
240
- proxies: acc.proxies,
241
- addedAt: clampNonNegativeInt(acc.addedAt, baseNow),
242
- lastUsed: clampNonNegativeInt(acc.lastUsed, 0),
243
- parts: {
244
- refreshToken: acc.refreshToken,
245
- projectId: acc.projectId,
246
- managedProjectId: acc.managedProjectId,
247
- },
248
- access: matchesFallback ? authFallback?.access : undefined,
249
- expires: matchesFallback ? authFallback?.expires : undefined,
250
- enabled: acc.enabled !== false,
251
- rateLimitResetTimes: acc.rateLimitResetTimes ?? {},
252
- lastSwitchReason: acc.lastSwitchReason,
253
- coolingDownUntil: acc.coolingDownUntil,
254
- cooldownReason: acc.cooldownReason,
255
- touchedForQuota: {},
256
- fingerprint: acc.fingerprint ?? generateFingerprint(),
257
- fingerprintHistory: acc.fingerprintHistory ?? [],
258
- cachedQuota: acc.cachedQuota,
259
- cachedQuotaUpdatedAt: acc.cachedQuotaUpdatedAt,
260
- verificationRequired: acc.verificationRequired,
261
- verificationRequiredAt: acc.verificationRequiredAt,
262
- verificationRequiredReason: acc.verificationRequiredReason,
263
- verificationUrl: acc.verificationUrl,
264
- };
265
- })
266
- .filter((a) => a !== null);
267
- let fingerprintVersionChanged = false;
268
- for (const acc of this.accounts) {
269
- if (acc.fingerprint && updateFingerprintVersion(acc.fingerprint)) {
270
- fingerprintVersionChanged = true;
271
- }
272
- }
273
- this.cursor = clampNonNegativeInt(stored.activeIndex, 0);
274
- if (this.accounts.length > 0) {
275
- this.cursor = this.cursor % this.accounts.length;
276
- const defaultIndex = this.cursor;
277
- this.currentAccountIndexByFamily.claude = clampNonNegativeInt(stored.activeIndexByFamily?.claude, defaultIndex) % this.accounts.length;
278
- this.currentAccountIndexByFamily.gemini = clampNonNegativeInt(stored.activeIndexByFamily?.gemini, defaultIndex) % this.accounts.length;
279
- }
280
- if (fingerprintVersionChanged) {
281
- this.requestSaveToDisk();
282
- }
283
- return;
284
- }
285
- if (authFallback && this.accounts.length > 0) {
286
- const authParts = parseRefreshParts(authFallback.refresh);
287
- const hasMatching = this.accounts.some(acc => acc.parts.refreshToken === authParts.refreshToken);
288
- if (!hasMatching && authParts.refreshToken) {
289
- const now = nowMs();
290
- const newAccount = {
291
- index: this.accounts.length,
292
- email: undefined,
293
- addedAt: now,
294
- lastUsed: 0,
295
- parts: authParts,
296
- access: authFallback.access,
297
- expires: authFallback.expires,
298
- enabled: true,
299
- rateLimitResetTimes: {},
300
- touchedForQuota: {},
301
- };
302
- this.accounts.push(newAccount);
303
- this.currentAccountIndexByFamily.claude = Math.min(this.currentAccountIndexByFamily.claude, this.accounts.length - 1);
304
- this.currentAccountIndexByFamily.gemini = Math.min(this.currentAccountIndexByFamily.gemini, this.accounts.length - 1);
305
- }
306
- }
307
- if (authFallback) {
308
- const parts = parseRefreshParts(authFallback.refresh);
309
- if (parts.refreshToken) {
310
- const now = nowMs();
311
- this.accounts = [
312
- {
313
- index: 0,
314
- email: undefined,
315
- addedAt: now,
316
- lastUsed: 0,
317
- parts,
318
- access: authFallback.access,
319
- expires: authFallback.expires,
320
- enabled: true,
321
- rateLimitResetTimes: {},
322
- touchedForQuota: {},
323
- },
324
- ];
325
- this.cursor = 0;
326
- this.currentAccountIndexByFamily.claude = 0;
327
- this.currentAccountIndexByFamily.gemini = 0;
328
- }
329
- }
330
- }
331
- getAccountCount() {
332
- return this.getEnabledAccounts().length;
333
- }
334
- getTotalAccountCount() {
335
- return this.accounts.length;
336
- }
337
- getEnabledAccounts() {
338
- return this.accounts.filter((account) => account.enabled !== false);
339
- }
340
- getAccountsSnapshot() {
341
- return this.accounts.map((a) => ({ ...a, parts: { ...a.parts }, rateLimitResetTimes: { ...a.rateLimitResetTimes } }));
342
- }
343
- getCurrentAccountForFamily(family) {
344
- const currentIndex = this.currentAccountIndexByFamily[family];
345
- if (currentIndex >= 0 && currentIndex < this.accounts.length) {
346
- const account = this.accounts[currentIndex] ?? null;
347
- if (account && account.enabled !== false) {
348
- return account;
349
- }
350
- }
351
- return null;
352
- }
353
- markSwitched(account, reason, family) {
354
- account.lastSwitchReason = reason;
355
- this.currentAccountIndexByFamily[family] = account.index;
356
- }
357
- /**
358
- * Check if we should show an account switch toast.
359
- * Debounces repeated toasts for the same account.
360
- */
361
- shouldShowAccountToast(accountIndex, debounceMs = 30000) {
362
- const now = nowMs();
363
- if (accountIndex !== this.lastToastAccountIndex) {
364
- return true;
365
- }
366
- return now - this.lastToastTime >= debounceMs;
367
- }
368
- markToastShown(accountIndex) {
369
- this.lastToastAccountIndex = accountIndex;
370
- this.lastToastTime = nowMs();
371
- }
372
- getCurrentOrNextForFamily(family, model, strategy = 'sticky', headerStyle = 'antigravity', pidOffsetEnabled = false, softQuotaThresholdPercent = 100, softQuotaCacheTtlMs = 10 * 60 * 1000) {
373
- const quotaKey = getQuotaKey(family, headerStyle, model);
374
- if (strategy === 'round-robin') {
375
- const next = this.getNextForFamily(family, model, headerStyle, softQuotaThresholdPercent, softQuotaCacheTtlMs);
376
- if (next) {
377
- this.markTouchedForQuota(next, quotaKey);
378
- this.currentAccountIndexByFamily[family] = next.index;
379
- }
380
- return next;
381
- }
382
- if (strategy === 'hybrid') {
383
- const healthTracker = getHealthTracker();
384
- const tokenTracker = getTokenTracker();
385
- const accountsWithMetrics = this.accounts
386
- .filter(acc => acc.enabled !== false)
387
- .map(acc => {
388
- clearExpiredRateLimits(acc);
389
- return {
390
- index: acc.index,
391
- lastUsed: acc.lastUsed,
392
- healthScore: healthTracker.getScore(acc.index),
393
- isRateLimited: isRateLimitedForFamily(acc, family, model) ||
394
- isOverSoftQuotaThreshold(acc, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model),
395
- isCoolingDown: this.isAccountCoolingDown(acc),
396
- };
397
- });
398
- const currentIndex = this.currentAccountIndexByFamily[family] ?? null;
399
- const selectedIndex = selectHybridAccount(accountsWithMetrics, tokenTracker, currentIndex);
400
- if (selectedIndex !== null) {
401
- const selected = this.accounts[selectedIndex];
402
- if (selected) {
403
- selected.lastUsed = nowMs();
404
- this.markTouchedForQuota(selected, quotaKey);
405
- this.currentAccountIndexByFamily[family] = selected.index;
406
- return selected;
407
- }
408
- }
409
- }
410
- if (pidOffsetEnabled && !this.sessionOffsetApplied[family] && this.accounts.length > 1) {
411
- const pidOffset = process.pid % this.accounts.length;
412
- const baseIndex = this.currentAccountIndexByFamily[family] ?? 0;
413
- const newIndex = (baseIndex + pidOffset) % this.accounts.length;
414
- debugLogToFile(`[Account] Applying PID offset: pid=${process.pid} offset=${pidOffset} family=${family} index=${baseIndex}->${newIndex}`);
415
- this.currentAccountIndexByFamily[family] = newIndex;
416
- this.sessionOffsetApplied[family] = true;
417
- }
418
- const current = this.getCurrentAccountForFamily(family);
419
- if (current) {
420
- clearExpiredRateLimits(current);
421
- const isLimitedForRequestedStyle = isRateLimitedForHeaderStyle(current, family, headerStyle, model);
422
- const isOverThreshold = isOverSoftQuotaThreshold(current, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model);
423
- if (!isLimitedForRequestedStyle && !isOverThreshold && !this.isAccountCoolingDown(current)) {
424
- this.markTouchedForQuota(current, quotaKey);
425
- return current;
426
- }
427
- }
428
- const next = this.getNextForFamily(family, model, headerStyle, softQuotaThresholdPercent, softQuotaCacheTtlMs);
429
- if (next) {
430
- this.markTouchedForQuota(next, quotaKey);
431
- this.currentAccountIndexByFamily[family] = next.index;
432
- }
433
- return next;
434
- }
435
- getNextForFamily(family, model, headerStyle = "antigravity", softQuotaThresholdPercent = 100, softQuotaCacheTtlMs = 10 * 60 * 1000) {
436
- const available = this.accounts.filter((a) => {
437
- clearExpiredRateLimits(a);
438
- return a.enabled !== false &&
439
- !isRateLimitedForHeaderStyle(a, family, headerStyle, model) &&
440
- !isOverSoftQuotaThreshold(a, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model) &&
441
- !this.isAccountCoolingDown(a);
442
- });
443
- console.log("getNextForFamily: available count=", available.length);
444
- if (available.length === 0) {
445
- return null;
446
- }
447
- const account = available[this.cursor % available.length];
448
- if (!account) {
449
- return null;
450
- }
451
- this.cursor++;
452
- return account;
453
- }
454
- markRateLimited(account, retryAfterMs, family, headerStyle = "antigravity", model) {
455
- const key = getQuotaKey(family, headerStyle, model);
456
- account.rateLimitResetTimes[key] = nowMs() + retryAfterMs;
457
- }
458
- /**
459
- * Mark an account as used after a successful API request.
460
- * This updates the lastUsed timestamp for freshness calculations.
461
- * Should be called AFTER request completion, not during account selection.
462
- */
463
- markAccountUsed(accountIndex) {
464
- const account = this.accounts.find(a => a.index === accountIndex);
465
- if (account) {
466
- account.lastUsed = nowMs();
467
- }
468
- }
469
- markRateLimitedWithReason(account, family, headerStyle, model, reason = "UNKNOWN", retryAfterMs, ttlMs) {
470
- const now = nowMs();
471
- const failures = (account.consecutiveFailures ?? 0) + 1;
472
- account.consecutiveFailures = failures;
473
- account.lastFailureTime = now;
474
- const smartBackoffMs = calculateBackoffMs(reason, failures - 1, retryAfterMs);
475
- const baseRetryMs = retryAfterMs && retryAfterMs > 0 ? Math.max(retryAfterMs, MIN_BACKOFF_MS) : 0;
476
- const backoffMs = Math.max(smartBackoffMs, baseRetryMs * Math.pow(1.5, failures - 1));
477
- const key = getQuotaKey(family, headerStyle, model);
478
- account.rateLimitResetTimes[key] = now + backoffMs;
479
- return backoffMs;
480
- }
481
- markRequestSuccess(account) {
482
- if (account.consecutiveFailures) {
483
- account.consecutiveFailures = 0;
484
- }
485
- }
486
- clearAllRateLimitsForFamily(family, model) {
487
- for (const account of this.accounts) {
488
- if (family === "claude") {
489
- delete account.rateLimitResetTimes.claude;
490
- }
491
- else {
492
- const antigravityKey = getQuotaKey(family, "antigravity", model);
493
- const cliKey = getQuotaKey(family, "gemini-cli", model);
494
- delete account.rateLimitResetTimes[antigravityKey];
495
- delete account.rateLimitResetTimes[cliKey];
496
- }
497
- account.consecutiveFailures = 0;
498
- }
499
- }
500
- shouldTryOptimisticReset(family, model) {
501
- const minWaitMs = this.getMinWaitTimeForFamily(family, model);
502
- return minWaitMs > 0 && minWaitMs <= 2_000;
503
- }
504
- markAccountCoolingDown(account, cooldownMs, reason) {
505
- account.coolingDownUntil = nowMs() + cooldownMs;
506
- account.cooldownReason = reason;
507
- }
508
- isAccountCoolingDown(account) {
509
- if (account.coolingDownUntil === undefined) {
510
- return false;
511
- }
512
- if (nowMs() >= account.coolingDownUntil) {
513
- this.clearAccountCooldown(account);
514
- return false;
515
- }
516
- return true;
517
- }
518
- clearAccountCooldown(account) {
519
- delete account.coolingDownUntil;
520
- delete account.cooldownReason;
521
- }
522
- getAccountCooldownReason(account) {
523
- return this.isAccountCoolingDown(account) ? account.cooldownReason : undefined;
524
- }
525
- markTouchedForQuota(account, quotaKey) {
526
- account.touchedForQuota[quotaKey] = nowMs();
527
- }
528
- isFreshForQuota(account, quotaKey) {
529
- const touchedAt = account.touchedForQuota[quotaKey];
530
- if (!touchedAt)
531
- return true;
532
- const resetTime = account.rateLimitResetTimes[quotaKey];
533
- if (resetTime && touchedAt < resetTime)
534
- return true;
535
- return false;
536
- }
537
- getFreshAccountsForQuota(quotaKey, family, model) {
538
- return this.accounts.filter(acc => {
539
- clearExpiredRateLimits(acc);
540
- return acc.enabled !== false &&
541
- this.isFreshForQuota(acc, quotaKey) &&
542
- !isRateLimitedForFamily(acc, family, model) &&
543
- !this.isAccountCoolingDown(acc);
544
- });
545
- }
546
- isRateLimitedForHeaderStyle(account, family, headerStyle, model) {
547
- return isRateLimitedForHeaderStyle(account, family, headerStyle, model);
548
- }
549
- getAvailableHeaderStyle(account, family, model) {
550
- clearExpiredRateLimits(account);
551
- if (family === "claude") {
552
- return isRateLimitedForHeaderStyle(account, family, "antigravity") ? null : "antigravity";
553
- }
554
- if (!isRateLimitedForHeaderStyle(account, family, "antigravity", model)) {
555
- return "antigravity";
556
- }
557
- if (!isRateLimitedForHeaderStyle(account, family, "gemini-cli", model)) {
558
- return "gemini-cli";
559
- }
560
- return null;
561
- }
562
- /**
563
- * Check if any OTHER account has antigravity quota available for the given family/model.
564
- *
565
- * Used to determine whether to switch accounts vs fall back to gemini-cli:
566
- * - If true: Switch to another account (preserve antigravity priority)
567
- * - If false: All accounts exhausted antigravity, safe to fall back to gemini-cli
568
- *
569
- * @param currentAccountIndex - Index of the current account (will be excluded from check)
570
- * @param family - Model family ("gemini" or "claude")
571
- * @param model - Optional model name for model-specific rate limits
572
- * @returns true if any other enabled, non-cooling-down account has antigravity available
573
- */
574
- hasOtherAccountWithAntigravityAvailable(currentAccountIndex, family, model) {
575
- // (This method is only relevant for Gemini's dual quota pools)
576
- if (family === "claude") {
577
- return false;
578
- }
579
- return this.accounts.some(acc => {
580
- if (acc.index === currentAccountIndex) {
581
- return false;
582
- }
583
- if (acc.enabled === false) {
584
- return false;
585
- }
586
- if (this.isAccountCoolingDown(acc)) {
587
- return false;
588
- }
589
- clearExpiredRateLimits(acc);
590
- return !isRateLimitedForHeaderStyle(acc, family, "antigravity", model);
591
- });
592
- }
593
- setAccountEnabled(accountIndex, enabled) {
594
- const account = this.accounts[accountIndex];
595
- if (!account) {
596
- return false;
597
- }
598
- account.enabled = enabled;
599
- if (!enabled) {
600
- for (const family of Object.keys(this.currentAccountIndexByFamily)) {
601
- if (this.currentAccountIndexByFamily[family] === accountIndex) {
602
- const next = this.accounts.find((a, i) => i !== accountIndex && a.enabled !== false);
603
- this.currentAccountIndexByFamily[family] = next?.index ?? -1;
604
- }
605
- }
606
- }
607
- this.requestSaveToDisk();
608
- return true;
609
- }
610
- markAccountVerificationRequired(accountIndex, reason, verifyUrl) {
611
- const account = this.accounts[accountIndex];
612
- if (!account) {
613
- return false;
614
- }
615
- account.verificationRequired = true;
616
- account.verificationRequiredAt = nowMs();
617
- account.verificationRequiredReason = reason?.trim() || undefined;
618
- const normalizedVerifyUrl = verifyUrl?.trim();
619
- if (normalizedVerifyUrl) {
620
- account.verificationUrl = normalizedVerifyUrl;
621
- }
622
- if (account.enabled !== false) {
623
- this.setAccountEnabled(accountIndex, false);
624
- }
625
- else {
626
- this.requestSaveToDisk();
627
- }
628
- return true;
629
- }
630
- clearAccountVerificationRequired(accountIndex, enableAccount = false) {
631
- const account = this.accounts[accountIndex];
632
- if (!account) {
633
- return false;
634
- }
635
- const wasVerificationRequired = account.verificationRequired === true;
636
- const hadMetadata = (account.verificationRequiredAt !== undefined ||
637
- account.verificationRequiredReason !== undefined ||
638
- account.verificationUrl !== undefined);
639
- account.verificationRequired = false;
640
- account.verificationRequiredAt = undefined;
641
- account.verificationRequiredReason = undefined;
642
- account.verificationUrl = undefined;
643
- if (enableAccount && wasVerificationRequired && account.enabled === false) {
644
- this.setAccountEnabled(accountIndex, true);
645
- }
646
- else if (wasVerificationRequired || hadMetadata) {
647
- this.requestSaveToDisk();
648
- }
649
- return true;
650
- }
651
- removeAccountByIndex(accountIndex) {
652
- if (accountIndex < 0 || accountIndex >= this.accounts.length) {
653
- return false;
654
- }
655
- const account = this.accounts[accountIndex];
656
- if (!account) {
657
- return false;
658
- }
659
- return this.removeAccount(account);
660
- }
661
- removeAccount(account) {
662
- const idx = this.accounts.indexOf(account);
663
- if (idx < 0) {
664
- return false;
665
- }
666
- this.accounts.splice(idx, 1);
667
- this.accounts.forEach((acc, index) => {
668
- acc.index = index;
669
- });
670
- if (this.accounts.length === 0) {
671
- this.cursor = 0;
672
- this.currentAccountIndexByFamily.claude = -1;
673
- this.currentAccountIndexByFamily.gemini = -1;
674
- return true;
675
- }
676
- if (this.cursor > idx) {
677
- this.cursor -= 1;
678
- }
679
- this.cursor = this.cursor % this.accounts.length;
680
- for (const family of ["claude", "gemini"]) {
681
- if (this.currentAccountIndexByFamily[family] > idx) {
682
- this.currentAccountIndexByFamily[family] -= 1;
683
- }
684
- if (this.currentAccountIndexByFamily[family] >= this.accounts.length) {
685
- this.currentAccountIndexByFamily[family] = -1;
686
- }
687
- }
688
- return true;
689
- }
690
- updateFromAuth(account, auth) {
691
- const parts = parseRefreshParts(auth.refresh);
692
- account.parts = {
693
- ...parts,
694
- projectId: parts.projectId ?? account.parts.projectId,
695
- managedProjectId: parts.managedProjectId ?? account.parts.managedProjectId,
696
- };
697
- account.access = auth.access;
698
- account.expires = auth.expires;
699
- }
700
- toAuthDetails(account) {
701
- return {
702
- type: "oauth",
703
- refresh: formatRefreshParts(account.parts),
704
- access: account.access,
705
- expires: account.expires,
706
- };
707
- }
708
- getMinWaitTimeForFamily(family, model, headerStyle, strict) {
709
- const available = this.accounts.filter((a) => {
710
- clearExpiredRateLimits(a);
711
- return a.enabled !== false && !this.isAccountCoolingDown(a) && (strict && headerStyle
712
- ? !isRateLimitedForHeaderStyle(a, family, headerStyle, model)
713
- : !isRateLimitedForFamily(a, family, model));
714
- });
715
- if (available.length > 0) {
716
- return 0;
717
- }
718
- const waitTimes = [];
719
- for (const a of this.accounts) {
720
- if (a.enabled === false)
721
- continue;
722
- const coolWait = a.coolingDownUntil ? Math.max(0, a.coolingDownUntil - nowMs()) : 0;
723
- let rateWait = Infinity;
724
- if (family === "claude") {
725
- const t = a.rateLimitResetTimes.claude;
726
- if (t !== undefined)
727
- rateWait = Math.max(0, t - nowMs());
728
- }
729
- else if (strict && headerStyle) {
730
- const key = getQuotaKey(family, headerStyle, model);
731
- const t = a.rateLimitResetTimes[key];
732
- if (t !== undefined)
733
- rateWait = Math.max(0, t - nowMs());
734
- }
735
- else {
736
- const antigravityKey = getQuotaKey(family, "antigravity", model);
737
- const cliKey = getQuotaKey(family, "gemini-cli", model);
738
- const t1 = a.rateLimitResetTimes[antigravityKey];
739
- const t2 = a.rateLimitResetTimes[cliKey];
740
- rateWait = Math.min(t1 !== undefined ? Math.max(0, t1 - nowMs()) : Infinity, t2 !== undefined ? Math.max(0, t2 - nowMs()) : Infinity);
741
- }
742
- const totalWait = Math.max(coolWait, rateWait === Infinity ? 0 : rateWait);
743
- if (totalWait > 0) {
744
- waitTimes.push(totalWait);
745
- }
746
- }
747
- return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
748
- }
749
- getAccounts() {
750
- return [...this.accounts];
751
- }
752
- async saveToDisk() {
753
- const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude);
754
- const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini);
755
- const storage = {
756
- version: 4,
757
- accounts: this.accounts.map((a) => ({
758
- email: a.email,
759
- proxies: a.proxies,
760
- refreshToken: a.parts.refreshToken,
761
- projectId: a.parts.projectId,
762
- managedProjectId: a.parts.managedProjectId,
763
- addedAt: a.addedAt,
764
- lastUsed: a.lastUsed,
765
- enabled: a.enabled,
766
- lastSwitchReason: a.lastSwitchReason,
767
- rateLimitResetTimes: Object.keys(a.rateLimitResetTimes).length > 0 ? a.rateLimitResetTimes : undefined,
768
- coolingDownUntil: a.coolingDownUntil,
769
- cooldownReason: a.cooldownReason,
770
- fingerprint: a.fingerprint,
771
- fingerprintHistory: a.fingerprintHistory?.length ? a.fingerprintHistory : undefined,
772
- cachedQuota: a.cachedQuota && Object.keys(a.cachedQuota).length > 0 ? a.cachedQuota : undefined,
773
- cachedQuotaUpdatedAt: a.cachedQuotaUpdatedAt,
774
- verificationRequired: a.verificationRequired,
775
- verificationRequiredAt: a.verificationRequiredAt,
776
- verificationRequiredReason: a.verificationRequiredReason,
777
- verificationUrl: a.verificationUrl,
778
- })),
779
- activeIndex: claudeIndex,
780
- activeIndexByFamily: {
781
- claude: claudeIndex,
782
- gemini: geminiIndex,
783
- },
784
- };
785
- await saveAccounts(storage);
786
- }
787
- requestSaveToDisk() {
788
- if (this.savePending) {
789
- return;
790
- }
791
- this.savePending = true;
792
- this.saveTimeout = setTimeout(() => {
793
- void this.executeSave();
794
- }, 1000);
795
- }
796
- async flushSaveToDisk() {
797
- if (!this.savePending) {
798
- return;
799
- }
800
- return new Promise((resolve) => {
801
- this.savePromiseResolvers.push(resolve);
802
- });
803
- }
804
- async executeSave() {
805
- this.savePending = false;
806
- this.saveTimeout = null;
807
- try {
808
- await this.saveToDisk();
809
- }
810
- catch {
811
- }
812
- finally {
813
- const resolvers = this.savePromiseResolvers;
814
- this.savePromiseResolvers = [];
815
- for (const resolve of resolvers) {
816
- resolve();
817
- }
818
- }
819
- }
820
- // ========== Fingerprint Management ==========
821
- /**
822
- * Regenerate fingerprint for an account, saving the old one to history.
823
- * @param accountIndex - Index of the account to regenerate fingerprint for
824
- * @returns The new fingerprint, or null if account not found
825
- */
826
- regenerateAccountFingerprint(accountIndex) {
827
- const account = this.accounts[accountIndex];
828
- if (!account)
829
- return null;
830
- if (account.fingerprint) {
831
- const historyEntry = {
832
- fingerprint: account.fingerprint,
833
- timestamp: nowMs(),
834
- reason: 'regenerated',
835
- };
836
- if (!account.fingerprintHistory) {
837
- account.fingerprintHistory = [];
838
- }
839
- account.fingerprintHistory.unshift(historyEntry);
840
- if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
841
- account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
842
- }
843
- }
844
- account.fingerprint = generateFingerprint();
845
- this.requestSaveToDisk();
846
- return account.fingerprint;
847
- }
848
- /**
849
- * Restore a fingerprint from history for an account.
850
- * @param accountIndex - Index of the account
851
- * @param historyIndex - Index in the fingerprint history to restore from (0 = most recent)
852
- * @returns The restored fingerprint, or null if account/history not found
853
- */
854
- restoreAccountFingerprint(accountIndex, historyIndex) {
855
- const account = this.accounts[accountIndex];
856
- if (!account)
857
- return null;
858
- const history = account.fingerprintHistory;
859
- if (!history || historyIndex < 0 || historyIndex >= history.length) {
860
- return null;
861
- }
862
- const fingerprintToRestore = history[historyIndex].fingerprint;
863
- if (account.fingerprint) {
864
- const historyEntry = {
865
- fingerprint: account.fingerprint,
866
- timestamp: nowMs(),
867
- reason: 'restored',
868
- };
869
- account.fingerprintHistory.unshift(historyEntry);
870
- if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
871
- account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
872
- }
873
- }
874
- account.fingerprint = { ...fingerprintToRestore, createdAt: nowMs() };
875
- this.requestSaveToDisk();
876
- return account.fingerprint;
877
- }
878
- /**
879
- * Get fingerprint history for an account.
880
- * @param accountIndex - Index of the account
881
- * @returns Array of fingerprint versions, or empty array if not found
882
- */
883
- getAccountFingerprintHistory(accountIndex) {
884
- const account = this.accounts[accountIndex];
885
- if (!account || !account.fingerprintHistory) {
886
- return [];
887
- }
888
- return [...account.fingerprintHistory];
889
- }
890
- updateQuotaCache(accountIndex, quotaGroups) {
891
- const account = this.accounts[accountIndex];
892
- if (account) {
893
- account.cachedQuota = quotaGroups;
894
- account.cachedQuotaUpdatedAt = nowMs();
895
- }
896
- }
897
- isAccountOverSoftQuota(account, family, thresholdPercent, cacheTtlMs, model) {
898
- return isOverSoftQuotaThreshold(account, family, thresholdPercent, cacheTtlMs, model);
899
- }
900
- getAccountsForQuotaCheck() {
901
- return this.accounts.map((a) => ({
902
- email: a.email,
903
- refreshToken: a.parts.refreshToken,
904
- projectId: a.parts.projectId,
905
- managedProjectId: a.parts.managedProjectId,
906
- addedAt: a.addedAt,
907
- lastUsed: a.lastUsed,
908
- enabled: a.enabled,
909
- }));
910
- }
911
- getOldestQuotaCacheAge() {
912
- let oldest = null;
913
- for (const acc of this.accounts) {
914
- if (acc.enabled === false)
915
- continue;
916
- if (acc.cachedQuotaUpdatedAt == null)
917
- return null;
918
- const age = nowMs() - acc.cachedQuotaUpdatedAt;
919
- if (oldest === null || age > oldest)
920
- oldest = age;
921
- }
922
- return oldest;
923
- }
924
- areAllAccountsOverSoftQuota(family, thresholdPercent, cacheTtlMs, model) {
925
- if (thresholdPercent >= 100)
926
- return false;
927
- const enabled = this.accounts.filter(a => a.enabled !== false);
928
- if (enabled.length === 0)
929
- return false;
930
- return enabled.every(a => isOverSoftQuotaThreshold(a, family, thresholdPercent, cacheTtlMs, model));
931
- }
932
- /**
933
- * Get minimum wait time until any account's soft quota resets.
934
- * Returns 0 if any account is available (not over threshold).
935
- * Returns the minimum resetTime across all over-threshold accounts.
936
- * Returns null if no resetTime data is available.
937
- */
938
- getMinWaitTimeForSoftQuota(family, thresholdPercent, cacheTtlMs, model) {
939
- if (thresholdPercent >= 100)
940
- return 0;
941
- const enabled = this.accounts.filter(a => a.enabled !== false);
942
- if (enabled.length === 0)
943
- return null;
944
- const available = enabled.filter(a => !isOverSoftQuotaThreshold(a, family, thresholdPercent, cacheTtlMs, model));
945
- if (available.length > 0)
946
- return 0;
947
- if (!model && family !== "claude")
948
- return null;
949
- const quotaGroup = resolveQuotaGroup(family, model);
950
- const now = nowMs();
951
- const waitTimes = [];
952
- for (const acc of enabled) {
953
- const groupData = acc.cachedQuota?.[quotaGroup];
954
- if (groupData?.resetTime) {
955
- const resetTimestamp = Date.parse(groupData.resetTime);
956
- if (Number.isFinite(resetTimestamp)) {
957
- waitTimes.push(Math.max(0, resetTimestamp - now));
958
- }
959
- }
960
- }
961
- if (waitTimes.length === 0)
962
- return null;
963
- const minWait = Math.min(...waitTimes);
964
- return minWait === 0 ? null : minWait;
965
- }
966
- }