antigravity-claude-proxy 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -14,6 +14,7 @@ import {
14
14
  import { refreshAccessToken } from '../auth/oauth.js';
15
15
  import { getAuthStatus } from '../auth/database.js';
16
16
  import { logger } from '../utils/logger.js';
17
+ import { isNetworkError } from '../utils/helpers.js';
17
18
 
18
19
  /**
19
20
  * Get OAuth token for an account
@@ -48,6 +49,13 @@ export async function getTokenForAccount(account, tokenCache, onInvalid, onSave)
48
49
  }
49
50
  logger.success(`[AccountManager] Refreshed OAuth token for: ${account.email}`);
50
51
  } catch (error) {
52
+ // Check if it's a transient network error
53
+ if (isNetworkError(error)) {
54
+ logger.warn(`[AccountManager] Failed to refresh token for ${account.email} due to network error: ${error.message}`);
55
+ // Do NOT mark as invalid, just throw so caller knows it failed
56
+ throw new Error(`AUTH_NETWORK_ERROR: ${error.message}`);
57
+ }
58
+
51
59
  logger.error(`[AccountManager] Failed to refresh token for ${account.email}:`, error.message);
52
60
  // Mark account as invalid (credentials need re-auth)
53
61
  if (onInvalid) onInvalid(account.email, error.message);
@@ -123,14 +131,20 @@ export async function discoverProject(token) {
123
131
  })
124
132
  });
125
133
 
126
- if (!response.ok) continue;
134
+ if (!response.ok) {
135
+ const errorText = await response.text();
136
+ logger.warn(`[AccountManager] Project discovery failed at ${endpoint}: ${response.status} - ${errorText}`);
137
+ continue;
138
+ }
127
139
 
128
140
  const data = await response.json();
129
141
 
130
142
  if (typeof data.cloudaicompanionProject === 'string') {
143
+ logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject}`);
131
144
  return data.cloudaicompanionProject;
132
145
  }
133
146
  if (data.cloudaicompanionProject?.id) {
147
+ logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject.id}`);
134
148
  return data.cloudaicompanionProject.id;
135
149
  }
136
150
  } catch (error) {
@@ -138,7 +152,8 @@ export async function discoverProject(token) {
138
152
  }
139
153
  }
140
154
 
141
- logger.info(`[AccountManager] Using default project: ${DEFAULT_PROJECT_ID}`);
155
+ logger.warn(`[AccountManager] Project discovery failed for all endpoints. Using default project: ${DEFAULT_PROJECT_ID}`);
156
+ logger.warn(`[AccountManager] If you see 404 errors, your account may not have Gemini Code Assist enabled.`);
142
157
  return DEFAULT_PROJECT_ID;
143
158
  }
144
159
 
@@ -81,18 +81,20 @@ export class AccountManager {
81
81
 
82
82
  /**
83
83
  * Check if all accounts are rate-limited
84
+ * @param {string} [modelId] - Optional model ID
84
85
  * @returns {boolean} True if all accounts are rate-limited
85
86
  */
86
- isAllRateLimited() {
87
- return checkAllRateLimited(this.#accounts);
87
+ isAllRateLimited(modelId = null) {
88
+ return checkAllRateLimited(this.#accounts, modelId);
88
89
  }
89
90
 
90
91
  /**
91
92
  * Get list of available (non-rate-limited, non-invalid) accounts
93
+ * @param {string} [modelId] - Optional model ID
92
94
  * @returns {Array<Object>} Array of available account objects
93
95
  */
94
- getAvailableAccounts() {
95
- return getAvailable(this.#accounts);
96
+ getAvailableAccounts(modelId = null) {
97
+ return getAvailable(this.#accounts, modelId);
96
98
  }
97
99
 
98
100
  /**
@@ -127,10 +129,11 @@ export class AccountManager {
127
129
  /**
128
130
  * Pick the next available account (fallback when current is unavailable).
129
131
  * Sets activeIndex to the selected account's index.
132
+ * @param {string} [modelId] - Optional model ID
130
133
  * @returns {Object|null} The next available account or null if none available
131
134
  */
132
- pickNext() {
133
- const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk());
135
+ pickNext(modelId = null) {
136
+ const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
134
137
  this.#currentIndex = newIndex;
135
138
  return account;
136
139
  }
@@ -138,10 +141,11 @@ export class AccountManager {
138
141
  /**
139
142
  * Get the current account without advancing the index (sticky selection).
140
143
  * Used for cache continuity - sticks to the same account until rate-limited.
144
+ * @param {string} [modelId] - Optional model ID
141
145
  * @returns {Object|null} The current account or null if unavailable/rate-limited
142
146
  */
143
- getCurrentStickyAccount() {
144
- const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
147
+ getCurrentStickyAccount(modelId = null) {
148
+ const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
145
149
  this.#currentIndex = newIndex;
146
150
  return account;
147
151
  }
@@ -149,10 +153,11 @@ export class AccountManager {
149
153
  /**
150
154
  * Check if we should wait for the current account's rate limit to reset.
151
155
  * Used for sticky account selection - wait if rate limit is short (≤ threshold).
156
+ * @param {string} [modelId] - Optional model ID
152
157
  * @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
153
158
  */
154
- shouldWaitForCurrentAccount() {
155
- return shouldWait(this.#accounts, this.#currentIndex);
159
+ shouldWaitForCurrentAccount(modelId = null) {
160
+ return shouldWait(this.#accounts, this.#currentIndex, modelId);
156
161
  }
157
162
 
158
163
  /**
@@ -160,10 +165,11 @@ export class AccountManager {
160
165
  * Prefers the current account for cache continuity, only switches when:
161
166
  * - Current account is rate-limited for > 2 minutes
162
167
  * - Current account is invalid
168
+ * @param {string} [modelId] - Optional model ID
163
169
  * @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
164
170
  */
165
- pickStickyAccount() {
166
- const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
171
+ pickStickyAccount(modelId = null) {
172
+ const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
167
173
  this.#currentIndex = newIndex;
168
174
  return { account, waitMs };
169
175
  }
@@ -172,9 +178,10 @@ export class AccountManager {
172
178
  * Mark an account as rate-limited
173
179
  * @param {string} email - Email of the account to mark
174
180
  * @param {number|null} resetMs - Time in ms until rate limit resets (optional)
181
+ * @param {string} [modelId] - Optional model ID to mark specific limit
175
182
  */
176
- markRateLimited(email, resetMs = null) {
177
- markLimited(this.#accounts, email, resetMs, this.#settings);
183
+ markRateLimited(email, resetMs = null, modelId = null) {
184
+ markLimited(this.#accounts, email, resetMs, this.#settings, modelId);
178
185
  this.saveToDisk();
179
186
  }
180
187
 
@@ -190,10 +197,11 @@ export class AccountManager {
190
197
 
191
198
  /**
192
199
  * Get the minimum wait time until any account becomes available
200
+ * @param {string} [modelId] - Optional model ID
193
201
  * @returns {number} Wait time in milliseconds
194
202
  */
195
- getMinWaitTimeMs() {
196
- return getMinWait(this.#accounts);
203
+ getMinWaitTimeMs(modelId = null) {
204
+ return getMinWait(this.#accounts, modelId);
197
205
  }
198
206
 
199
207
  /**
@@ -251,9 +259,16 @@ export class AccountManager {
251
259
  */
252
260
  getStatus() {
253
261
  const available = this.getAvailableAccounts();
254
- const rateLimited = this.#accounts.filter(a => a.isRateLimited);
255
262
  const invalid = this.getInvalidAccounts();
256
263
 
264
+ // Count accounts that have any active model-specific rate limits
265
+ const rateLimited = this.#accounts.filter(a => {
266
+ if (!a.modelRateLimits) return false;
267
+ return Object.values(a.modelRateLimits).some(
268
+ limit => limit.isRateLimited && limit.resetTime > Date.now()
269
+ );
270
+ });
271
+
257
272
  return {
258
273
  total: this.#accounts.length,
259
274
  available: available.length,
@@ -263,8 +278,7 @@ export class AccountManager {
263
278
  accounts: this.#accounts.map(a => ({
264
279
  email: a.email,
265
280
  source: a.source,
266
- isRateLimited: a.isRateLimited,
267
- rateLimitResetTime: a.rateLimitResetTime,
281
+ modelRateLimits: a.modelRateLimits || {},
268
282
  isInvalid: a.isInvalid || false,
269
283
  invalidReason: a.invalidReason || null,
270
284
  lastUsed: a.lastUsed
@@ -2,6 +2,7 @@
2
2
  * Rate Limit Management
3
3
  *
4
4
  * Handles rate limit tracking and state management for accounts.
5
+ * All rate limits are model-specific.
5
6
  */
6
7
 
7
8
  import { DEFAULT_COOLDOWN_MS } from '../constants.js';
@@ -9,24 +10,44 @@ import { formatDuration } from '../utils/helpers.js';
9
10
  import { logger } from '../utils/logger.js';
10
11
 
11
12
  /**
12
- * Check if all accounts are rate-limited
13
+ * Check if all accounts are rate-limited for a specific model
13
14
  *
14
15
  * @param {Array} accounts - Array of account objects
16
+ * @param {string} modelId - Model ID to check rate limits for
15
17
  * @returns {boolean} True if all accounts are rate-limited
16
18
  */
17
- export function isAllRateLimited(accounts) {
19
+ export function isAllRateLimited(accounts, modelId) {
18
20
  if (accounts.length === 0) return true;
19
- return accounts.every(acc => acc.isRateLimited);
21
+ if (!modelId) return false; // No model specified = not rate limited
22
+
23
+ return accounts.every(acc => {
24
+ if (acc.isInvalid) return true; // Invalid accounts count as unavailable
25
+ const modelLimits = acc.modelRateLimits || {};
26
+ const limit = modelLimits[modelId];
27
+ return limit && limit.isRateLimited && limit.resetTime > Date.now();
28
+ });
20
29
  }
21
30
 
22
31
  /**
23
- * Get list of available (non-rate-limited, non-invalid) accounts
32
+ * Get list of available (non-rate-limited, non-invalid) accounts for a model
24
33
  *
25
34
  * @param {Array} accounts - Array of account objects
35
+ * @param {string} [modelId] - Model ID to filter by
26
36
  * @returns {Array} Array of available account objects
27
37
  */
28
- export function getAvailableAccounts(accounts) {
29
- return accounts.filter(acc => !acc.isRateLimited && !acc.isInvalid);
38
+ export function getAvailableAccounts(accounts, modelId = null) {
39
+ return accounts.filter(acc => {
40
+ if (acc.isInvalid) return false;
41
+
42
+ if (modelId && acc.modelRateLimits && acc.modelRateLimits[modelId]) {
43
+ const limit = acc.modelRateLimits[modelId];
44
+ if (limit.isRateLimited && limit.resetTime > Date.now()) {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ return true;
50
+ });
30
51
  }
31
52
 
32
53
  /**
@@ -50,11 +71,15 @@ export function clearExpiredLimits(accounts) {
50
71
  let cleared = 0;
51
72
 
52
73
  for (const account of accounts) {
53
- if (account.isRateLimited && account.rateLimitResetTime && account.rateLimitResetTime <= now) {
54
- account.isRateLimited = false;
55
- account.rateLimitResetTime = null;
56
- cleared++;
57
- logger.success(`[AccountManager] Rate limit expired for: ${account.email}`);
74
+ if (account.modelRateLimits) {
75
+ for (const [modelId, limit] of Object.entries(account.modelRateLimits)) {
76
+ if (limit.isRateLimited && limit.resetTime <= now) {
77
+ limit.isRateLimited = false;
78
+ limit.resetTime = null;
79
+ cleared++;
80
+ logger.success(`[AccountManager] Rate limit expired for: ${account.email} (model: ${modelId})`);
81
+ }
82
+ }
58
83
  }
59
84
  }
60
85
 
@@ -68,31 +93,43 @@ export function clearExpiredLimits(accounts) {
68
93
  */
69
94
  export function resetAllRateLimits(accounts) {
70
95
  for (const account of accounts) {
71
- account.isRateLimited = false;
72
- account.rateLimitResetTime = null;
96
+ if (account.modelRateLimits) {
97
+ for (const key of Object.keys(account.modelRateLimits)) {
98
+ account.modelRateLimits[key] = { isRateLimited: false, resetTime: null };
99
+ }
100
+ }
73
101
  }
74
102
  logger.warn('[AccountManager] Reset all rate limits for optimistic retry');
75
103
  }
76
104
 
77
105
  /**
78
- * Mark an account as rate-limited
106
+ * Mark an account as rate-limited for a specific model
79
107
  *
80
108
  * @param {Array} accounts - Array of account objects
81
109
  * @param {string} email - Email of the account to mark
82
- * @param {number|null} resetMs - Time in ms until rate limit resets (optional)
110
+ * @param {number|null} resetMs - Time in ms until rate limit resets
83
111
  * @param {Object} settings - Settings object with cooldownDurationMs
112
+ * @param {string} modelId - Model ID to mark rate limit for
84
113
  * @returns {boolean} True if account was found and marked
85
114
  */
86
- export function markRateLimited(accounts, email, resetMs = null, settings = {}) {
115
+ export function markRateLimited(accounts, email, resetMs = null, settings = {}, modelId) {
87
116
  const account = accounts.find(a => a.email === email);
88
117
  if (!account) return false;
89
118
 
90
- account.isRateLimited = true;
91
119
  const cooldownMs = resetMs || settings.cooldownDurationMs || DEFAULT_COOLDOWN_MS;
92
- account.rateLimitResetTime = Date.now() + cooldownMs;
120
+ const resetTime = Date.now() + cooldownMs;
121
+
122
+ if (!account.modelRateLimits) {
123
+ account.modelRateLimits = {};
124
+ }
125
+
126
+ account.modelRateLimits[modelId] = {
127
+ isRateLimited: true,
128
+ resetTime: resetTime
129
+ };
93
130
 
94
131
  logger.warn(
95
- `[AccountManager] Rate limited: ${email}. Available in ${formatDuration(cooldownMs)}`
132
+ `[AccountManager] Rate limited: ${email} (model: ${modelId}). Available in ${formatDuration(cooldownMs)}`
96
133
  );
97
134
 
98
135
  return true;
@@ -128,24 +165,28 @@ export function markInvalid(accounts, email, reason = 'Unknown error') {
128
165
  }
129
166
 
130
167
  /**
131
- * Get the minimum wait time until any account becomes available
168
+ * Get the minimum wait time until any account becomes available for a model
132
169
  *
133
170
  * @param {Array} accounts - Array of account objects
171
+ * @param {string} modelId - Model ID to check
134
172
  * @returns {number} Wait time in milliseconds
135
173
  */
136
- export function getMinWaitTimeMs(accounts) {
137
- if (!isAllRateLimited(accounts)) return 0;
174
+ export function getMinWaitTimeMs(accounts, modelId) {
175
+ if (!isAllRateLimited(accounts, modelId)) return 0;
138
176
 
139
177
  const now = Date.now();
140
178
  let minWait = Infinity;
141
179
  let soonestAccount = null;
142
180
 
143
181
  for (const account of accounts) {
144
- if (account.rateLimitResetTime) {
145
- const wait = account.rateLimitResetTime - now;
146
- if (wait > 0 && wait < minWait) {
147
- minWait = wait;
148
- soonestAccount = account;
182
+ if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
183
+ const limit = account.modelRateLimits[modelId];
184
+ if (limit.isRateLimited && limit.resetTime) {
185
+ const wait = limit.resetTime - now;
186
+ if (wait > 0 && wait < minWait) {
187
+ minWait = wait;
188
+ soonestAccount = account;
189
+ }
149
190
  }
150
191
  }
151
192
  }
@@ -2,6 +2,7 @@
2
2
  * Account Selection
3
3
  *
4
4
  * Handles account picking logic (round-robin, sticky) for cache continuity.
5
+ * All rate limit checks are model-specific.
5
6
  */
6
7
 
7
8
  import { MAX_WAIT_BEFORE_ERROR_MS } from '../constants.js';
@@ -9,18 +10,38 @@ import { formatDuration } from '../utils/helpers.js';
9
10
  import { logger } from '../utils/logger.js';
10
11
  import { clearExpiredLimits, getAvailableAccounts } from './rate-limits.js';
11
12
 
13
+ /**
14
+ * Check if an account is usable for a specific model
15
+ * @param {Object} account - Account object
16
+ * @param {string} modelId - Model ID to check
17
+ * @returns {boolean} True if account is usable
18
+ */
19
+ function isAccountUsable(account, modelId) {
20
+ if (!account || account.isInvalid) return false;
21
+
22
+ if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
23
+ const limit = account.modelRateLimits[modelId];
24
+ if (limit.isRateLimited && limit.resetTime > Date.now()) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ return true;
30
+ }
31
+
12
32
  /**
13
33
  * Pick the next available account (fallback when current is unavailable).
14
34
  *
15
35
  * @param {Array} accounts - Array of account objects
16
36
  * @param {number} currentIndex - Current account index
17
37
  * @param {Function} onSave - Callback to save changes
38
+ * @param {string} [modelId] - Model ID to check rate limits for
18
39
  * @returns {{account: Object|null, newIndex: number}} The next available account and new index
19
40
  */
20
- export function pickNext(accounts, currentIndex, onSave) {
41
+ export function pickNext(accounts, currentIndex, onSave, modelId = null) {
21
42
  clearExpiredLimits(accounts);
22
43
 
23
- const available = getAvailableAccounts(accounts);
44
+ const available = getAvailableAccounts(accounts, modelId);
24
45
  if (available.length === 0) {
25
46
  return { account: null, newIndex: currentIndex };
26
47
  }
@@ -36,7 +57,7 @@ export function pickNext(accounts, currentIndex, onSave) {
36
57
  const idx = (index + i) % accounts.length;
37
58
  const account = accounts[idx];
38
59
 
39
- if (!account.isRateLimited && !account.isInvalid) {
60
+ if (isAccountUsable(account, modelId)) {
40
61
  account.lastUsed = Date.now();
41
62
 
42
63
  const position = idx + 1;
@@ -59,9 +80,10 @@ export function pickNext(accounts, currentIndex, onSave) {
59
80
  * @param {Array} accounts - Array of account objects
60
81
  * @param {number} currentIndex - Current account index
61
82
  * @param {Function} onSave - Callback to save changes
83
+ * @param {string} [modelId] - Model ID to check rate limits for
62
84
  * @returns {{account: Object|null, newIndex: number}} The current account and index
63
85
  */
64
- export function getCurrentStickyAccount(accounts, currentIndex, onSave) {
86
+ export function getCurrentStickyAccount(accounts, currentIndex, onSave, modelId = null) {
65
87
  clearExpiredLimits(accounts);
66
88
 
67
89
  if (accounts.length === 0) {
@@ -77,8 +99,7 @@ export function getCurrentStickyAccount(accounts, currentIndex, onSave) {
77
99
  // Get current account directly (activeIndex = current account)
78
100
  const account = accounts[index];
79
101
 
80
- // Return if available
81
- if (account && !account.isRateLimited && !account.isInvalid) {
102
+ if (isAccountUsable(account, modelId)) {
82
103
  account.lastUsed = Date.now();
83
104
  // Trigger save (don't await to avoid blocking)
84
105
  if (onSave) onSave();
@@ -93,9 +114,10 @@ export function getCurrentStickyAccount(accounts, currentIndex, onSave) {
93
114
  *
94
115
  * @param {Array} accounts - Array of account objects
95
116
  * @param {number} currentIndex - Current account index
117
+ * @param {string} [modelId] - Model ID to check rate limits for
96
118
  * @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
97
119
  */
98
- export function shouldWaitForCurrentAccount(accounts, currentIndex) {
120
+ export function shouldWaitForCurrentAccount(accounts, currentIndex, modelId = null) {
99
121
  if (accounts.length === 0) {
100
122
  return { shouldWait: false, waitMs: 0, account: null };
101
123
  }
@@ -113,15 +135,21 @@ export function shouldWaitForCurrentAccount(accounts, currentIndex) {
113
135
  return { shouldWait: false, waitMs: 0, account: null };
114
136
  }
115
137
 
116
- if (account.isRateLimited && account.rateLimitResetTime) {
117
- const waitMs = account.rateLimitResetTime - Date.now();
138
+ let waitMs = 0;
118
139
 
119
- // If wait time is within threshold, recommend waiting
120
- if (waitMs > 0 && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
121
- return { shouldWait: true, waitMs, account };
140
+ // Check model-specific limit
141
+ if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
142
+ const limit = account.modelRateLimits[modelId];
143
+ if (limit.isRateLimited && limit.resetTime) {
144
+ waitMs = limit.resetTime - Date.now();
122
145
  }
123
146
  }
124
147
 
148
+ // If wait time is within threshold, recommend waiting
149
+ if (waitMs > 0 && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
150
+ return { shouldWait: true, waitMs, account };
151
+ }
152
+
125
153
  return { shouldWait: false, waitMs: 0, account };
126
154
  }
127
155
 
@@ -132,21 +160,22 @@ export function shouldWaitForCurrentAccount(accounts, currentIndex) {
132
160
  * @param {Array} accounts - Array of account objects
133
161
  * @param {number} currentIndex - Current account index
134
162
  * @param {Function} onSave - Callback to save changes
163
+ * @param {string} [modelId] - Model ID to check rate limits for
135
164
  * @returns {{account: Object|null, waitMs: number, newIndex: number}}
136
165
  */
137
- export function pickStickyAccount(accounts, currentIndex, onSave) {
166
+ export function pickStickyAccount(accounts, currentIndex, onSave, modelId = null) {
138
167
  // First try to get the current sticky account
139
- const { account: stickyAccount, newIndex: stickyIndex } = getCurrentStickyAccount(accounts, currentIndex, onSave);
168
+ const { account: stickyAccount, newIndex: stickyIndex } = getCurrentStickyAccount(accounts, currentIndex, onSave, modelId);
140
169
  if (stickyAccount) {
141
170
  return { account: stickyAccount, waitMs: 0, newIndex: stickyIndex };
142
171
  }
143
172
 
144
173
  // Current account is rate-limited or invalid.
145
174
  // CHECK IF OTHERS ARE AVAILABLE before deciding to wait.
146
- const available = getAvailableAccounts(accounts);
175
+ const available = getAvailableAccounts(accounts, modelId);
147
176
  if (available.length > 0) {
148
177
  // Found a free account! Switch immediately.
149
- const { account: nextAccount, newIndex } = pickNext(accounts, currentIndex, onSave);
178
+ const { account: nextAccount, newIndex } = pickNext(accounts, currentIndex, onSave, modelId);
150
179
  if (nextAccount) {
151
180
  logger.info(`[AccountManager] Switched to new account (failover): ${nextAccount.email}`);
152
181
  return { account: nextAccount, waitMs: 0, newIndex };
@@ -154,14 +183,14 @@ export function pickStickyAccount(accounts, currentIndex, onSave) {
154
183
  }
155
184
 
156
185
  // No other accounts available. Now checking if we should wait for current account.
157
- const waitInfo = shouldWaitForCurrentAccount(accounts, currentIndex);
186
+ const waitInfo = shouldWaitForCurrentAccount(accounts, currentIndex, modelId);
158
187
  if (waitInfo.shouldWait) {
159
188
  logger.info(`[AccountManager] Waiting ${formatDuration(waitInfo.waitMs)} for sticky account: ${waitInfo.account.email}`);
160
189
  return { account: null, waitMs: waitInfo.waitMs, newIndex: currentIndex };
161
190
  }
162
191
 
163
192
  // Current account unavailable for too long/invalid, and no others available?
164
- const { account: nextAccount, newIndex } = pickNext(accounts, currentIndex, onSave);
193
+ const { account: nextAccount, newIndex } = pickNext(accounts, currentIndex, onSave, modelId);
165
194
  if (nextAccount) {
166
195
  logger.info(`[AccountManager] Switched to new account for cache: ${nextAccount.email}`);
167
196
  }
@@ -26,9 +26,11 @@ export async function loadAccounts(configPath = ACCOUNT_CONFIG_PATH) {
26
26
 
27
27
  const accounts = (config.accounts || []).map(acc => ({
28
28
  ...acc,
29
- isRateLimited: acc.isRateLimited || false,
30
- rateLimitResetTime: acc.rateLimitResetTime || null,
31
- lastUsed: acc.lastUsed || null
29
+ lastUsed: acc.lastUsed || null,
30
+ // Reset invalid flag on startup - give accounts a fresh chance to refresh
31
+ isInvalid: false,
32
+ invalidReason: null,
33
+ modelRateLimits: acc.modelRateLimits || {}
32
34
  }));
33
35
 
34
36
  const settings = config.settings || {};
@@ -66,9 +68,8 @@ export function loadDefaultAccount(dbPath) {
66
68
  const account = {
67
69
  email: authData.email || 'default@antigravity',
68
70
  source: 'database',
69
- isRateLimited: false,
70
- rateLimitResetTime: null,
71
- lastUsed: null
71
+ lastUsed: null,
72
+ modelRateLimits: {}
72
73
  };
73
74
 
74
75
  const tokenCache = new Map();
@@ -111,10 +112,9 @@ export async function saveAccounts(configPath, accounts, settings, activeIndex)
111
112
  apiKey: acc.source === 'manual' ? acc.apiKey : undefined,
112
113
  projectId: acc.projectId || undefined,
113
114
  addedAt: acc.addedAt || undefined,
114
- isRateLimited: acc.isRateLimited,
115
- rateLimitResetTime: acc.rateLimitResetTime,
116
115
  isInvalid: acc.isInvalid || false,
117
116
  invalidReason: acc.invalidReason || null,
117
+ modelRateLimits: acc.modelRateLimits || {},
118
118
  lastUsed: acc.lastUsed
119
119
  })),
120
120
  settings: settings,
@@ -138,8 +138,7 @@ function saveAccounts(accounts, settings = {}) {
138
138
  projectId: acc.projectId,
139
139
  addedAt: acc.addedAt || new Date().toISOString(),
140
140
  lastUsed: acc.lastUsed || null,
141
- isRateLimited: acc.isRateLimited || false,
142
- rateLimitResetTime: acc.rateLimitResetTime || null
141
+ modelRateLimits: acc.modelRateLimits || {}
143
142
  })),
144
143
  settings: {
145
144
  cooldownDurationMs: 60000,
@@ -168,7 +167,11 @@ function displayAccounts(accounts) {
168
167
 
169
168
  console.log(`\n${accounts.length} account(s) saved:`);
170
169
  accounts.forEach((acc, i) => {
171
- const status = acc.isRateLimited ? ' (rate-limited)' : '';
170
+ // Check for any active model-specific rate limits
171
+ const hasActiveLimit = Object.values(acc.modelRateLimits || {}).some(
172
+ limit => limit.isRateLimited && limit.resetTime > Date.now()
173
+ );
174
+ const status = hasActiveLimit ? ' (rate-limited)' : '';
172
175
  console.log(` ${i + 1}. ${acc.email}${status}`);
173
176
  });
174
177
  }
@@ -218,8 +221,7 @@ async function addAccount(existingAccounts) {
218
221
  refreshToken: result.refreshToken,
219
222
  projectId: result.projectId,
220
223
  addedAt: new Date().toISOString(),
221
- isRateLimited: false,
222
- rateLimitResetTime: null
224
+ modelRateLimits: {}
223
225
  };
224
226
  } catch (error) {
225
227
  console.error(`\n✗ Authentication failed: ${error.message}`);
@@ -280,7 +282,7 @@ async function interactiveAdd(rl) {
280
282
  if (accounts.length > 0) {
281
283
  displayAccounts(accounts);
282
284
 
283
- const choice = await rl.question('\n(a)dd new, (r)emove existing, or (f)resh start? [a/r/f]: ');
285
+ const choice = await rl.question('\n(a)dd new, (r)emove existing, (f)resh start, or (e)xit? [a/r/f/e]: ');
284
286
  const c = choice.toLowerCase();
285
287
 
286
288
  if (c === 'r') {
@@ -291,36 +293,32 @@ async function interactiveAdd(rl) {
291
293
  accounts.length = 0;
292
294
  } else if (c === 'a') {
293
295
  console.log('\nAdding to existing accounts.');
296
+ } else if (c === 'e') {
297
+ console.log('\nExiting...');
298
+ return; // Exit cleanly
294
299
  } else {
295
300
  console.log('\nInvalid choice, defaulting to add.');
296
301
  }
297
302
  }
298
303
 
299
- // Add accounts loop
300
- while (accounts.length < MAX_ACCOUNTS) {
301
- const newAccount = await addAccount(accounts);
302
- if (newAccount) {
303
- accounts.push(newAccount);
304
- // Auto-save after each successful add to prevent data loss
305
- saveAccounts(accounts);
306
- } else if (accounts.length > 0) {
307
- // Even if newAccount is null (duplicate update), save the updated accounts
308
- saveAccounts(accounts);
309
- }
310
-
311
- if (accounts.length >= MAX_ACCOUNTS) {
312
- console.log(`\nMaximum of ${MAX_ACCOUNTS} accounts reached.`);
313
- break;
314
- }
304
+ // Add single account
305
+ if (accounts.length >= MAX_ACCOUNTS) {
306
+ console.log(`\nMaximum of ${MAX_ACCOUNTS} accounts reached.`);
307
+ return;
308
+ }
315
309
 
316
- const addMore = await rl.question('\nAdd another account? [y/N]: ');
317
- if (addMore.toLowerCase() !== 'y') {
318
- break;
319
- }
310
+ const newAccount = await addAccount(accounts);
311
+ if (newAccount) {
312
+ accounts.push(newAccount);
313
+ saveAccounts(accounts);
314
+ } else if (accounts.length > 0) {
315
+ // Even if newAccount is null (duplicate update), save the updated accounts
316
+ saveAccounts(accounts);
320
317
  }
321
318
 
322
319
  if (accounts.length > 0) {
323
320
  displayAccounts(accounts);
321
+ console.log('\nTo add more accounts, run this command again.');
324
322
  } else {
325
323
  console.log('\nNo accounts to save.');
326
324
  }
@@ -431,6 +429,8 @@ async function main() {
431
429
  }
432
430
  } finally {
433
431
  rl.close();
432
+ // Force exit to prevent hanging
433
+ process.exit(0);
434
434
  }
435
435
  }
436
436