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,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Manager
|
|
3
|
+
* Manages multiple Antigravity accounts with configurable selection strategies,
|
|
4
|
+
* automatic failover, and smart cooldown for rate-limited accounts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ACCOUNT_CONFIG_PATH } from '../constants.js';
|
|
8
|
+
import { loadAccounts, loadDefaultAccount, saveAccounts } from './storage.js';
|
|
9
|
+
import {
|
|
10
|
+
isAllRateLimited as checkAllRateLimited,
|
|
11
|
+
getAvailableAccounts as getAvailable,
|
|
12
|
+
getInvalidAccounts as getInvalid,
|
|
13
|
+
clearExpiredLimits as clearLimits,
|
|
14
|
+
resetAllRateLimits as resetLimits,
|
|
15
|
+
markRateLimited as markLimited,
|
|
16
|
+
markInvalid as markAccountInvalid,
|
|
17
|
+
getMinWaitTimeMs as getMinWait,
|
|
18
|
+
getRateLimitInfo as getLimitInfo,
|
|
19
|
+
getConsecutiveFailures as getFailures,
|
|
20
|
+
resetConsecutiveFailures as resetFailures,
|
|
21
|
+
incrementConsecutiveFailures as incrementFailures,
|
|
22
|
+
markAccountCoolingDown as markCoolingDown,
|
|
23
|
+
isAccountCoolingDown as checkCoolingDown,
|
|
24
|
+
clearAccountCooldown as clearCooldown,
|
|
25
|
+
getCooldownRemaining as getCooldownMs,
|
|
26
|
+
CooldownReason
|
|
27
|
+
} from './rate-limits.js';
|
|
28
|
+
import {
|
|
29
|
+
getTokenForAccount as fetchToken,
|
|
30
|
+
getProjectForAccount as fetchProject,
|
|
31
|
+
clearProjectCache as clearProject,
|
|
32
|
+
clearTokenCache as clearToken
|
|
33
|
+
} from './credentials.js';
|
|
34
|
+
import { createStrategy, getStrategyLabel, DEFAULT_STRATEGY } from './strategies/index.js';
|
|
35
|
+
import { logger } from '../utils/logger.js';
|
|
36
|
+
import { config } from '../config.js';
|
|
37
|
+
|
|
38
|
+
export class AccountManager {
|
|
39
|
+
#accounts = [];
|
|
40
|
+
#currentIndex = 0;
|
|
41
|
+
#configPath;
|
|
42
|
+
#settings = {};
|
|
43
|
+
#initialized = false;
|
|
44
|
+
#strategy = null;
|
|
45
|
+
#strategyName = DEFAULT_STRATEGY;
|
|
46
|
+
|
|
47
|
+
// Per-account caches
|
|
48
|
+
#tokenCache = new Map(); // email -> { token, extractedAt }
|
|
49
|
+
#projectCache = new Map(); // email -> projectId
|
|
50
|
+
|
|
51
|
+
constructor(configPath = ACCOUNT_CONFIG_PATH, strategyName = null) {
|
|
52
|
+
this.#configPath = configPath;
|
|
53
|
+
// Strategy name can be set at construction or later via initialize
|
|
54
|
+
if (strategyName) {
|
|
55
|
+
this.#strategyName = strategyName;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize the account manager by loading config
|
|
61
|
+
* @param {string} [strategyOverride] - Override strategy name (from CLI flag or env var)
|
|
62
|
+
*/
|
|
63
|
+
async initialize(strategyOverride = null) {
|
|
64
|
+
if (this.#initialized) return;
|
|
65
|
+
|
|
66
|
+
const { accounts, settings, activeIndex } = await loadAccounts(this.#configPath);
|
|
67
|
+
|
|
68
|
+
this.#accounts = accounts;
|
|
69
|
+
this.#settings = settings;
|
|
70
|
+
this.#currentIndex = activeIndex;
|
|
71
|
+
|
|
72
|
+
// If config exists but has no accounts, fall back to Antigravity database
|
|
73
|
+
if (this.#accounts.length === 0) {
|
|
74
|
+
logger.warn('[AccountManager] No accounts in config. Falling back to Antigravity database');
|
|
75
|
+
const { accounts: defaultAccounts, tokenCache } = loadDefaultAccount();
|
|
76
|
+
this.#accounts = defaultAccounts;
|
|
77
|
+
this.#tokenCache = tokenCache;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Determine strategy: CLI override > env var > config file > default
|
|
81
|
+
const configStrategy = config?.accountSelection?.strategy;
|
|
82
|
+
const envStrategy = process.env.ACCOUNT_STRATEGY;
|
|
83
|
+
this.#strategyName = strategyOverride || envStrategy || configStrategy || this.#strategyName;
|
|
84
|
+
|
|
85
|
+
// Create the strategy instance
|
|
86
|
+
const strategyConfig = config?.accountSelection || {};
|
|
87
|
+
this.#strategy = createStrategy(this.#strategyName, strategyConfig);
|
|
88
|
+
logger.info(`[AccountManager] Using ${getStrategyLabel(this.#strategyName)} selection strategy`);
|
|
89
|
+
|
|
90
|
+
// Clear any expired rate limits
|
|
91
|
+
this.clearExpiredLimits();
|
|
92
|
+
|
|
93
|
+
this.#initialized = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reload accounts from disk (force re-initialization)
|
|
98
|
+
* Useful when accounts.json is modified externally (e.g., by WebUI)
|
|
99
|
+
*/
|
|
100
|
+
async reload() {
|
|
101
|
+
this.#initialized = false;
|
|
102
|
+
await this.initialize();
|
|
103
|
+
logger.info('[AccountManager] Accounts reloaded from disk');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the number of accounts
|
|
108
|
+
* @returns {number} Number of configured accounts
|
|
109
|
+
*/
|
|
110
|
+
getAccountCount() {
|
|
111
|
+
return this.#accounts.length;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if all accounts are rate-limited
|
|
116
|
+
* @param {string} [modelId] - Optional model ID
|
|
117
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
118
|
+
*/
|
|
119
|
+
isAllRateLimited(modelId = null) {
|
|
120
|
+
return checkAllRateLimited(this.#accounts, modelId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get list of available (non-rate-limited, non-invalid) accounts
|
|
125
|
+
* @param {string} [modelId] - Optional model ID
|
|
126
|
+
* @returns {Array<Object>} Array of available account objects
|
|
127
|
+
*/
|
|
128
|
+
getAvailableAccounts(modelId = null) {
|
|
129
|
+
return getAvailable(this.#accounts, modelId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get list of invalid accounts
|
|
134
|
+
* @returns {Array<Object>} Array of invalid account objects
|
|
135
|
+
*/
|
|
136
|
+
getInvalidAccounts() {
|
|
137
|
+
return getInvalid(this.#accounts);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear expired rate limits
|
|
142
|
+
* @returns {number} Number of rate limits cleared
|
|
143
|
+
*/
|
|
144
|
+
clearExpiredLimits() {
|
|
145
|
+
const cleared = clearLimits(this.#accounts);
|
|
146
|
+
if (cleared > 0) {
|
|
147
|
+
this.saveToDisk();
|
|
148
|
+
}
|
|
149
|
+
return cleared;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear all rate limits to force a fresh check
|
|
154
|
+
* (Optimistic retry strategy)
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
resetAllRateLimits() {
|
|
158
|
+
resetLimits(this.#accounts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Select an account using the configured strategy.
|
|
163
|
+
* This is the main method to use for account selection.
|
|
164
|
+
* @param {string} [modelId] - Model ID for the request
|
|
165
|
+
* @param {Object} [options] - Additional options
|
|
166
|
+
* @param {string} [options.sessionId] - Session ID for cache continuity
|
|
167
|
+
* @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
|
|
168
|
+
*/
|
|
169
|
+
selectAccount(modelId = null, options = {}) {
|
|
170
|
+
if (!this.#strategy) {
|
|
171
|
+
throw new Error('AccountManager not initialized. Call initialize() first.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = this.#strategy.selectAccount(this.#accounts, modelId, {
|
|
175
|
+
currentIndex: this.#currentIndex,
|
|
176
|
+
onSave: () => this.saveToDisk(),
|
|
177
|
+
...options
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.#currentIndex = result.index;
|
|
181
|
+
return { account: result.account, waitMs: result.waitMs || 0 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Notify the strategy of a successful request
|
|
186
|
+
* @param {Object} account - The account that was used
|
|
187
|
+
* @param {string} modelId - The model ID that was used
|
|
188
|
+
*/
|
|
189
|
+
notifySuccess(account, modelId) {
|
|
190
|
+
if (this.#strategy) {
|
|
191
|
+
this.#strategy.onSuccess(account, modelId);
|
|
192
|
+
}
|
|
193
|
+
// Reset consecutive failures on success (matches opencode-cloudcode-auth)
|
|
194
|
+
if (account?.email) {
|
|
195
|
+
resetFailures(this.#accounts, account.email);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Notify the strategy of a rate limit
|
|
201
|
+
* @param {Object} account - The account that was rate-limited
|
|
202
|
+
* @param {string} modelId - The model ID that was rate-limited
|
|
203
|
+
*/
|
|
204
|
+
notifyRateLimit(account, modelId) {
|
|
205
|
+
if (this.#strategy) {
|
|
206
|
+
this.#strategy.onRateLimit(account, modelId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Notify the strategy of a failure
|
|
212
|
+
* @param {Object} account - The account that failed
|
|
213
|
+
* @param {string} modelId - The model ID that failed
|
|
214
|
+
*/
|
|
215
|
+
notifyFailure(account, modelId) {
|
|
216
|
+
if (this.#strategy) {
|
|
217
|
+
this.#strategy.onFailure(account, modelId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the consecutive failure count for an account
|
|
223
|
+
* Used for progressive backoff calculation
|
|
224
|
+
* @param {string} email - Account email
|
|
225
|
+
* @returns {number} Number of consecutive failures
|
|
226
|
+
*/
|
|
227
|
+
getConsecutiveFailures(email) {
|
|
228
|
+
return getFailures(this.#accounts, email);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Increment the consecutive failure count without marking as rate limited
|
|
233
|
+
* Used for quick retries to track failures while staying on same account
|
|
234
|
+
* @param {string} email - Account email
|
|
235
|
+
* @returns {number} New consecutive failure count
|
|
236
|
+
*/
|
|
237
|
+
incrementConsecutiveFailures(email) {
|
|
238
|
+
return incrementFailures(this.#accounts, email);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the current strategy name
|
|
243
|
+
* @returns {string} Strategy name
|
|
244
|
+
*/
|
|
245
|
+
getStrategyName() {
|
|
246
|
+
return this.#strategyName;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the strategy display label
|
|
251
|
+
* @returns {string} Strategy display label
|
|
252
|
+
*/
|
|
253
|
+
getStrategyLabel() {
|
|
254
|
+
return getStrategyLabel(this.#strategyName);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the health tracker from the current strategy (if available)
|
|
259
|
+
* Used by handlers for consecutive failure tracking
|
|
260
|
+
* Only available when using hybrid strategy
|
|
261
|
+
* @returns {Object|null} Health tracker instance or null if not available
|
|
262
|
+
*/
|
|
263
|
+
getHealthTracker() {
|
|
264
|
+
if (this.#strategy && typeof this.#strategy.getHealthTracker === 'function') {
|
|
265
|
+
return this.#strategy.getHealthTracker();
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Mark an account as rate-limited
|
|
272
|
+
* @param {string} email - Email of the account to mark
|
|
273
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
|
|
274
|
+
* @param {string} [modelId] - Optional model ID to mark specific limit
|
|
275
|
+
*/
|
|
276
|
+
markRateLimited(email, resetMs = null, modelId = null) {
|
|
277
|
+
markLimited(this.#accounts, email, resetMs, modelId);
|
|
278
|
+
this.saveToDisk();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
283
|
+
* @param {string} email - Email of the account to mark
|
|
284
|
+
* @param {string} reason - Reason for marking as invalid
|
|
285
|
+
*/
|
|
286
|
+
markInvalid(email, reason = 'Unknown error') {
|
|
287
|
+
markAccountInvalid(this.#accounts, email, reason);
|
|
288
|
+
this.saveToDisk();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get the minimum wait time until any account becomes available
|
|
293
|
+
* @param {string} [modelId] - Optional model ID
|
|
294
|
+
* @returns {number} Wait time in milliseconds
|
|
295
|
+
*/
|
|
296
|
+
getMinWaitTimeMs(modelId = null) {
|
|
297
|
+
return getMinWait(this.#accounts, modelId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get rate limit info for a specific account and model
|
|
302
|
+
* @param {string} email - Email of the account
|
|
303
|
+
* @param {string} modelId - Model ID to check
|
|
304
|
+
* @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info
|
|
305
|
+
*/
|
|
306
|
+
getRateLimitInfo(email, modelId) {
|
|
307
|
+
return getLimitInfo(this.#accounts, email, modelId);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Cooldown Methods (matches opencode-cloudcode-auth)
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Mark an account as cooling down for a specified duration
|
|
316
|
+
* Used for temporary backoff separate from rate limits
|
|
317
|
+
* @param {string} email - Email of the account
|
|
318
|
+
* @param {number} cooldownMs - Duration of cooldown in milliseconds
|
|
319
|
+
* @param {string} [reason] - Reason for the cooldown (use CooldownReason constants)
|
|
320
|
+
*/
|
|
321
|
+
markAccountCoolingDown(email, cooldownMs, reason = CooldownReason.RATE_LIMIT) {
|
|
322
|
+
markCoolingDown(this.#accounts, email, cooldownMs, reason);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if an account is currently cooling down
|
|
327
|
+
* @param {string} email - Email of the account
|
|
328
|
+
* @returns {boolean} True if account is cooling down
|
|
329
|
+
*/
|
|
330
|
+
isAccountCoolingDown(email) {
|
|
331
|
+
const account = this.#accounts.find(a => a.email === email);
|
|
332
|
+
return account ? checkCoolingDown(account) : false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Clear the cooldown for an account
|
|
337
|
+
* @param {string} email - Email of the account
|
|
338
|
+
*/
|
|
339
|
+
clearAccountCooldown(email) {
|
|
340
|
+
const account = this.#accounts.find(a => a.email === email);
|
|
341
|
+
if (account) {
|
|
342
|
+
clearCooldown(account);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get time remaining until cooldown expires for an account
|
|
348
|
+
* @param {string} email - Email of the account
|
|
349
|
+
* @returns {number} Milliseconds until cooldown expires, 0 if not cooling down
|
|
350
|
+
*/
|
|
351
|
+
getCooldownRemaining(email) {
|
|
352
|
+
const account = this.#accounts.find(a => a.email === email);
|
|
353
|
+
return account ? getCooldownMs(account) : 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get OAuth token for an account
|
|
358
|
+
* @param {Object} account - Account object with email and credentials
|
|
359
|
+
* @returns {Promise<string>} OAuth access token
|
|
360
|
+
* @throws {Error} If token refresh fails
|
|
361
|
+
*/
|
|
362
|
+
async getTokenForAccount(account) {
|
|
363
|
+
return fetchToken(
|
|
364
|
+
account,
|
|
365
|
+
this.#tokenCache,
|
|
366
|
+
(email, reason) => this.markInvalid(email, reason),
|
|
367
|
+
() => this.saveToDisk()
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get project ID for an account
|
|
373
|
+
* @param {Object} account - Account object
|
|
374
|
+
* @param {string} token - OAuth access token
|
|
375
|
+
* @returns {Promise<string>} Project ID
|
|
376
|
+
*/
|
|
377
|
+
async getProjectForAccount(account, token) {
|
|
378
|
+
// Pass onSave callback to persist managedProjectId in refresh token
|
|
379
|
+
return fetchProject(account, token, this.#projectCache, () => this.saveToDisk());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Clear project cache for an account (useful on auth errors)
|
|
384
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
385
|
+
*/
|
|
386
|
+
clearProjectCache(email = null) {
|
|
387
|
+
clearProject(this.#projectCache, email);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Clear token cache for an account (useful on auth errors)
|
|
392
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
393
|
+
*/
|
|
394
|
+
clearTokenCache(email = null) {
|
|
395
|
+
clearToken(this.#tokenCache, email);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Save current state to disk (async)
|
|
400
|
+
* @returns {Promise<void>}
|
|
401
|
+
*/
|
|
402
|
+
async saveToDisk() {
|
|
403
|
+
await saveAccounts(this.#configPath, this.#accounts, this.#settings, this.#currentIndex);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get status object for logging/API
|
|
408
|
+
* @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
|
|
409
|
+
*/
|
|
410
|
+
getStatus() {
|
|
411
|
+
const available = this.getAvailableAccounts();
|
|
412
|
+
const invalid = this.getInvalidAccounts();
|
|
413
|
+
|
|
414
|
+
// Count accounts that have any active model-specific rate limits
|
|
415
|
+
const rateLimited = this.#accounts.filter(a => {
|
|
416
|
+
if (!a.modelRateLimits) return false;
|
|
417
|
+
return Object.values(a.modelRateLimits).some(
|
|
418
|
+
limit => limit.isRateLimited && limit.resetTime > Date.now()
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
total: this.#accounts.length,
|
|
424
|
+
available: available.length,
|
|
425
|
+
rateLimited: rateLimited.length,
|
|
426
|
+
invalid: invalid.length,
|
|
427
|
+
summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
|
|
428
|
+
accounts: this.#accounts.map(a => ({
|
|
429
|
+
email: a.email,
|
|
430
|
+
source: a.source,
|
|
431
|
+
enabled: a.enabled !== false, // Default to true if undefined
|
|
432
|
+
projectId: a.projectId || null,
|
|
433
|
+
modelRateLimits: a.modelRateLimits || {},
|
|
434
|
+
isInvalid: a.isInvalid || false,
|
|
435
|
+
invalidReason: a.invalidReason || null,
|
|
436
|
+
lastUsed: a.lastUsed
|
|
437
|
+
}))
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get settings
|
|
443
|
+
* @returns {Object} Current settings object
|
|
444
|
+
*/
|
|
445
|
+
getSettings() {
|
|
446
|
+
return { ...this.#settings };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get all accounts (internal use for quota fetching)
|
|
451
|
+
* Returns the full account objects including credentials
|
|
452
|
+
* @returns {Array<Object>} Array of account objects
|
|
453
|
+
*/
|
|
454
|
+
getAllAccounts() {
|
|
455
|
+
return this.#accounts;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Re-export CooldownReason for use by handlers
|
|
460
|
+
export { CooldownReason };
|
|
461
|
+
|
|
462
|
+
export default AccountManager;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Onboarding
|
|
3
|
+
*
|
|
4
|
+
* Handles provisioning of managed projects for accounts that don't have one.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ONBOARD_USER_ENDPOINTS,
|
|
9
|
+
ANTIGRAVITY_HEADERS
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { sleep } from '../utils/helpers.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the default tier ID from allowed tiers list
|
|
16
|
+
*
|
|
17
|
+
* @param {Array} allowedTiers - List of allowed tiers from loadCodeAssist
|
|
18
|
+
* @returns {string|undefined} Default tier ID
|
|
19
|
+
*/
|
|
20
|
+
export function getDefaultTierId(allowedTiers) {
|
|
21
|
+
if (!allowedTiers || allowedTiers.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Find the tier marked as default
|
|
26
|
+
for (const tier of allowedTiers) {
|
|
27
|
+
if (tier?.isDefault) {
|
|
28
|
+
return tier.id;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fall back to first tier
|
|
33
|
+
return allowedTiers[0]?.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Onboard a user to get a managed project
|
|
38
|
+
*
|
|
39
|
+
* @param {string} token - OAuth access token
|
|
40
|
+
* @param {string} tierId - Tier ID (raw API value, e.g., 'free-tier', 'standard-tier', 'g1-pro-tier')
|
|
41
|
+
* @param {string} [projectId] - Optional GCP project ID (required for non-free tiers)
|
|
42
|
+
* @param {number} [maxAttempts=10] - Maximum polling attempts
|
|
43
|
+
* @param {number} [delayMs=5000] - Delay between polling attempts
|
|
44
|
+
* @returns {Promise<string|null>} Managed project ID or null if failed
|
|
45
|
+
*/
|
|
46
|
+
export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) {
|
|
47
|
+
const metadata = {
|
|
48
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
49
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
50
|
+
pluginType: 'GEMINI'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (projectId) {
|
|
54
|
+
metadata.duetProject = projectId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const requestBody = {
|
|
58
|
+
tierId,
|
|
59
|
+
metadata
|
|
60
|
+
};
|
|
61
|
+
// Note: Do NOT add cloudaicompanionProject to requestBody
|
|
62
|
+
// Reference implementation only sets metadata.duetProject, not the body field
|
|
63
|
+
// Adding cloudaicompanionProject causes 400 errors for auto-provisioned tiers (g1-pro, g1-ultra)
|
|
64
|
+
|
|
65
|
+
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`);
|
|
66
|
+
|
|
67
|
+
for (const endpoint of ONBOARD_USER_ENDPOINTS) {
|
|
68
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`${endpoint}/v1internal:onboardUser`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Authorization': `Bearer ${token}`,
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
...ANTIGRAVITY_HEADERS
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify(requestBody)
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorText = await response.text();
|
|
82
|
+
logger.warn(`[Onboarding] onboardUser failed at ${endpoint}: ${response.status} - ${errorText}`);
|
|
83
|
+
break; // Try next endpoint
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
logger.debug(`[Onboarding] onboardUser response (attempt ${attempt + 1}):`, JSON.stringify(data));
|
|
88
|
+
|
|
89
|
+
// Check if onboarding is complete
|
|
90
|
+
const managedProjectId = data.response?.cloudaicompanionProject?.id;
|
|
91
|
+
if (data.done && managedProjectId) {
|
|
92
|
+
return managedProjectId;
|
|
93
|
+
}
|
|
94
|
+
if (data.done && projectId) {
|
|
95
|
+
return projectId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Not done yet, wait and retry
|
|
99
|
+
if (attempt < maxAttempts - 1) {
|
|
100
|
+
logger.debug(`[Onboarding] onboardUser not complete, waiting ${delayMs}ms...`);
|
|
101
|
+
await sleep(delayMs);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.warn(`[Onboarding] onboardUser error at ${endpoint}:`, error.message);
|
|
105
|
+
break; // Try next endpoint
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
logger.warn(`[Onboarding] All onboarding attempts failed for tierId: ${tierId}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|