antigravity-claude-proxy 1.2.3 → 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 +8 -0
- 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 +5 -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);
|
|
@@ -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,12 +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
29
|
lastUsed: acc.lastUsed || null,
|
|
32
30
|
// Reset invalid flag on startup - give accounts a fresh chance to refresh
|
|
33
31
|
isInvalid: false,
|
|
34
|
-
invalidReason: null
|
|
32
|
+
invalidReason: null,
|
|
33
|
+
modelRateLimits: acc.modelRateLimits || {}
|
|
35
34
|
}));
|
|
36
35
|
|
|
37
36
|
const settings = config.settings || {};
|
|
@@ -69,9 +68,8 @@ export function loadDefaultAccount(dbPath) {
|
|
|
69
68
|
const account = {
|
|
70
69
|
email: authData.email || 'default@antigravity',
|
|
71
70
|
source: 'database',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
lastUsed: null
|
|
71
|
+
lastUsed: null,
|
|
72
|
+
modelRateLimits: {}
|
|
75
73
|
};
|
|
76
74
|
|
|
77
75
|
const tokenCache = new Map();
|
|
@@ -114,10 +112,9 @@ export async function saveAccounts(configPath, accounts, settings, activeIndex)
|
|
|
114
112
|
apiKey: acc.source === 'manual' ? acc.apiKey : undefined,
|
|
115
113
|
projectId: acc.projectId || undefined,
|
|
116
114
|
addedAt: acc.addedAt || undefined,
|
|
117
|
-
isRateLimited: acc.isRateLimited,
|
|
118
|
-
rateLimitResetTime: acc.rateLimitResetTime,
|
|
119
115
|
isInvalid: acc.isInvalid || false,
|
|
120
116
|
invalidReason: acc.invalidReason || null,
|
|
117
|
+
modelRateLimits: acc.modelRateLimits || {},
|
|
121
118
|
lastUsed: acc.lastUsed
|
|
122
119
|
})),
|
|
123
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
|
|
|
@@ -13,28 +13,12 @@ import {
|
|
|
13
13
|
} from '../constants.js';
|
|
14
14
|
import { convertGoogleToAnthropic } from '../format/index.js';
|
|
15
15
|
import { isRateLimitError, isAuthError } from '../errors.js';
|
|
16
|
-
import { formatDuration, sleep } from '../utils/helpers.js';
|
|
16
|
+
import { formatDuration, sleep, isNetworkError } from '../utils/helpers.js';
|
|
17
17
|
import { logger } from '../utils/logger.js';
|
|
18
18
|
import { parseResetTime } from './rate-limit-parser.js';
|
|
19
19
|
import { buildCloudCodeRequest, buildHeaders } from './request-builder.js';
|
|
20
20
|
import { parseThinkingSSEResponse } from './sse-parser.js';
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Check if an error is a rate limit error (429 or RESOURCE_EXHAUSTED)
|
|
24
|
-
* @deprecated Use isRateLimitError from errors.js instead
|
|
25
|
-
*/
|
|
26
|
-
function is429Error(error) {
|
|
27
|
-
return isRateLimitError(error);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Check if an error is an auth-invalid error (credentials need re-authentication)
|
|
32
|
-
* @deprecated Use isAuthError from errors.js instead
|
|
33
|
-
*/
|
|
34
|
-
function isAuthInvalidError(error) {
|
|
35
|
-
return isAuthError(error);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
22
|
/**
|
|
39
23
|
* Send a non-streaming request to Cloud Code with multi-account support
|
|
40
24
|
* Uses SSE endpoint for thinking models (non-streaming doesn't return thinking blocks)
|
|
@@ -59,7 +43,7 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
59
43
|
|
|
60
44
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
61
45
|
// Use sticky account selection for cache continuity
|
|
62
|
-
const { account: stickyAccount, waitMs } = accountManager.pickStickyAccount();
|
|
46
|
+
const { account: stickyAccount, waitMs } = accountManager.pickStickyAccount(model);
|
|
63
47
|
let account = stickyAccount;
|
|
64
48
|
|
|
65
49
|
// Handle waiting for sticky account
|
|
@@ -67,19 +51,19 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
67
51
|
logger.info(`[CloudCode] Waiting ${formatDuration(waitMs)} for sticky account...`);
|
|
68
52
|
await sleep(waitMs);
|
|
69
53
|
accountManager.clearExpiredLimits();
|
|
70
|
-
account = accountManager.getCurrentStickyAccount();
|
|
54
|
+
account = accountManager.getCurrentStickyAccount(model);
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
// Handle all accounts rate-limited
|
|
74
58
|
if (!account) {
|
|
75
|
-
if (accountManager.isAllRateLimited()) {
|
|
76
|
-
const allWaitMs = accountManager.getMinWaitTimeMs();
|
|
59
|
+
if (accountManager.isAllRateLimited(model)) {
|
|
60
|
+
const allWaitMs = accountManager.getMinWaitTimeMs(model);
|
|
77
61
|
const resetTime = new Date(Date.now() + allWaitMs).toISOString();
|
|
78
62
|
|
|
79
63
|
// If wait time is too long (> 2 minutes), throw error immediately
|
|
80
64
|
if (allWaitMs > MAX_WAIT_BEFORE_ERROR_MS) {
|
|
81
65
|
throw new Error(
|
|
82
|
-
`RESOURCE_EXHAUSTED: Rate limited. Quota will reset after ${formatDuration(allWaitMs)}. Next available: ${resetTime}`
|
|
66
|
+
`RESOURCE_EXHAUSTED: Rate limited on ${model}. Quota will reset after ${formatDuration(allWaitMs)}. Next available: ${resetTime}`
|
|
83
67
|
);
|
|
84
68
|
}
|
|
85
69
|
|
|
@@ -88,7 +72,7 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
88
72
|
logger.warn(`[CloudCode] All ${accountCount} account(s) rate-limited. Waiting ${formatDuration(allWaitMs)}...`);
|
|
89
73
|
await sleep(allWaitMs);
|
|
90
74
|
accountManager.clearExpiredLimits();
|
|
91
|
-
account = accountManager.pickNext();
|
|
75
|
+
account = accountManager.pickNext(model);
|
|
92
76
|
}
|
|
93
77
|
|
|
94
78
|
if (!account) {
|
|
@@ -163,7 +147,7 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
163
147
|
return convertGoogleToAnthropic(data, anthropicRequest.model);
|
|
164
148
|
|
|
165
149
|
} catch (endpointError) {
|
|
166
|
-
if (
|
|
150
|
+
if (isRateLimitError(endpointError)) {
|
|
167
151
|
throw endpointError; // Re-throw to trigger account switch
|
|
168
152
|
}
|
|
169
153
|
logger.warn(`[CloudCode] Error at ${endpoint}:`, endpointError.message);
|
|
@@ -176,19 +160,19 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
176
160
|
// If all endpoints returned 429, mark account as rate-limited
|
|
177
161
|
if (lastError.is429) {
|
|
178
162
|
logger.warn(`[CloudCode] All endpoints rate-limited for ${account.email}`);
|
|
179
|
-
accountManager.markRateLimited(account.email, lastError.resetMs);
|
|
163
|
+
accountManager.markRateLimited(account.email, lastError.resetMs, model);
|
|
180
164
|
throw new Error(`Rate limited: ${lastError.errorText}`);
|
|
181
165
|
}
|
|
182
166
|
throw lastError;
|
|
183
167
|
}
|
|
184
168
|
|
|
185
169
|
} catch (error) {
|
|
186
|
-
if (
|
|
170
|
+
if (isRateLimitError(error)) {
|
|
187
171
|
// Rate limited - already marked, continue to next account
|
|
188
172
|
logger.info(`[CloudCode] Account ${account.email} rate-limited, trying next...`);
|
|
189
173
|
continue;
|
|
190
174
|
}
|
|
191
|
-
if (
|
|
175
|
+
if (isAuthError(error)) {
|
|
192
176
|
// Auth invalid - already marked, continue to next account
|
|
193
177
|
logger.warn(`[CloudCode] Account ${account.email} has invalid credentials, trying next...`);
|
|
194
178
|
continue;
|
|
@@ -197,10 +181,17 @@ export async function sendMessage(anthropicRequest, accountManager) {
|
|
|
197
181
|
// UNLESS it's a 500 error, then we treat it as a "soft" failure for this account and try the next one
|
|
198
182
|
if (error.message.includes('API error 5') || error.message.includes('500') || error.message.includes('503')) {
|
|
199
183
|
logger.warn(`[CloudCode] Account ${account.email} failed with 5xx error, trying next...`);
|
|
200
|
-
accountManager.pickNext(); // Force advance to next account
|
|
184
|
+
accountManager.pickNext(model); // Force advance to next account
|
|
201
185
|
continue;
|
|
202
186
|
}
|
|
203
187
|
|
|
188
|
+
if (isNetworkError(error)) {
|
|
189
|
+
logger.warn(`[CloudCode] Network error for ${account.email}, trying next account... (${error.message})`);
|
|
190
|
+
await sleep(1000); // Brief pause before retry
|
|
191
|
+
accountManager.pickNext(model); // Advance to next account
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
204
195
|
throw error;
|
|
205
196
|
}
|
|
206
197
|
}
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
* Handles model listing and quota retrieval from the Cloud Code API.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_HEADERS } from '../constants.js';
|
|
7
|
+
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_HEADERS, getModelFamily } from '../constants.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Check if a model is supported (Claude or Gemini)
|
|
12
|
+
* @param {string} modelId - Model ID to check
|
|
13
|
+
* @returns {boolean} True if model is supported
|
|
14
|
+
*/
|
|
15
|
+
function isSupportedModel(modelId) {
|
|
16
|
+
const family = getModelFamily(modelId);
|
|
17
|
+
return family === 'claude' || family === 'gemini';
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* List available models in Anthropic API format
|
|
12
22
|
* Fetches models dynamically from the Cloud Code API
|
|
@@ -20,7 +30,9 @@ export async function listModels(token) {
|
|
|
20
30
|
return { object: 'list', data: [] };
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
const modelList = Object.entries(data.models)
|
|
33
|
+
const modelList = Object.entries(data.models)
|
|
34
|
+
.filter(([modelId]) => isSupportedModel(modelId))
|
|
35
|
+
.map(([modelId, modelData]) => ({
|
|
24
36
|
id: modelId,
|
|
25
37
|
object: 'model',
|
|
26
38
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -85,6 +97,9 @@ export async function getModelQuotas(token) {
|
|
|
85
97
|
|
|
86
98
|
const quotas = {};
|
|
87
99
|
for (const [modelId, modelData] of Object.entries(data.models)) {
|
|
100
|
+
// Only include Claude and Gemini models
|
|
101
|
+
if (!isSupportedModel(modelId)) continue;
|
|
102
|
+
|
|
88
103
|
if (modelData.quotaInfo) {
|
|
89
104
|
quotas[modelId] = {
|
|
90
105
|
remainingFraction: modelData.quotaInfo.remainingFraction ?? null,
|
|
@@ -11,27 +11,12 @@ import {
|
|
|
11
11
|
MAX_WAIT_BEFORE_ERROR_MS
|
|
12
12
|
} from '../constants.js';
|
|
13
13
|
import { isRateLimitError, isAuthError } from '../errors.js';
|
|
14
|
-
import { formatDuration, sleep } from '../utils/helpers.js';
|
|
14
|
+
import { formatDuration, sleep, isNetworkError } from '../utils/helpers.js';
|
|
15
15
|
import { logger } from '../utils/logger.js';
|
|
16
16
|
import { parseResetTime } from './rate-limit-parser.js';
|
|
17
17
|
import { buildCloudCodeRequest, buildHeaders } from './request-builder.js';
|
|
18
18
|
import { streamSSEResponse } from './sse-streamer.js';
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Check if an error is a rate limit error (429 or RESOURCE_EXHAUSTED)
|
|
22
|
-
* @deprecated Use isRateLimitError from errors.js instead
|
|
23
|
-
*/
|
|
24
|
-
function is429Error(error) {
|
|
25
|
-
return isRateLimitError(error);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if an error is an auth-invalid error (credentials need re-authentication)
|
|
30
|
-
* @deprecated Use isAuthError from errors.js instead
|
|
31
|
-
*/
|
|
32
|
-
function isAuthInvalidError(error) {
|
|
33
|
-
return isAuthError(error);
|
|
34
|
-
}
|
|
35
20
|
|
|
36
21
|
/**
|
|
37
22
|
* Send a streaming request to Cloud Code with multi-account support
|
|
@@ -56,7 +41,7 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
56
41
|
|
|
57
42
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
58
43
|
// Use sticky account selection for cache continuity
|
|
59
|
-
const { account: stickyAccount, waitMs } = accountManager.pickStickyAccount();
|
|
44
|
+
const { account: stickyAccount, waitMs } = accountManager.pickStickyAccount(model);
|
|
60
45
|
let account = stickyAccount;
|
|
61
46
|
|
|
62
47
|
// Handle waiting for sticky account
|
|
@@ -64,19 +49,19 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
64
49
|
logger.info(`[CloudCode] Waiting ${formatDuration(waitMs)} for sticky account...`);
|
|
65
50
|
await sleep(waitMs);
|
|
66
51
|
accountManager.clearExpiredLimits();
|
|
67
|
-
account = accountManager.getCurrentStickyAccount();
|
|
52
|
+
account = accountManager.getCurrentStickyAccount(model);
|
|
68
53
|
}
|
|
69
54
|
|
|
70
55
|
// Handle all accounts rate-limited
|
|
71
56
|
if (!account) {
|
|
72
|
-
if (accountManager.isAllRateLimited()) {
|
|
73
|
-
const allWaitMs = accountManager.getMinWaitTimeMs();
|
|
57
|
+
if (accountManager.isAllRateLimited(model)) {
|
|
58
|
+
const allWaitMs = accountManager.getMinWaitTimeMs(model);
|
|
74
59
|
const resetTime = new Date(Date.now() + allWaitMs).toISOString();
|
|
75
60
|
|
|
76
61
|
// If wait time is too long (> 2 minutes), throw error immediately
|
|
77
62
|
if (allWaitMs > MAX_WAIT_BEFORE_ERROR_MS) {
|
|
78
63
|
throw new Error(
|
|
79
|
-
`RESOURCE_EXHAUSTED: Rate limited. Quota will reset after ${formatDuration(allWaitMs)}. Next available: ${resetTime}`
|
|
64
|
+
`RESOURCE_EXHAUSTED: Rate limited on ${model}. Quota will reset after ${formatDuration(allWaitMs)}. Next available: ${resetTime}`
|
|
80
65
|
);
|
|
81
66
|
}
|
|
82
67
|
|
|
@@ -85,7 +70,7 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
85
70
|
logger.warn(`[CloudCode] All ${accountCount} account(s) rate-limited. Waiting ${formatDuration(allWaitMs)}...`);
|
|
86
71
|
await sleep(allWaitMs);
|
|
87
72
|
accountManager.clearExpiredLimits();
|
|
88
|
-
account = accountManager.pickNext();
|
|
73
|
+
account = accountManager.pickNext(model);
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
if (!account) {
|
|
@@ -153,7 +138,7 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
153
138
|
return;
|
|
154
139
|
|
|
155
140
|
} catch (endpointError) {
|
|
156
|
-
if (
|
|
141
|
+
if (isRateLimitError(endpointError)) {
|
|
157
142
|
throw endpointError; // Re-throw to trigger account switch
|
|
158
143
|
}
|
|
159
144
|
logger.warn(`[CloudCode] Stream error at ${endpoint}:`, endpointError.message);
|
|
@@ -166,19 +151,19 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
166
151
|
// If all endpoints returned 429, mark account as rate-limited
|
|
167
152
|
if (lastError.is429) {
|
|
168
153
|
logger.warn(`[CloudCode] All endpoints rate-limited for ${account.email}`);
|
|
169
|
-
accountManager.markRateLimited(account.email, lastError.resetMs);
|
|
154
|
+
accountManager.markRateLimited(account.email, lastError.resetMs, model);
|
|
170
155
|
throw new Error(`Rate limited: ${lastError.errorText}`);
|
|
171
156
|
}
|
|
172
157
|
throw lastError;
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
} catch (error) {
|
|
176
|
-
if (
|
|
161
|
+
if (isRateLimitError(error)) {
|
|
177
162
|
// Rate limited - already marked, continue to next account
|
|
178
163
|
logger.info(`[CloudCode] Account ${account.email} rate-limited, trying next...`);
|
|
179
164
|
continue;
|
|
180
165
|
}
|
|
181
|
-
if (
|
|
166
|
+
if (isAuthError(error)) {
|
|
182
167
|
// Auth invalid - already marked, continue to next account
|
|
183
168
|
logger.warn(`[CloudCode] Account ${account.email} has invalid credentials, trying next...`);
|
|
184
169
|
continue;
|
|
@@ -187,10 +172,17 @@ export async function* sendMessageStream(anthropicRequest, accountManager) {
|
|
|
187
172
|
// UNLESS it's a 500 error, then we treat it as a "soft" failure for this account and try the next one
|
|
188
173
|
if (error.message.includes('API error 5') || error.message.includes('500') || error.message.includes('503')) {
|
|
189
174
|
logger.warn(`[CloudCode] Account ${account.email} failed with 5xx stream error, trying next...`);
|
|
190
|
-
accountManager.pickNext(); // Force advance to next account
|
|
175
|
+
accountManager.pickNext(model); // Force advance to next account
|
|
191
176
|
continue;
|
|
192
177
|
}
|
|
193
178
|
|
|
179
|
+
if (isNetworkError(error)) {
|
|
180
|
+
logger.warn(`[CloudCode] Network error for ${account.email} (stream), trying next account... (${error.message})`);
|
|
181
|
+
await sleep(1000); // Brief pause before retry
|
|
182
|
+
accountManager.pickNext(model); // Advance to next account
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
194
186
|
throw error;
|
|
195
187
|
}
|
|
196
188
|
}
|
|
@@ -37,6 +37,8 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
|
|
|
37
37
|
const parts = [];
|
|
38
38
|
|
|
39
39
|
for (const block of content) {
|
|
40
|
+
if (!block) continue;
|
|
41
|
+
|
|
40
42
|
if (block.type === 'text') {
|
|
41
43
|
// Skip empty text blocks - they cause API errors
|
|
42
44
|
if (block.text && block.text.trim()) {
|
|
@@ -152,6 +152,16 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
|
|
152
152
|
if (thinkingBudget) {
|
|
153
153
|
thinkingConfig.thinking_budget = thinkingBudget;
|
|
154
154
|
logger.debug(`[RequestConverter] Claude thinking enabled with budget: ${thinkingBudget}`);
|
|
155
|
+
|
|
156
|
+
// Validate max_tokens > thinking_budget as required by the API
|
|
157
|
+
const currentMaxTokens = googleRequest.generationConfig.maxOutputTokens;
|
|
158
|
+
if (currentMaxTokens && currentMaxTokens <= thinkingBudget) {
|
|
159
|
+
// Bump max_tokens to allow for some response content
|
|
160
|
+
// Default to budget + 8192 (standard output buffer)
|
|
161
|
+
const adjustedMaxTokens = thinkingBudget + 8192;
|
|
162
|
+
logger.warn(`[RequestConverter] max_tokens (${currentMaxTokens}) <= thinking_budget (${thinkingBudget}). Adjusting to ${adjustedMaxTokens} to satisfy API requirements`);
|
|
163
|
+
googleRequest.generationConfig.maxOutputTokens = adjustedMaxTokens;
|
|
164
|
+
}
|
|
155
165
|
} else {
|
|
156
166
|
logger.debug('[RequestConverter] Claude thinking enabled (no budget specified)');
|
|
157
167
|
}
|
package/src/index.js
CHANGED
|
@@ -30,29 +30,47 @@ app.listen(PORT, () => {
|
|
|
30
30
|
// Clear console for a clean start
|
|
31
31
|
console.clear();
|
|
32
32
|
|
|
33
|
+
const border = '║';
|
|
34
|
+
// align for 2-space indent (60 chars), align4 for 4-space indent (58 chars)
|
|
35
|
+
const align = (text) => text + ' '.repeat(Math.max(0, 60 - text.length));
|
|
36
|
+
const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
|
|
37
|
+
|
|
38
|
+
// Build Control section dynamically
|
|
39
|
+
let controlSection = '║ Control: ║\n';
|
|
40
|
+
if (!isDebug) {
|
|
41
|
+
controlSection += '║ --debug Enable debug logging ║\n';
|
|
42
|
+
}
|
|
43
|
+
controlSection += '║ Ctrl+C Stop server ║';
|
|
44
|
+
|
|
45
|
+
// Build status section if debug mode is active
|
|
46
|
+
let statusSection = '';
|
|
47
|
+
if (isDebug) {
|
|
48
|
+
statusSection = '║ ║\n';
|
|
49
|
+
statusSection += '║ Active Modes: ║\n';
|
|
50
|
+
statusSection += '║ ✓ Debug mode enabled ║\n';
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
logger.log(`
|
|
34
54
|
╔══════════════════════════════════════════════════════════════╗
|
|
35
55
|
║ Antigravity Claude Proxy Server ║
|
|
36
56
|
╠══════════════════════════════════════════════════════════════╣
|
|
37
57
|
║ ║
|
|
38
|
-
|
|
39
|
-
║ ║
|
|
40
|
-
|
|
41
|
-
║ --debug Enable debug logging ║
|
|
42
|
-
║ Ctrl+C Stop server ║
|
|
58
|
+
${border} ${align(`Server running at: http://localhost:${PORT}`)}${border}
|
|
59
|
+
${statusSection}║ ║
|
|
60
|
+
${controlSection}
|
|
43
61
|
║ ║
|
|
44
62
|
║ Endpoints: ║
|
|
45
|
-
║ POST /v1/messages
|
|
46
|
-
║ GET /v1/models
|
|
47
|
-
║ GET /health
|
|
48
|
-
║ GET /account-limits
|
|
49
|
-
║ POST /refresh-token
|
|
63
|
+
║ POST /v1/messages - Anthropic Messages API ║
|
|
64
|
+
║ GET /v1/models - List available models ║
|
|
65
|
+
║ GET /health - Health check ║
|
|
66
|
+
║ GET /account-limits - Account status & quotas ║
|
|
67
|
+
║ POST /refresh-token - Force token refresh ║
|
|
50
68
|
║ ║
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
${border} ${align(`Configuration:`)}${border}
|
|
70
|
+
${border} ${align4(`Storage: ${CONFIG_DIR}`)}${border}
|
|
53
71
|
║ ║
|
|
54
72
|
║ Usage with Claude Code: ║
|
|
55
|
-
|
|
73
|
+
${border} ${align4(`export ANTHROPIC_BASE_URL=http://localhost:${PORT}`)}${border}
|
|
56
74
|
║ export ANTHROPIC_API_KEY=dummy ║
|
|
57
75
|
║ claude ║
|
|
58
76
|
║ ║
|
package/src/server.js
CHANGED
|
@@ -70,8 +70,9 @@ function parseError(error) {
|
|
|
70
70
|
statusCode = 400; // Use 400 to ensure client does not retry (429 and 529 trigger retries)
|
|
71
71
|
|
|
72
72
|
// Try to extract the quota reset time from the error
|
|
73
|
-
const resetMatch = error.message.match(/quota will reset after (\
|
|
74
|
-
|
|
73
|
+
const resetMatch = error.message.match(/quota will reset after ([\dh\dm\ds]+)/i);
|
|
74
|
+
// Try to extract model from our error format "Rate limited on <model>" or JSON format
|
|
75
|
+
const modelMatch = error.message.match(/Rate limited on ([^.]+)\./) || error.message.match(/"model":\s*"([^"]+)"/);
|
|
75
76
|
const model = modelMatch ? modelMatch[1] : 'the model';
|
|
76
77
|
|
|
77
78
|
if (resetMatch) {
|
|
@@ -111,22 +112,107 @@ app.use((req, res, next) => {
|
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
/**
|
|
114
|
-
* Health check endpoint
|
|
115
|
+
* Health check endpoint - Detailed status
|
|
116
|
+
* Returns status of all accounts including rate limits and model quotas
|
|
115
117
|
*/
|
|
116
118
|
app.get('/health', async (req, res) => {
|
|
117
119
|
try {
|
|
118
120
|
await ensureInitialized();
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
|
|
123
|
+
// Get high-level status first
|
|
119
124
|
const status = accountManager.getStatus();
|
|
125
|
+
const allAccounts = accountManager.getAllAccounts();
|
|
126
|
+
|
|
127
|
+
// Fetch quotas for each account in parallel to get detailed model info
|
|
128
|
+
const accountDetails = await Promise.allSettled(
|
|
129
|
+
allAccounts.map(async (account) => {
|
|
130
|
+
// Check model-specific rate limits
|
|
131
|
+
const activeModelLimits = Object.entries(account.modelRateLimits || {})
|
|
132
|
+
.filter(([_, limit]) => limit.isRateLimited && limit.resetTime > Date.now());
|
|
133
|
+
const isRateLimited = activeModelLimits.length > 0;
|
|
134
|
+
const soonestReset = activeModelLimits.length > 0
|
|
135
|
+
? Math.min(...activeModelLimits.map(([_, l]) => l.resetTime))
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
const baseInfo = {
|
|
139
|
+
email: account.email,
|
|
140
|
+
lastUsed: account.lastUsed ? new Date(account.lastUsed).toISOString() : null,
|
|
141
|
+
modelRateLimits: account.modelRateLimits || {},
|
|
142
|
+
rateLimitCooldownRemaining: soonestReset ? Math.max(0, soonestReset - Date.now()) : 0
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Skip invalid accounts for quota check
|
|
146
|
+
if (account.isInvalid) {
|
|
147
|
+
return {
|
|
148
|
+
...baseInfo,
|
|
149
|
+
status: 'invalid',
|
|
150
|
+
error: account.invalidReason,
|
|
151
|
+
models: {}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const token = await accountManager.getTokenForAccount(account);
|
|
157
|
+
const quotas = await getModelQuotas(token);
|
|
158
|
+
|
|
159
|
+
// Format quotas for readability
|
|
160
|
+
const formattedQuotas = {};
|
|
161
|
+
for (const [modelId, info] of Object.entries(quotas)) {
|
|
162
|
+
formattedQuotas[modelId] = {
|
|
163
|
+
remaining: info.remainingFraction !== null ? `${Math.round(info.remainingFraction * 100)}%` : 'N/A',
|
|
164
|
+
remainingFraction: info.remainingFraction,
|
|
165
|
+
resetTime: info.resetTime || null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...baseInfo,
|
|
171
|
+
status: isRateLimited ? 'rate-limited' : 'ok',
|
|
172
|
+
models: formattedQuotas
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
...baseInfo,
|
|
177
|
+
status: 'error',
|
|
178
|
+
error: error.message,
|
|
179
|
+
models: {}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Process results
|
|
186
|
+
const detailedAccounts = accountDetails.map((result, index) => {
|
|
187
|
+
if (result.status === 'fulfilled') {
|
|
188
|
+
return result.value;
|
|
189
|
+
} else {
|
|
190
|
+
const acc = allAccounts[index];
|
|
191
|
+
return {
|
|
192
|
+
email: acc.email,
|
|
193
|
+
status: 'error',
|
|
194
|
+
error: result.reason?.message || 'Unknown error',
|
|
195
|
+
modelRateLimits: acc.modelRateLimits || {}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
});
|
|
120
199
|
|
|
121
200
|
res.json({
|
|
122
201
|
status: 'ok',
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
latencyMs: Date.now() - start,
|
|
204
|
+
summary: status.summary,
|
|
205
|
+
counts: {
|
|
206
|
+
total: status.total,
|
|
207
|
+
available: status.available,
|
|
208
|
+
rateLimited: status.rateLimited,
|
|
209
|
+
invalid: status.invalid
|
|
210
|
+
},
|
|
211
|
+
accounts: detailedAccounts
|
|
128
212
|
});
|
|
213
|
+
|
|
129
214
|
} catch (error) {
|
|
215
|
+
logger.error('[API] Health check failed:', error);
|
|
130
216
|
res.status(503).json({
|
|
131
217
|
status: 'error',
|
|
132
218
|
error: error.message,
|
|
@@ -236,11 +322,21 @@ app.get('/account-limits', async (req, res) => {
|
|
|
236
322
|
let accStatus;
|
|
237
323
|
if (acc.isInvalid) {
|
|
238
324
|
accStatus = 'invalid';
|
|
239
|
-
} else if (
|
|
240
|
-
|
|
241
|
-
accStatus = remaining > 0 ? `limited (${formatDuration(remaining)})` : 'rate-limited';
|
|
325
|
+
} else if (accLimit?.status === 'error') {
|
|
326
|
+
accStatus = 'error';
|
|
242
327
|
} else {
|
|
243
|
-
|
|
328
|
+
// Count exhausted models (0% or null remaining)
|
|
329
|
+
const models = accLimit?.models || {};
|
|
330
|
+
const modelCount = Object.keys(models).length;
|
|
331
|
+
const exhaustedCount = Object.values(models).filter(
|
|
332
|
+
q => q.remainingFraction === 0 || q.remainingFraction === null
|
|
333
|
+
).length;
|
|
334
|
+
|
|
335
|
+
if (exhaustedCount === 0) {
|
|
336
|
+
accStatus = 'ok';
|
|
337
|
+
} else {
|
|
338
|
+
accStatus = `(${exhaustedCount}/${modelCount}) limited`;
|
|
339
|
+
}
|
|
244
340
|
}
|
|
245
341
|
|
|
246
342
|
// Get reset time from quota API
|
|
@@ -262,14 +358,14 @@ app.get('/account-limits', async (req, res) => {
|
|
|
262
358
|
}
|
|
263
359
|
lines.push('');
|
|
264
360
|
|
|
265
|
-
// Calculate column widths
|
|
266
|
-
const modelColWidth = Math.max(
|
|
267
|
-
const accountColWidth =
|
|
361
|
+
// Calculate column widths - need more space for reset time info
|
|
362
|
+
const modelColWidth = Math.max(28, ...sortedModels.map(m => m.length)) + 2;
|
|
363
|
+
const accountColWidth = 30;
|
|
268
364
|
|
|
269
365
|
// Header row
|
|
270
366
|
let header = 'Model'.padEnd(modelColWidth);
|
|
271
367
|
for (const acc of accountLimits) {
|
|
272
|
-
const shortEmail = acc.email.split('@')[0].slice(0,
|
|
368
|
+
const shortEmail = acc.email.split('@')[0].slice(0, 26);
|
|
273
369
|
header += shortEmail.padEnd(accountColWidth);
|
|
274
370
|
}
|
|
275
371
|
lines.push(header);
|
|
@@ -281,12 +377,22 @@ app.get('/account-limits', async (req, res) => {
|
|
|
281
377
|
for (const acc of accountLimits) {
|
|
282
378
|
const quota = acc.models?.[modelId];
|
|
283
379
|
let cell;
|
|
284
|
-
if (acc.status !== 'ok') {
|
|
380
|
+
if (acc.status !== 'ok' && acc.status !== 'rate-limited') {
|
|
285
381
|
cell = `[${acc.status}]`;
|
|
286
382
|
} else if (!quota) {
|
|
287
383
|
cell = '-';
|
|
288
|
-
} else if (quota.remainingFraction === null) {
|
|
289
|
-
|
|
384
|
+
} else if (quota.remainingFraction === 0 || quota.remainingFraction === null) {
|
|
385
|
+
// Show reset time for exhausted models
|
|
386
|
+
if (quota.resetTime) {
|
|
387
|
+
const resetMs = new Date(quota.resetTime).getTime() - Date.now();
|
|
388
|
+
if (resetMs > 0) {
|
|
389
|
+
cell = `0% (wait ${formatDuration(resetMs)})`;
|
|
390
|
+
} else {
|
|
391
|
+
cell = '0% (resetting...)';
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
cell = '0% (exhausted)';
|
|
395
|
+
}
|
|
290
396
|
} else {
|
|
291
397
|
const pct = Math.round(quota.remainingFraction * 100);
|
|
292
398
|
cell = `${pct}%`;
|
|
@@ -404,17 +510,17 @@ app.post('/v1/messages/count_tokens', (req, res) => {
|
|
|
404
510
|
/**
|
|
405
511
|
* Main messages endpoint - Anthropic Messages API compatible
|
|
406
512
|
*/
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Anthropic-compatible Messages API
|
|
517
|
+
* POST /v1/messages
|
|
518
|
+
*/
|
|
407
519
|
app.post('/v1/messages', async (req, res) => {
|
|
408
520
|
try {
|
|
409
521
|
// Ensure account manager is initialized
|
|
410
522
|
await ensureInitialized();
|
|
411
523
|
|
|
412
|
-
// Optimistic Retry: If ALL accounts are rate-limited, reset them to force a fresh check.
|
|
413
|
-
// If we have some available accounts, we try them first.
|
|
414
|
-
if (accountManager.isAllRateLimited()) {
|
|
415
|
-
logger.warn('[Server] All accounts rate-limited. Resetting state for optimistic retry.');
|
|
416
|
-
accountManager.resetAllRateLimits();
|
|
417
|
-
}
|
|
418
524
|
|
|
419
525
|
const {
|
|
420
526
|
model,
|
|
@@ -430,6 +536,14 @@ app.post('/v1/messages', async (req, res) => {
|
|
|
430
536
|
temperature
|
|
431
537
|
} = req.body;
|
|
432
538
|
|
|
539
|
+
// Optimistic Retry: If ALL accounts are rate-limited for this model, reset them to force a fresh check.
|
|
540
|
+
// If we have some available accounts, we try them first.
|
|
541
|
+
const modelId = model || 'claude-3-5-sonnet-20241022';
|
|
542
|
+
if (accountManager.isAllRateLimited(modelId)) {
|
|
543
|
+
logger.warn(`[Server] All accounts rate-limited for ${modelId}. Resetting state for optimistic retry.`);
|
|
544
|
+
accountManager.resetAllRateLimits();
|
|
545
|
+
}
|
|
546
|
+
|
|
433
547
|
// Validate required fields
|
|
434
548
|
if (!messages || !Array.isArray(messages)) {
|
|
435
549
|
return res.status(400).json({
|
package/src/utils/helpers.js
CHANGED
|
@@ -23,6 +23,7 @@ export function formatDuration(ms) {
|
|
|
23
23
|
return `${secs}s`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
/**
|
|
27
28
|
* Sleep for specified milliseconds
|
|
28
29
|
* @param {number} ms - Duration to sleep in milliseconds
|
|
@@ -31,3 +32,49 @@ export function formatDuration(ms) {
|
|
|
31
32
|
export function sleep(ms) {
|
|
32
33
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
33
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error is a network error (transient)
|
|
38
|
+
* @param {Error} error - The error to check
|
|
39
|
+
* @returns {boolean} True if it is a network error
|
|
40
|
+
*/
|
|
41
|
+
export function isNetworkError(error) {
|
|
42
|
+
const msg = error.message.toLowerCase();
|
|
43
|
+
return (
|
|
44
|
+
msg.includes('fetch failed') ||
|
|
45
|
+
msg.includes('network error') ||
|
|
46
|
+
msg.includes('econnreset') ||
|
|
47
|
+
msg.includes('etimedout') ||
|
|
48
|
+
msg.includes('socket hang up') ||
|
|
49
|
+
msg.includes('timeout')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if an error is an authentication error (permanent until fixed)
|
|
55
|
+
* @param {Error} error - The error to check
|
|
56
|
+
* @returns {boolean} True if it is an auth error
|
|
57
|
+
*/
|
|
58
|
+
export function isAuthError(error) {
|
|
59
|
+
const msg = error.message.toLowerCase();
|
|
60
|
+
return (
|
|
61
|
+
msg.includes('401') ||
|
|
62
|
+
msg.includes('unauthenticated') ||
|
|
63
|
+
msg.includes('invalid_grant') ||
|
|
64
|
+
msg.includes('invalid_client')
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if an error is a rate limit error
|
|
70
|
+
* @param {Error} error - The error to check
|
|
71
|
+
* @returns {boolean} True if it is a rate limit error
|
|
72
|
+
*/
|
|
73
|
+
export function isRateLimitError(error) {
|
|
74
|
+
const msg = error.message.toLowerCase();
|
|
75
|
+
return (
|
|
76
|
+
msg.includes('429') ||
|
|
77
|
+
msg.includes('resource_exhausted') ||
|
|
78
|
+
msg.includes('quota_exhausted')
|
|
79
|
+
);
|
|
80
|
+
}
|