commons-proxy 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Management
|
|
3
|
+
*
|
|
4
|
+
* Handles rate limit tracking and state management for accounts.
|
|
5
|
+
* All rate limits are model-specific.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_COOLDOWN_MS } from '../constants.js';
|
|
9
|
+
import { formatDuration } from '../utils/helpers.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if all accounts are rate-limited for a specific model
|
|
14
|
+
*
|
|
15
|
+
* @param {Array} accounts - Array of account objects
|
|
16
|
+
* @param {string} modelId - Model ID to check rate limits for
|
|
17
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
18
|
+
*/
|
|
19
|
+
export function isAllRateLimited(accounts, modelId) {
|
|
20
|
+
if (accounts.length === 0) return true;
|
|
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
|
+
if (acc.enabled === false) return true; // Disabled accounts count as unavailable
|
|
26
|
+
const modelLimits = acc.modelRateLimits || {};
|
|
27
|
+
const limit = modelLimits[modelId];
|
|
28
|
+
return limit && limit.isRateLimited && limit.resetTime > Date.now();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get list of available (non-rate-limited, non-invalid) accounts for a model
|
|
34
|
+
*
|
|
35
|
+
* @param {Array} accounts - Array of account objects
|
|
36
|
+
* @param {string} [modelId] - Model ID to filter by
|
|
37
|
+
* @returns {Array} Array of available account objects
|
|
38
|
+
*/
|
|
39
|
+
export function getAvailableAccounts(accounts, modelId = null) {
|
|
40
|
+
return accounts.filter(acc => {
|
|
41
|
+
if (acc.isInvalid) return false;
|
|
42
|
+
|
|
43
|
+
// WebUI: Skip disabled accounts
|
|
44
|
+
if (acc.enabled === false) return false;
|
|
45
|
+
|
|
46
|
+
if (modelId && acc.modelRateLimits && acc.modelRateLimits[modelId]) {
|
|
47
|
+
const limit = acc.modelRateLimits[modelId];
|
|
48
|
+
if (limit.isRateLimited && limit.resetTime > Date.now()) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get list of invalid accounts
|
|
59
|
+
*
|
|
60
|
+
* @param {Array} accounts - Array of account objects
|
|
61
|
+
* @returns {Array} Array of invalid account objects
|
|
62
|
+
*/
|
|
63
|
+
export function getInvalidAccounts(accounts) {
|
|
64
|
+
return accounts.filter(acc => acc.isInvalid);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clear expired rate limits
|
|
69
|
+
*
|
|
70
|
+
* @param {Array} accounts - Array of account objects
|
|
71
|
+
* @returns {number} Number of rate limits cleared
|
|
72
|
+
*/
|
|
73
|
+
export function clearExpiredLimits(accounts) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
let cleared = 0;
|
|
76
|
+
|
|
77
|
+
for (const account of accounts) {
|
|
78
|
+
if (account.modelRateLimits) {
|
|
79
|
+
for (const [modelId, limit] of Object.entries(account.modelRateLimits)) {
|
|
80
|
+
if (limit.isRateLimited && limit.resetTime <= now) {
|
|
81
|
+
limit.isRateLimited = false;
|
|
82
|
+
limit.resetTime = null;
|
|
83
|
+
cleared++;
|
|
84
|
+
logger.success(`[AccountManager] Rate limit expired for: ${account.email} (model: ${modelId})`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return cleared;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear all rate limits to force a fresh check (optimistic retry strategy)
|
|
95
|
+
*
|
|
96
|
+
* @param {Array} accounts - Array of account objects
|
|
97
|
+
*/
|
|
98
|
+
export function resetAllRateLimits(accounts) {
|
|
99
|
+
for (const account of accounts) {
|
|
100
|
+
if (account.modelRateLimits) {
|
|
101
|
+
for (const key of Object.keys(account.modelRateLimits)) {
|
|
102
|
+
account.modelRateLimits[key] = { isRateLimited: false, resetTime: null };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
logger.warn('[AccountManager] Reset all rate limits for optimistic retry');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Mark an account as rate-limited for a specific model
|
|
111
|
+
*
|
|
112
|
+
* @param {Array} accounts - Array of account objects
|
|
113
|
+
* @param {string} email - Email of the account to mark
|
|
114
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (from API)
|
|
115
|
+
* @param {string} modelId - Model ID to mark rate limit for
|
|
116
|
+
* @returns {boolean} True if account was found and marked
|
|
117
|
+
*/
|
|
118
|
+
export function markRateLimited(accounts, email, resetMs = null, modelId) {
|
|
119
|
+
const account = accounts.find(a => a.email === email);
|
|
120
|
+
if (!account) return false;
|
|
121
|
+
|
|
122
|
+
// Store the ACTUAL reset time from the API
|
|
123
|
+
// This is used to decide whether to wait (short) or switch accounts (long)
|
|
124
|
+
const actualResetMs = (resetMs && resetMs > 0) ? resetMs : DEFAULT_COOLDOWN_MS;
|
|
125
|
+
|
|
126
|
+
if (!account.modelRateLimits) {
|
|
127
|
+
account.modelRateLimits = {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
account.modelRateLimits[modelId] = {
|
|
131
|
+
isRateLimited: true,
|
|
132
|
+
resetTime: Date.now() + actualResetMs, // Actual reset time for decisions
|
|
133
|
+
actualResetMs: actualResetMs // Original duration from API
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Track consecutive failures for progressive backoff (matches opencode-cloudcode-auth)
|
|
137
|
+
account.consecutiveFailures = (account.consecutiveFailures || 0) + 1;
|
|
138
|
+
|
|
139
|
+
// Log appropriately based on duration
|
|
140
|
+
if (actualResetMs > DEFAULT_COOLDOWN_MS) {
|
|
141
|
+
logger.warn(
|
|
142
|
+
`[AccountManager] Quota exhausted: ${email} (model: ${modelId}). Resets in ${formatDuration(actualResetMs)}`
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
logger.warn(
|
|
146
|
+
`[AccountManager] Rate limited: ${email} (model: ${modelId}). Available in ${formatDuration(actualResetMs)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
155
|
+
*
|
|
156
|
+
* @param {Array} accounts - Array of account objects
|
|
157
|
+
* @param {string} email - Email of the account to mark
|
|
158
|
+
* @param {string} reason - Reason for marking as invalid
|
|
159
|
+
* @returns {boolean} True if account was found and marked
|
|
160
|
+
*/
|
|
161
|
+
export function markInvalid(accounts, email, reason = 'Unknown error') {
|
|
162
|
+
const account = accounts.find(a => a.email === email);
|
|
163
|
+
if (!account) return false;
|
|
164
|
+
|
|
165
|
+
account.isInvalid = true;
|
|
166
|
+
account.invalidReason = reason;
|
|
167
|
+
account.invalidAt = Date.now();
|
|
168
|
+
|
|
169
|
+
logger.error(
|
|
170
|
+
`[AccountManager] ⚠ Account INVALID: ${email}`
|
|
171
|
+
);
|
|
172
|
+
logger.error(
|
|
173
|
+
`[AccountManager] Reason: ${reason}`
|
|
174
|
+
);
|
|
175
|
+
logger.error(
|
|
176
|
+
`[AccountManager] Run 'npm run accounts' to re-authenticate this account`
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the minimum wait time until any account becomes available for a model
|
|
184
|
+
*
|
|
185
|
+
* @param {Array} accounts - Array of account objects
|
|
186
|
+
* @param {string} modelId - Model ID to check
|
|
187
|
+
* @returns {number} Wait time in milliseconds
|
|
188
|
+
*/
|
|
189
|
+
export function getMinWaitTimeMs(accounts, modelId) {
|
|
190
|
+
if (!isAllRateLimited(accounts, modelId)) return 0;
|
|
191
|
+
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
let minWait = Infinity;
|
|
194
|
+
let soonestAccount = null;
|
|
195
|
+
|
|
196
|
+
for (const account of accounts) {
|
|
197
|
+
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
198
|
+
const limit = account.modelRateLimits[modelId];
|
|
199
|
+
if (limit.isRateLimited && limit.resetTime) {
|
|
200
|
+
const wait = limit.resetTime - now;
|
|
201
|
+
if (wait > 0 && wait < minWait) {
|
|
202
|
+
minWait = wait;
|
|
203
|
+
soonestAccount = account;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (soonestAccount) {
|
|
210
|
+
logger.info(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get the rate limit info for a specific account and model
|
|
218
|
+
* Returns the actual reset time from API, not capped
|
|
219
|
+
*
|
|
220
|
+
* @param {Array} accounts - Array of account objects
|
|
221
|
+
* @param {string} email - Email of the account
|
|
222
|
+
* @param {string} modelId - Model ID to check
|
|
223
|
+
* @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info
|
|
224
|
+
*/
|
|
225
|
+
export function getRateLimitInfo(accounts, email, modelId) {
|
|
226
|
+
const account = accounts.find(a => a.email === email);
|
|
227
|
+
if (!account || !account.modelRateLimits || !account.modelRateLimits[modelId]) {
|
|
228
|
+
return { isRateLimited: false, actualResetMs: null, waitMs: 0 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const limit = account.modelRateLimits[modelId];
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const waitMs = limit.resetTime ? Math.max(0, limit.resetTime - now) : 0;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
isRateLimited: limit.isRateLimited && waitMs > 0,
|
|
237
|
+
actualResetMs: limit.actualResetMs || null,
|
|
238
|
+
waitMs
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get the consecutive failure count for an account
|
|
244
|
+
* Used for progressive backoff calculation (matches opencode-cloudcode-auth)
|
|
245
|
+
*
|
|
246
|
+
* @param {Array} accounts - Array of account objects
|
|
247
|
+
* @param {string} email - Email of the account
|
|
248
|
+
* @returns {number} Number of consecutive failures
|
|
249
|
+
*/
|
|
250
|
+
export function getConsecutiveFailures(accounts, email) {
|
|
251
|
+
const account = accounts.find(a => a.email === email);
|
|
252
|
+
return account?.consecutiveFailures || 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reset the consecutive failure count for an account
|
|
257
|
+
* Called on successful request (matches opencode-cloudcode-auth)
|
|
258
|
+
*
|
|
259
|
+
* @param {Array} accounts - Array of account objects
|
|
260
|
+
* @param {string} email - Email of the account
|
|
261
|
+
* @returns {boolean} True if account was found and reset
|
|
262
|
+
*/
|
|
263
|
+
export function resetConsecutiveFailures(accounts, email) {
|
|
264
|
+
const account = accounts.find(a => a.email === email);
|
|
265
|
+
if (!account) return false;
|
|
266
|
+
account.consecutiveFailures = 0;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Increment the consecutive failure count for an account WITHOUT marking as rate limited
|
|
272
|
+
* Used for quick retries where we want to track failures but not skip the account
|
|
273
|
+
* (matches opencode-cloudcode-auth behavior of always incrementing on 429)
|
|
274
|
+
*
|
|
275
|
+
* @param {Array} accounts - Array of account objects
|
|
276
|
+
* @param {string} email - Email of the account
|
|
277
|
+
* @returns {number} New consecutive failure count
|
|
278
|
+
*/
|
|
279
|
+
export function incrementConsecutiveFailures(accounts, email) {
|
|
280
|
+
const account = accounts.find(a => a.email === email);
|
|
281
|
+
if (!account) return 0;
|
|
282
|
+
account.consecutiveFailures = (account.consecutiveFailures || 0) + 1;
|
|
283
|
+
return account.consecutiveFailures;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Cooldown Mechanism (matches opencode-cloudcode-auth)
|
|
288
|
+
// Separate from rate limits - used for temporary backoff after failures
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Cooldown reasons for debugging/logging
|
|
293
|
+
*/
|
|
294
|
+
export const CooldownReason = {
|
|
295
|
+
RATE_LIMIT: 'rate_limit',
|
|
296
|
+
AUTH_FAILURE: 'auth_failure',
|
|
297
|
+
CONSECUTIVE_FAILURES: 'consecutive_failures',
|
|
298
|
+
SERVER_ERROR: 'server_error'
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark an account as cooling down for a specified duration
|
|
303
|
+
* Used for temporary backoff separate from rate limits
|
|
304
|
+
*
|
|
305
|
+
* @param {Array} accounts - Array of account objects
|
|
306
|
+
* @param {string} email - Email of the account
|
|
307
|
+
* @param {number} cooldownMs - Duration of cooldown in milliseconds
|
|
308
|
+
* @param {string} [reason] - Reason for the cooldown
|
|
309
|
+
* @returns {boolean} True if account was found and marked
|
|
310
|
+
*/
|
|
311
|
+
export function markAccountCoolingDown(accounts, email, cooldownMs, reason = CooldownReason.RATE_LIMIT) {
|
|
312
|
+
const account = accounts.find(a => a.email === email);
|
|
313
|
+
if (!account) return false;
|
|
314
|
+
|
|
315
|
+
account.coolingDownUntil = Date.now() + cooldownMs;
|
|
316
|
+
account.cooldownReason = reason;
|
|
317
|
+
|
|
318
|
+
logger.debug(`[AccountManager] Account ${email} cooling down for ${formatDuration(cooldownMs)} (reason: ${reason})`);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if an account is currently cooling down
|
|
324
|
+
* Automatically clears expired cooldowns
|
|
325
|
+
*
|
|
326
|
+
* @param {Object} account - Account object
|
|
327
|
+
* @returns {boolean} True if account is cooling down
|
|
328
|
+
*/
|
|
329
|
+
export function isAccountCoolingDown(account) {
|
|
330
|
+
if (!account || account.coolingDownUntil === undefined) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
if (now >= account.coolingDownUntil) {
|
|
336
|
+
// Cooldown expired - clear it
|
|
337
|
+
clearAccountCooldown(account);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Clear the cooldown for an account
|
|
346
|
+
*
|
|
347
|
+
* @param {Object} account - Account object
|
|
348
|
+
*/
|
|
349
|
+
export function clearAccountCooldown(account) {
|
|
350
|
+
if (account) {
|
|
351
|
+
delete account.coolingDownUntil;
|
|
352
|
+
delete account.cooldownReason;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get time remaining until cooldown expires for an account
|
|
358
|
+
*
|
|
359
|
+
* @param {Object} account - Account object
|
|
360
|
+
* @returns {number} Milliseconds until cooldown expires, 0 if not cooling down
|
|
361
|
+
*/
|
|
362
|
+
export function getCooldownRemaining(account) {
|
|
363
|
+
if (!account || account.coolingDownUntil === undefined) {
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const remaining = account.coolingDownUntil - Date.now();
|
|
368
|
+
return remaining > 0 ? remaining : 0;
|
|
369
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Storage
|
|
3
|
+
*
|
|
4
|
+
* Handles loading and saving account configuration to disk.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
8
|
+
import { constants as fsConstants } from 'fs';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { ACCOUNT_CONFIG_PATH } from '../constants.js';
|
|
11
|
+
import { getAuthStatus } from '../auth/database.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect provider from legacy source field
|
|
16
|
+
* @param {string} source - Account source ('oauth', 'manual', 'database')
|
|
17
|
+
* @returns {string} Provider ID
|
|
18
|
+
*/
|
|
19
|
+
function detectProviderFromSource(source) {
|
|
20
|
+
// Legacy accounts use 'oauth' or 'database' for Google OAuth
|
|
21
|
+
if (source === 'oauth' || source === 'database') {
|
|
22
|
+
return 'google';
|
|
23
|
+
}
|
|
24
|
+
// Manual accounts default to Google (legacy behavior)
|
|
25
|
+
if (source === 'manual') {
|
|
26
|
+
return 'google';
|
|
27
|
+
}
|
|
28
|
+
// Default to Google
|
|
29
|
+
return 'google';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load accounts from the config file
|
|
34
|
+
*
|
|
35
|
+
* @param {string} configPath - Path to the config file
|
|
36
|
+
* @returns {Promise<{accounts: Array, settings: Object, activeIndex: number}>}
|
|
37
|
+
*/
|
|
38
|
+
export async function loadAccounts(configPath = ACCOUNT_CONFIG_PATH) {
|
|
39
|
+
try {
|
|
40
|
+
// Check if config file exists using async access
|
|
41
|
+
await access(configPath, fsConstants.F_OK);
|
|
42
|
+
const configData = await readFile(configPath, 'utf-8');
|
|
43
|
+
const config = JSON.parse(configData);
|
|
44
|
+
|
|
45
|
+
const accounts = (config.accounts || []).map(acc => ({
|
|
46
|
+
...acc,
|
|
47
|
+
lastUsed: acc.lastUsed || null,
|
|
48
|
+
enabled: acc.enabled !== false, // Default to true if not specified
|
|
49
|
+
// Reset invalid flag on startup - give accounts a fresh chance to refresh
|
|
50
|
+
isInvalid: false,
|
|
51
|
+
invalidReason: null,
|
|
52
|
+
modelRateLimits: acc.modelRateLimits || {},
|
|
53
|
+
// New fields for subscription and quota tracking
|
|
54
|
+
subscription: acc.subscription || { tier: 'unknown', projectId: null, detectedAt: null },
|
|
55
|
+
quota: acc.quota || { models: {}, lastChecked: null },
|
|
56
|
+
// Multi-provider support
|
|
57
|
+
provider: acc.provider || detectProviderFromSource(acc.source),
|
|
58
|
+
customApiEndpoint: acc.customApiEndpoint || null
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const settings = config.settings || {};
|
|
62
|
+
let activeIndex = config.activeIndex || 0;
|
|
63
|
+
|
|
64
|
+
// Clamp activeIndex to valid range
|
|
65
|
+
if (activeIndex >= accounts.length) {
|
|
66
|
+
activeIndex = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
logger.info(`[AccountManager] Loaded ${accounts.length} account(s) from config`);
|
|
70
|
+
|
|
71
|
+
return { accounts, settings, activeIndex };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code === 'ENOENT') {
|
|
74
|
+
// No config file - return empty
|
|
75
|
+
logger.info('[AccountManager] No config file found. Using Antigravity database (single account mode)');
|
|
76
|
+
} else {
|
|
77
|
+
logger.error('[AccountManager] Failed to load config:', error.message);
|
|
78
|
+
}
|
|
79
|
+
return { accounts: [], settings: {}, activeIndex: 0 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load the default account from Antigravity's database
|
|
85
|
+
*
|
|
86
|
+
* @param {string} dbPath - Optional path to the database
|
|
87
|
+
* @returns {{accounts: Array, tokenCache: Map}}
|
|
88
|
+
*/
|
|
89
|
+
export function loadDefaultAccount(dbPath) {
|
|
90
|
+
try {
|
|
91
|
+
const authData = getAuthStatus(dbPath);
|
|
92
|
+
if (authData?.apiKey) {
|
|
93
|
+
const account = {
|
|
94
|
+
email: authData.email || 'default@commons',
|
|
95
|
+
source: 'database',
|
|
96
|
+
lastUsed: null,
|
|
97
|
+
modelRateLimits: {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const tokenCache = new Map();
|
|
101
|
+
tokenCache.set(account.email, {
|
|
102
|
+
token: authData.apiKey,
|
|
103
|
+
extractedAt: Date.now()
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
logger.info(`[AccountManager] Loaded default account: ${account.email}`);
|
|
107
|
+
|
|
108
|
+
return { accounts: [account], tokenCache };
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error('[AccountManager] Failed to load default account:', error.message);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { accounts: [], tokenCache: new Map() };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Save account configuration to disk
|
|
119
|
+
*
|
|
120
|
+
* @param {string} configPath - Path to the config file
|
|
121
|
+
* @param {Array} accounts - Array of account objects
|
|
122
|
+
* @param {Object} settings - Settings object
|
|
123
|
+
* @param {number} activeIndex - Current active account index
|
|
124
|
+
*/
|
|
125
|
+
export async function saveAccounts(configPath, accounts, settings, activeIndex) {
|
|
126
|
+
try {
|
|
127
|
+
// Ensure directory exists
|
|
128
|
+
const dir = dirname(configPath);
|
|
129
|
+
await mkdir(dir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
const config = {
|
|
132
|
+
accounts: accounts.map(acc => ({
|
|
133
|
+
email: acc.email,
|
|
134
|
+
source: acc.source,
|
|
135
|
+
enabled: acc.enabled !== false, // Persist enabled state
|
|
136
|
+
dbPath: acc.dbPath || null,
|
|
137
|
+
refreshToken: acc.source === 'oauth' ? acc.refreshToken : undefined,
|
|
138
|
+
apiKey: (acc.source === 'manual' || acc.provider !== 'google') ? acc.apiKey : undefined,
|
|
139
|
+
projectId: acc.projectId || undefined,
|
|
140
|
+
addedAt: acc.addedAt || undefined,
|
|
141
|
+
isInvalid: acc.isInvalid || false,
|
|
142
|
+
invalidReason: acc.invalidReason || null,
|
|
143
|
+
modelRateLimits: acc.modelRateLimits || {},
|
|
144
|
+
lastUsed: acc.lastUsed,
|
|
145
|
+
// Persist subscription and quota data
|
|
146
|
+
subscription: acc.subscription || { tier: 'unknown', projectId: null, detectedAt: null },
|
|
147
|
+
quota: acc.quota || { models: {}, lastChecked: null },
|
|
148
|
+
// Multi-provider support
|
|
149
|
+
provider: acc.provider || detectProviderFromSource(acc.source),
|
|
150
|
+
customApiEndpoint: acc.customApiEndpoint || undefined
|
|
151
|
+
})),
|
|
152
|
+
settings: settings,
|
|
153
|
+
activeIndex: activeIndex
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error('[AccountManager] Failed to save config:', error.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Strategy
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class defining the interface for account selection strategies.
|
|
5
|
+
* All strategies must implement the selectAccount method.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isAccountCoolingDown } from '../rate-limits.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SelectionResult
|
|
12
|
+
* @property {Object|null} account - The selected account or null if none available
|
|
13
|
+
* @property {number} index - The index of the selected account
|
|
14
|
+
* @property {number} [waitMs] - Optional wait time before account becomes available
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class BaseStrategy {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new BaseStrategy
|
|
20
|
+
* @param {Object} config - Strategy configuration
|
|
21
|
+
*/
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
if (new.target === BaseStrategy) {
|
|
24
|
+
throw new Error('BaseStrategy is abstract and cannot be instantiated directly');
|
|
25
|
+
}
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Select an account for a request
|
|
31
|
+
* @param {Array} accounts - Array of account objects
|
|
32
|
+
* @param {string} modelId - The model ID for the request
|
|
33
|
+
* @param {Object} options - Additional options
|
|
34
|
+
* @param {number} options.currentIndex - Current account index
|
|
35
|
+
* @param {string} [options.sessionId] - Session ID for cache continuity
|
|
36
|
+
* @param {Function} [options.onSave] - Callback to save changes
|
|
37
|
+
* @returns {SelectionResult} The selected account and index
|
|
38
|
+
*/
|
|
39
|
+
selectAccount(accounts, modelId, options = {}) {
|
|
40
|
+
throw new Error('selectAccount must be implemented by subclass');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Called after a successful request
|
|
45
|
+
* @param {Object} account - The account that was used
|
|
46
|
+
* @param {string} modelId - The model ID that was used
|
|
47
|
+
*/
|
|
48
|
+
onSuccess(account, modelId) {
|
|
49
|
+
// Default: no-op, override in subclass if needed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called when a request is rate-limited
|
|
54
|
+
* @param {Object} account - The account that was rate-limited
|
|
55
|
+
* @param {string} modelId - The model ID that was rate-limited
|
|
56
|
+
*/
|
|
57
|
+
onRateLimit(account, modelId) {
|
|
58
|
+
// Default: no-op, override in subclass if needed
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called when a request fails (non-rate-limit error)
|
|
63
|
+
* @param {Object} account - The account that failed
|
|
64
|
+
* @param {string} modelId - The model ID that failed
|
|
65
|
+
*/
|
|
66
|
+
onFailure(account, modelId) {
|
|
67
|
+
// Default: no-op, override in subclass if needed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if an account is usable for a specific model
|
|
72
|
+
* @param {Object} account - Account object
|
|
73
|
+
* @param {string} modelId - Model ID to check
|
|
74
|
+
* @returns {boolean} True if account is usable
|
|
75
|
+
*/
|
|
76
|
+
isAccountUsable(account, modelId) {
|
|
77
|
+
if (!account || account.isInvalid) return false;
|
|
78
|
+
|
|
79
|
+
// Skip disabled accounts
|
|
80
|
+
if (account.enabled === false) return false;
|
|
81
|
+
|
|
82
|
+
// Check if account is cooling down (matches opencode-cloudcode-auth)
|
|
83
|
+
if (isAccountCoolingDown(account)) return false;
|
|
84
|
+
|
|
85
|
+
// Check model-specific rate limit
|
|
86
|
+
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
87
|
+
const limit = account.modelRateLimits[modelId];
|
|
88
|
+
if (limit.isRateLimited && limit.resetTime > Date.now()) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all usable accounts for a model
|
|
98
|
+
* @param {Array} accounts - Array of account objects
|
|
99
|
+
* @param {string} modelId - Model ID to check
|
|
100
|
+
* @returns {Array} Array of usable accounts with their original indices
|
|
101
|
+
*/
|
|
102
|
+
getUsableAccounts(accounts, modelId) {
|
|
103
|
+
return accounts
|
|
104
|
+
.map((account, index) => ({ account, index }))
|
|
105
|
+
.filter(({ account }) => this.isAccountUsable(account, modelId));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default BaseStrategy;
|