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 +1 -1
- package/src/account-manager/credentials.js +17 -2
- package/src/account-manager/index.js +33 -19
- package/src/account-manager/rate-limits.js +68 -27
- package/src/account-manager/selection.js +47 -18
- package/src/account-manager/storage.js +8 -8
- package/src/cli/accounts.js +26 -26
- package/src/cloudcode/message-handler.js +19 -28
- package/src/cloudcode/model-api.js +17 -2
- package/src/cloudcode/streaming-handler.js +19 -27
- package/src/format/content-converter.js +2 -0
- package/src/format/request-converter.js +10 -0
- package/src/index.js +31 -13
- package/src/server.js +139 -25
- package/src/utils/helpers.js +47 -0
package/package.json
CHANGED
|
@@ -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)
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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.
|
|
54
|
-
account.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
const waitMs = account.rateLimitResetTime - Date.now();
|
|
138
|
+
let waitMs = 0;
|
|
118
139
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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,
|
package/src/cli/accounts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|