@kamel-ahmed/proxy-claude 1.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 +622 -0
- package/bin/cli.js +124 -0
- package/package.json +80 -0
- package/public/app.js +228 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.svg +10 -0
- package/public/index.html +381 -0
- package/public/js/components/account-manager.js +245 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +589 -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 +386 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +78 -0
- package/public/js/translations/en.js +351 -0
- package/public/js/translations/id.js +396 -0
- package/public/js/translations/pt.js +287 -0
- package/public/js/translations/tr.js +342 -0
- package/public/js/translations/zh.js +357 -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/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/views/accounts.html +329 -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 +1329 -0
- package/src/account-manager/credentials.js +243 -0
- package/src/account-manager/index.js +380 -0
- package/src/account-manager/onboarding.js +117 -0
- package/src/account-manager/rate-limits.js +237 -0
- package/src/account-manager/storage.js +136 -0
- package/src/account-manager/strategies/base-strategy.js +104 -0
- package/src/account-manager/strategies/hybrid-strategy.js +195 -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 +8 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +419 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +512 -0
- package/src/cli/refresh.js +201 -0
- package/src/cli/setup.js +338 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +386 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +181 -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 +492 -0
- package/src/config.js +107 -0
- package/src/constants.js +278 -0
- package/src/errors.js +238 -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 +248 -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 +558 -0
- package/src/index.js +146 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/server.js +861 -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 +707 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials Management
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth token handling and project discovery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ANTIGRAVITY_DB_PATH,
|
|
9
|
+
TOKEN_REFRESH_INTERVAL_MS,
|
|
10
|
+
LOAD_CODE_ASSIST_ENDPOINTS,
|
|
11
|
+
LOAD_CODE_ASSIST_HEADERS,
|
|
12
|
+
DEFAULT_PROJECT_ID
|
|
13
|
+
} from '../constants.js';
|
|
14
|
+
import { refreshAccessToken } from '../auth/oauth.js';
|
|
15
|
+
import { getAuthStatus } from '../auth/database.js';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
import { isNetworkError } from '../utils/helpers.js';
|
|
18
|
+
import { onboardUser, getDefaultTierId } from './onboarding.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get OAuth token for an account
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} account - Account object with email and credentials
|
|
24
|
+
* @param {Map} tokenCache - Token cache map
|
|
25
|
+
* @param {Function} onInvalid - Callback when account is invalid (email, reason)
|
|
26
|
+
* @param {Function} onSave - Callback to save changes
|
|
27
|
+
* @returns {Promise<string>} OAuth access token
|
|
28
|
+
* @throws {Error} If token refresh fails
|
|
29
|
+
*/
|
|
30
|
+
export async function getTokenForAccount(account, tokenCache, onInvalid, onSave) {
|
|
31
|
+
// Check cache first
|
|
32
|
+
const cached = tokenCache.get(account.email);
|
|
33
|
+
if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
|
|
34
|
+
return cached.token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get fresh token based on source
|
|
38
|
+
let token;
|
|
39
|
+
|
|
40
|
+
if (account.source === 'oauth' && account.refreshToken) {
|
|
41
|
+
// OAuth account - use refresh token to get new access token
|
|
42
|
+
try {
|
|
43
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
44
|
+
token = tokens.accessToken;
|
|
45
|
+
// Clear invalid flag on success
|
|
46
|
+
if (account.isInvalid) {
|
|
47
|
+
account.isInvalid = false;
|
|
48
|
+
account.invalidReason = null;
|
|
49
|
+
if (onSave) await onSave();
|
|
50
|
+
}
|
|
51
|
+
logger.success(`[AccountManager] Refreshed OAuth token for: ${account.email}`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Check if it's a transient network error
|
|
54
|
+
if (isNetworkError(error)) {
|
|
55
|
+
logger.warn(`[AccountManager] Failed to refresh token for ${account.email} due to network error: ${error.message}`);
|
|
56
|
+
// Do NOT mark as invalid, just throw so caller knows it failed
|
|
57
|
+
throw new Error(`AUTH_NETWORK_ERROR: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.error(`[AccountManager] Failed to refresh token for ${account.email}:`, error.message);
|
|
61
|
+
// Mark account as invalid (credentials need re-auth)
|
|
62
|
+
if (onInvalid) onInvalid(account.email, error.message);
|
|
63
|
+
throw new Error(`AUTH_INVALID: ${account.email}: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
} else if (account.source === 'manual' && account.apiKey) {
|
|
66
|
+
token = account.apiKey;
|
|
67
|
+
} else {
|
|
68
|
+
// Extract from database
|
|
69
|
+
const dbPath = account.dbPath || ANTIGRAVITY_DB_PATH;
|
|
70
|
+
const authData = getAuthStatus(dbPath);
|
|
71
|
+
token = authData.apiKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Cache the token
|
|
75
|
+
tokenCache.set(account.email, {
|
|
76
|
+
token,
|
|
77
|
+
extractedAt: Date.now()
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return token;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get project ID for an account
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} account - Account object
|
|
87
|
+
* @param {string} token - OAuth access token
|
|
88
|
+
* @param {Map} projectCache - Project cache map
|
|
89
|
+
* @returns {Promise<string>} Project ID
|
|
90
|
+
*/
|
|
91
|
+
export async function getProjectForAccount(account, token, projectCache) {
|
|
92
|
+
// Check cache first
|
|
93
|
+
const cached = projectCache.get(account.email);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// OAuth or manual accounts may have projectId specified
|
|
99
|
+
if (account.projectId) {
|
|
100
|
+
projectCache.set(account.email, account.projectId);
|
|
101
|
+
return account.projectId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Discover project via loadCodeAssist API
|
|
105
|
+
const project = await discoverProject(token);
|
|
106
|
+
projectCache.set(account.email, project);
|
|
107
|
+
return project;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Discover project ID via Cloud Code API
|
|
112
|
+
*
|
|
113
|
+
* @param {string} token - OAuth access token
|
|
114
|
+
* @returns {Promise<string>} Project ID
|
|
115
|
+
*/
|
|
116
|
+
export async function discoverProject(token) {
|
|
117
|
+
let lastError = null;
|
|
118
|
+
let gotSuccessfulResponse = false;
|
|
119
|
+
let loadCodeAssistData = null;
|
|
120
|
+
|
|
121
|
+
for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Authorization': `Bearer ${token}`,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
...LOAD_CODE_ASSIST_HEADERS
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
metadata: {
|
|
132
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
133
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
134
|
+
pluginType: 'GEMINI',
|
|
135
|
+
duetProject: DEFAULT_PROJECT_ID
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const errorText = await response.text();
|
|
142
|
+
lastError = `${response.status} - ${errorText}`;
|
|
143
|
+
logger.debug(`[AccountManager] loadCodeAssist failed at ${endpoint}: ${lastError}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
gotSuccessfulResponse = true;
|
|
149
|
+
loadCodeAssistData = data;
|
|
150
|
+
|
|
151
|
+
logger.debug(`[AccountManager] loadCodeAssist response from ${endpoint}:`, JSON.stringify(data));
|
|
152
|
+
|
|
153
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
154
|
+
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject}`);
|
|
155
|
+
return data.cloudaicompanionProject;
|
|
156
|
+
}
|
|
157
|
+
if (data.cloudaicompanionProject?.id) {
|
|
158
|
+
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject.id}`);
|
|
159
|
+
return data.cloudaicompanionProject.id;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// No project found - log tier data and try to onboard the user
|
|
163
|
+
logger.info(`[AccountManager] No project in loadCodeAssist response, attempting onboardUser...`);
|
|
164
|
+
logger.debug(`[AccountManager] Tier data for onboarding: paidTier=${JSON.stringify(data.paidTier)}, currentTier=${JSON.stringify(data.currentTier)}, allowedTiers=${JSON.stringify(data.allowedTiers?.map(t => ({ id: t?.id, isDefault: t?.isDefault })))}`);
|
|
165
|
+
break;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
lastError = error.message;
|
|
168
|
+
logger.debug(`[AccountManager] loadCodeAssist error at ${endpoint}:`, error.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// If we got a successful response but no project, try onboarding
|
|
173
|
+
if (gotSuccessfulResponse && loadCodeAssistData) {
|
|
174
|
+
// Priority: paidTier > currentTier > allowedTiers (consistent with model-api.js)
|
|
175
|
+
let tierId = null;
|
|
176
|
+
let tierSource = null;
|
|
177
|
+
|
|
178
|
+
if (loadCodeAssistData.paidTier?.id) {
|
|
179
|
+
tierId = loadCodeAssistData.paidTier.id;
|
|
180
|
+
tierSource = 'paidTier';
|
|
181
|
+
} else if (loadCodeAssistData.currentTier?.id) {
|
|
182
|
+
tierId = loadCodeAssistData.currentTier.id;
|
|
183
|
+
tierSource = 'currentTier';
|
|
184
|
+
} else {
|
|
185
|
+
tierId = getDefaultTierId(loadCodeAssistData.allowedTiers);
|
|
186
|
+
tierSource = 'allowedTiers';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
tierId = tierId || 'free-tier';
|
|
190
|
+
logger.info(`[AccountManager] Onboarding user with tier: ${tierId} (source: ${tierSource})`);
|
|
191
|
+
|
|
192
|
+
// Check if this is a free tier (raw API values contain 'free')
|
|
193
|
+
const isFree = tierId.toLowerCase().includes('free');
|
|
194
|
+
|
|
195
|
+
// For non-free tiers, pass DEFAULT_PROJECT_ID as the GCP project
|
|
196
|
+
// The API requires a project for paid tier onboarding
|
|
197
|
+
const onboardedProject = await onboardUser(
|
|
198
|
+
token,
|
|
199
|
+
tierId,
|
|
200
|
+
isFree ? null : DEFAULT_PROJECT_ID
|
|
201
|
+
);
|
|
202
|
+
if (onboardedProject) {
|
|
203
|
+
logger.success(`[AccountManager] Successfully onboarded, project: ${onboardedProject}`);
|
|
204
|
+
return onboardedProject;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
logger.warn(`[AccountManager] Onboarding failed, using default project: ${DEFAULT_PROJECT_ID}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Only warn if all endpoints failed with errors (not just missing project)
|
|
211
|
+
if (!gotSuccessfulResponse) {
|
|
212
|
+
logger.warn(`[AccountManager] loadCodeAssist failed for all endpoints: ${lastError}`);
|
|
213
|
+
}
|
|
214
|
+
return DEFAULT_PROJECT_ID;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clear project cache for an account
|
|
219
|
+
*
|
|
220
|
+
* @param {Map} projectCache - Project cache map
|
|
221
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
222
|
+
*/
|
|
223
|
+
export function clearProjectCache(projectCache, email = null) {
|
|
224
|
+
if (email) {
|
|
225
|
+
projectCache.delete(email);
|
|
226
|
+
} else {
|
|
227
|
+
projectCache.clear();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Clear token cache for an account
|
|
233
|
+
*
|
|
234
|
+
* @param {Map} tokenCache - Token cache map
|
|
235
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
236
|
+
*/
|
|
237
|
+
export function clearTokenCache(tokenCache, email = null) {
|
|
238
|
+
if (email) {
|
|
239
|
+
tokenCache.delete(email);
|
|
240
|
+
} else {
|
|
241
|
+
tokenCache.clear();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
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
|
+
} from './rate-limits.js';
|
|
20
|
+
import {
|
|
21
|
+
getTokenForAccount as fetchToken,
|
|
22
|
+
getProjectForAccount as fetchProject,
|
|
23
|
+
clearProjectCache as clearProject,
|
|
24
|
+
clearTokenCache as clearToken
|
|
25
|
+
} from './credentials.js';
|
|
26
|
+
import { createStrategy, getStrategyLabel, DEFAULT_STRATEGY } from './strategies/index.js';
|
|
27
|
+
import { logger } from '../utils/logger.js';
|
|
28
|
+
import { config } from '../config.js';
|
|
29
|
+
|
|
30
|
+
export class AccountManager {
|
|
31
|
+
#accounts = [];
|
|
32
|
+
#currentIndex = 0;
|
|
33
|
+
#configPath;
|
|
34
|
+
#settings = {};
|
|
35
|
+
#initialized = false;
|
|
36
|
+
#strategy = null;
|
|
37
|
+
#strategyName = DEFAULT_STRATEGY;
|
|
38
|
+
|
|
39
|
+
// Per-account caches
|
|
40
|
+
#tokenCache = new Map(); // email -> { token, extractedAt }
|
|
41
|
+
#projectCache = new Map(); // email -> projectId
|
|
42
|
+
|
|
43
|
+
constructor(configPath = ACCOUNT_CONFIG_PATH, strategyName = null) {
|
|
44
|
+
this.#configPath = configPath;
|
|
45
|
+
// Strategy name can be set at construction or later via initialize
|
|
46
|
+
if (strategyName) {
|
|
47
|
+
this.#strategyName = strategyName;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the account manager by loading config
|
|
53
|
+
* @param {string} [strategyOverride] - Override strategy name (from CLI flag or env var)
|
|
54
|
+
*/
|
|
55
|
+
async initialize(strategyOverride = null) {
|
|
56
|
+
if (this.#initialized) return;
|
|
57
|
+
|
|
58
|
+
const { accounts, settings, activeIndex } = await loadAccounts(this.#configPath);
|
|
59
|
+
|
|
60
|
+
this.#accounts = accounts;
|
|
61
|
+
this.#settings = settings;
|
|
62
|
+
this.#currentIndex = activeIndex;
|
|
63
|
+
|
|
64
|
+
// If config exists but has no accounts, fall back to Antigravity database
|
|
65
|
+
if (this.#accounts.length === 0) {
|
|
66
|
+
logger.warn('[AccountManager] No accounts in config. Falling back to Antigravity database');
|
|
67
|
+
const { accounts: defaultAccounts, tokenCache } = loadDefaultAccount();
|
|
68
|
+
this.#accounts = defaultAccounts;
|
|
69
|
+
this.#tokenCache = tokenCache;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine strategy: CLI override > env var > config file > default
|
|
73
|
+
const configStrategy = config?.accountSelection?.strategy;
|
|
74
|
+
const envStrategy = process.env.ACCOUNT_STRATEGY;
|
|
75
|
+
this.#strategyName = strategyOverride || envStrategy || configStrategy || this.#strategyName;
|
|
76
|
+
|
|
77
|
+
// Create the strategy instance
|
|
78
|
+
const strategyConfig = config?.accountSelection || {};
|
|
79
|
+
this.#strategy = createStrategy(this.#strategyName, strategyConfig);
|
|
80
|
+
logger.info(`[AccountManager] Using ${getStrategyLabel(this.#strategyName)} selection strategy`);
|
|
81
|
+
|
|
82
|
+
// Clear any expired rate limits
|
|
83
|
+
this.clearExpiredLimits();
|
|
84
|
+
|
|
85
|
+
this.#initialized = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reload accounts from disk (force re-initialization)
|
|
90
|
+
* Useful when accounts.json is modified externally (e.g., by WebUI)
|
|
91
|
+
*/
|
|
92
|
+
async reload() {
|
|
93
|
+
this.#initialized = false;
|
|
94
|
+
await this.initialize();
|
|
95
|
+
logger.info('[AccountManager] Accounts reloaded from disk');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the number of accounts
|
|
100
|
+
* @returns {number} Number of configured accounts
|
|
101
|
+
*/
|
|
102
|
+
getAccountCount() {
|
|
103
|
+
return this.#accounts.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if all accounts are rate-limited
|
|
108
|
+
* @param {string} [modelId] - Optional model ID
|
|
109
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
110
|
+
*/
|
|
111
|
+
isAllRateLimited(modelId = null) {
|
|
112
|
+
return checkAllRateLimited(this.#accounts, modelId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get list of available (non-rate-limited, non-invalid) accounts
|
|
117
|
+
* @param {string} [modelId] - Optional model ID
|
|
118
|
+
* @returns {Array<Object>} Array of available account objects
|
|
119
|
+
*/
|
|
120
|
+
getAvailableAccounts(modelId = null) {
|
|
121
|
+
return getAvailable(this.#accounts, modelId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get list of invalid accounts
|
|
126
|
+
* @returns {Array<Object>} Array of invalid account objects
|
|
127
|
+
*/
|
|
128
|
+
getInvalidAccounts() {
|
|
129
|
+
return getInvalid(this.#accounts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Clear expired rate limits
|
|
134
|
+
* @returns {number} Number of rate limits cleared
|
|
135
|
+
*/
|
|
136
|
+
clearExpiredLimits() {
|
|
137
|
+
const cleared = clearLimits(this.#accounts);
|
|
138
|
+
if (cleared > 0) {
|
|
139
|
+
this.saveToDisk();
|
|
140
|
+
}
|
|
141
|
+
return cleared;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all rate limits to force a fresh check
|
|
146
|
+
* (Optimistic retry strategy)
|
|
147
|
+
* @returns {void}
|
|
148
|
+
*/
|
|
149
|
+
resetAllRateLimits() {
|
|
150
|
+
resetLimits(this.#accounts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Select an account using the configured strategy.
|
|
155
|
+
* This is the main method to use for account selection.
|
|
156
|
+
* @param {string} [modelId] - Model ID for the request
|
|
157
|
+
* @param {Object} [options] - Additional options
|
|
158
|
+
* @param {string} [options.sessionId] - Session ID for cache continuity
|
|
159
|
+
* @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
|
|
160
|
+
*/
|
|
161
|
+
selectAccount(modelId = null, options = {}) {
|
|
162
|
+
if (!this.#strategy) {
|
|
163
|
+
throw new Error('AccountManager not initialized. Call initialize() first.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = this.#strategy.selectAccount(this.#accounts, modelId, {
|
|
167
|
+
currentIndex: this.#currentIndex,
|
|
168
|
+
onSave: () => this.saveToDisk(),
|
|
169
|
+
...options
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this.#currentIndex = result.index;
|
|
173
|
+
return { account: result.account, waitMs: result.waitMs || 0 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Notify the strategy of a successful request
|
|
178
|
+
* @param {Object} account - The account that was used
|
|
179
|
+
* @param {string} modelId - The model ID that was used
|
|
180
|
+
*/
|
|
181
|
+
notifySuccess(account, modelId) {
|
|
182
|
+
if (this.#strategy) {
|
|
183
|
+
this.#strategy.onSuccess(account, modelId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Notify the strategy of a rate limit
|
|
189
|
+
* @param {Object} account - The account that was rate-limited
|
|
190
|
+
* @param {string} modelId - The model ID that was rate-limited
|
|
191
|
+
*/
|
|
192
|
+
notifyRateLimit(account, modelId) {
|
|
193
|
+
if (this.#strategy) {
|
|
194
|
+
this.#strategy.onRateLimit(account, modelId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Notify the strategy of a failure
|
|
200
|
+
* @param {Object} account - The account that failed
|
|
201
|
+
* @param {string} modelId - The model ID that failed
|
|
202
|
+
*/
|
|
203
|
+
notifyFailure(account, modelId) {
|
|
204
|
+
if (this.#strategy) {
|
|
205
|
+
this.#strategy.onFailure(account, modelId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the current strategy name
|
|
211
|
+
* @returns {string} Strategy name
|
|
212
|
+
*/
|
|
213
|
+
getStrategyName() {
|
|
214
|
+
return this.#strategyName;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the strategy display label
|
|
219
|
+
* @returns {string} Strategy display label
|
|
220
|
+
*/
|
|
221
|
+
getStrategyLabel() {
|
|
222
|
+
return getStrategyLabel(this.#strategyName);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get the health tracker from the current strategy (if available)
|
|
227
|
+
* Used by handlers for consecutive failure tracking
|
|
228
|
+
* Only available when using hybrid strategy
|
|
229
|
+
* @returns {Object|null} Health tracker instance or null if not available
|
|
230
|
+
*/
|
|
231
|
+
getHealthTracker() {
|
|
232
|
+
if (this.#strategy && typeof this.#strategy.getHealthTracker === 'function') {
|
|
233
|
+
return this.#strategy.getHealthTracker();
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Mark an account as rate-limited
|
|
240
|
+
* @param {string} email - Email of the account to mark
|
|
241
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
|
|
242
|
+
* @param {string} [modelId] - Optional model ID to mark specific limit
|
|
243
|
+
*/
|
|
244
|
+
markRateLimited(email, resetMs = null, modelId = null) {
|
|
245
|
+
markLimited(this.#accounts, email, resetMs, modelId);
|
|
246
|
+
this.saveToDisk();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
251
|
+
* @param {string} email - Email of the account to mark
|
|
252
|
+
* @param {string} reason - Reason for marking as invalid
|
|
253
|
+
*/
|
|
254
|
+
markInvalid(email, reason = 'Unknown error') {
|
|
255
|
+
markAccountInvalid(this.#accounts, email, reason);
|
|
256
|
+
this.saveToDisk();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get the minimum wait time until any account becomes available
|
|
261
|
+
* @param {string} [modelId] - Optional model ID
|
|
262
|
+
* @returns {number} Wait time in milliseconds
|
|
263
|
+
*/
|
|
264
|
+
getMinWaitTimeMs(modelId = null) {
|
|
265
|
+
return getMinWait(this.#accounts, modelId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get rate limit info for a specific account and model
|
|
270
|
+
* @param {string} email - Email of the account
|
|
271
|
+
* @param {string} modelId - Model ID to check
|
|
272
|
+
* @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info
|
|
273
|
+
*/
|
|
274
|
+
getRateLimitInfo(email, modelId) {
|
|
275
|
+
return getLimitInfo(this.#accounts, email, modelId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get OAuth token for an account
|
|
280
|
+
* @param {Object} account - Account object with email and credentials
|
|
281
|
+
* @returns {Promise<string>} OAuth access token
|
|
282
|
+
* @throws {Error} If token refresh fails
|
|
283
|
+
*/
|
|
284
|
+
async getTokenForAccount(account) {
|
|
285
|
+
return fetchToken(
|
|
286
|
+
account,
|
|
287
|
+
this.#tokenCache,
|
|
288
|
+
(email, reason) => this.markInvalid(email, reason),
|
|
289
|
+
() => this.saveToDisk()
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get project ID for an account
|
|
295
|
+
* @param {Object} account - Account object
|
|
296
|
+
* @param {string} token - OAuth access token
|
|
297
|
+
* @returns {Promise<string>} Project ID
|
|
298
|
+
*/
|
|
299
|
+
async getProjectForAccount(account, token) {
|
|
300
|
+
return fetchProject(account, token, this.#projectCache);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Clear project cache for an account (useful on auth errors)
|
|
305
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
306
|
+
*/
|
|
307
|
+
clearProjectCache(email = null) {
|
|
308
|
+
clearProject(this.#projectCache, email);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Clear token cache for an account (useful on auth errors)
|
|
313
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
314
|
+
*/
|
|
315
|
+
clearTokenCache(email = null) {
|
|
316
|
+
clearToken(this.#tokenCache, email);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Save current state to disk (async)
|
|
321
|
+
* @returns {Promise<void>}
|
|
322
|
+
*/
|
|
323
|
+
async saveToDisk() {
|
|
324
|
+
await saveAccounts(this.#configPath, this.#accounts, this.#settings, this.#currentIndex);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get status object for logging/API
|
|
329
|
+
* @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
|
|
330
|
+
*/
|
|
331
|
+
getStatus() {
|
|
332
|
+
const available = this.getAvailableAccounts();
|
|
333
|
+
const invalid = this.getInvalidAccounts();
|
|
334
|
+
|
|
335
|
+
// Count accounts that have any active model-specific rate limits
|
|
336
|
+
const rateLimited = this.#accounts.filter(a => {
|
|
337
|
+
if (!a.modelRateLimits) return false;
|
|
338
|
+
return Object.values(a.modelRateLimits).some(
|
|
339
|
+
limit => limit.isRateLimited && limit.resetTime > Date.now()
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
total: this.#accounts.length,
|
|
345
|
+
available: available.length,
|
|
346
|
+
rateLimited: rateLimited.length,
|
|
347
|
+
invalid: invalid.length,
|
|
348
|
+
summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
|
|
349
|
+
accounts: this.#accounts.map(a => ({
|
|
350
|
+
email: a.email,
|
|
351
|
+
source: a.source,
|
|
352
|
+
enabled: a.enabled !== false, // Default to true if undefined
|
|
353
|
+
projectId: a.projectId || null,
|
|
354
|
+
modelRateLimits: a.modelRateLimits || {},
|
|
355
|
+
isInvalid: a.isInvalid || false,
|
|
356
|
+
invalidReason: a.invalidReason || null,
|
|
357
|
+
lastUsed: a.lastUsed
|
|
358
|
+
}))
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get settings
|
|
364
|
+
* @returns {Object} Current settings object
|
|
365
|
+
*/
|
|
366
|
+
getSettings() {
|
|
367
|
+
return { ...this.#settings };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get all accounts (internal use for quota fetching)
|
|
372
|
+
* Returns the full account objects including credentials
|
|
373
|
+
* @returns {Array<Object>} Array of account objects
|
|
374
|
+
*/
|
|
375
|
+
getAllAccounts() {
|
|
376
|
+
return this.#accounts;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export default AccountManager;
|