@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,117 @@
|
|
|
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 = null, 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
|
+
|
|
62
|
+
// Check if this is a free tier (handles raw API values like 'free-tier')
|
|
63
|
+
const isFree = tierId.toLowerCase().includes('free');
|
|
64
|
+
|
|
65
|
+
// Non-free tiers require a cloudaicompanionProject
|
|
66
|
+
if (!isFree && projectId) {
|
|
67
|
+
requestBody.cloudaicompanionProject = projectId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}, isFree: ${isFree}`);
|
|
71
|
+
|
|
72
|
+
for (const endpoint of ONBOARD_USER_ENDPOINTS) {
|
|
73
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(`${endpoint}/v1internal:onboardUser`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Authorization': `Bearer ${token}`,
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
...ANTIGRAVITY_HEADERS
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(requestBody)
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const errorText = await response.text();
|
|
87
|
+
logger.warn(`[Onboarding] onboardUser failed at ${endpoint}: ${response.status} - ${errorText}`);
|
|
88
|
+
break; // Try next endpoint
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
logger.debug(`[Onboarding] onboardUser response (attempt ${attempt + 1}):`, JSON.stringify(data));
|
|
93
|
+
|
|
94
|
+
// Check if onboarding is complete
|
|
95
|
+
const managedProjectId = data.response?.cloudaicompanionProject?.id;
|
|
96
|
+
if (data.done && managedProjectId) {
|
|
97
|
+
return managedProjectId;
|
|
98
|
+
}
|
|
99
|
+
if (data.done && projectId) {
|
|
100
|
+
return projectId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Not done yet, wait and retry
|
|
104
|
+
if (attempt < maxAttempts - 1) {
|
|
105
|
+
logger.debug(`[Onboarding] onboardUser not complete, waiting ${delayMs}ms...`);
|
|
106
|
+
await sleep(delayMs);
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.warn(`[Onboarding] onboardUser error at ${endpoint}:`, error.message);
|
|
110
|
+
break; // Try next endpoint
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logger.warn(`[Onboarding] All onboarding attempts failed for tierId: ${tierId}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
// Log appropriately based on duration
|
|
137
|
+
if (actualResetMs > DEFAULT_COOLDOWN_MS) {
|
|
138
|
+
logger.warn(
|
|
139
|
+
`[AccountManager] Quota exhausted: ${email} (model: ${modelId}). Resets in ${formatDuration(actualResetMs)}`
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
logger.warn(
|
|
143
|
+
`[AccountManager] Rate limited: ${email} (model: ${modelId}). Available in ${formatDuration(actualResetMs)}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
152
|
+
*
|
|
153
|
+
* @param {Array} accounts - Array of account objects
|
|
154
|
+
* @param {string} email - Email of the account to mark
|
|
155
|
+
* @param {string} reason - Reason for marking as invalid
|
|
156
|
+
* @returns {boolean} True if account was found and marked
|
|
157
|
+
*/
|
|
158
|
+
export function markInvalid(accounts, email, reason = 'Unknown error') {
|
|
159
|
+
const account = accounts.find(a => a.email === email);
|
|
160
|
+
if (!account) return false;
|
|
161
|
+
|
|
162
|
+
account.isInvalid = true;
|
|
163
|
+
account.invalidReason = reason;
|
|
164
|
+
account.invalidAt = Date.now();
|
|
165
|
+
|
|
166
|
+
logger.error(
|
|
167
|
+
`[AccountManager] ⚠ Account INVALID: ${email}`
|
|
168
|
+
);
|
|
169
|
+
logger.error(
|
|
170
|
+
`[AccountManager] Reason: ${reason}`
|
|
171
|
+
);
|
|
172
|
+
logger.error(
|
|
173
|
+
`[AccountManager] Run 'npm run accounts' to re-authenticate this account`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the minimum wait time until any account becomes available for a model
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} accounts - Array of account objects
|
|
183
|
+
* @param {string} modelId - Model ID to check
|
|
184
|
+
* @returns {number} Wait time in milliseconds
|
|
185
|
+
*/
|
|
186
|
+
export function getMinWaitTimeMs(accounts, modelId) {
|
|
187
|
+
if (!isAllRateLimited(accounts, modelId)) return 0;
|
|
188
|
+
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
let minWait = Infinity;
|
|
191
|
+
let soonestAccount = null;
|
|
192
|
+
|
|
193
|
+
for (const account of accounts) {
|
|
194
|
+
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
195
|
+
const limit = account.modelRateLimits[modelId];
|
|
196
|
+
if (limit.isRateLimited && limit.resetTime) {
|
|
197
|
+
const wait = limit.resetTime - now;
|
|
198
|
+
if (wait > 0 && wait < minWait) {
|
|
199
|
+
minWait = wait;
|
|
200
|
+
soonestAccount = account;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (soonestAccount) {
|
|
207
|
+
logger.info(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get the rate limit info for a specific account and model
|
|
215
|
+
* Returns the actual reset time from API, not capped
|
|
216
|
+
*
|
|
217
|
+
* @param {Array} accounts - Array of account objects
|
|
218
|
+
* @param {string} email - Email of the account
|
|
219
|
+
* @param {string} modelId - Model ID to check
|
|
220
|
+
* @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info
|
|
221
|
+
*/
|
|
222
|
+
export function getRateLimitInfo(accounts, email, modelId) {
|
|
223
|
+
const account = accounts.find(a => a.email === email);
|
|
224
|
+
if (!account || !account.modelRateLimits || !account.modelRateLimits[modelId]) {
|
|
225
|
+
return { isRateLimited: false, actualResetMs: null, waitMs: 0 };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const limit = account.modelRateLimits[modelId];
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const waitMs = limit.resetTime ? Math.max(0, limit.resetTime - now) : 0;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
isRateLimited: limit.isRateLimited && waitMs > 0,
|
|
234
|
+
actualResetMs: limit.actualResetMs || null,
|
|
235
|
+
waitMs
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
* Load accounts from the config file
|
|
16
|
+
*
|
|
17
|
+
* @param {string} configPath - Path to the config file
|
|
18
|
+
* @returns {Promise<{accounts: Array, settings: Object, activeIndex: number}>}
|
|
19
|
+
*/
|
|
20
|
+
export async function loadAccounts(configPath = ACCOUNT_CONFIG_PATH) {
|
|
21
|
+
try {
|
|
22
|
+
// Check if config file exists using async access
|
|
23
|
+
await access(configPath, fsConstants.F_OK);
|
|
24
|
+
const configData = await readFile(configPath, 'utf-8');
|
|
25
|
+
const config = JSON.parse(configData);
|
|
26
|
+
|
|
27
|
+
const accounts = (config.accounts || []).map(acc => ({
|
|
28
|
+
...acc,
|
|
29
|
+
lastUsed: acc.lastUsed || null,
|
|
30
|
+
enabled: acc.enabled !== false, // Default to true if not specified
|
|
31
|
+
// Reset invalid flag on startup - give accounts a fresh chance to refresh
|
|
32
|
+
isInvalid: false,
|
|
33
|
+
invalidReason: null,
|
|
34
|
+
modelRateLimits: acc.modelRateLimits || {},
|
|
35
|
+
// New fields for subscription and quota tracking
|
|
36
|
+
subscription: acc.subscription || { tier: 'unknown', projectId: null, detectedAt: null },
|
|
37
|
+
quota: acc.quota || { models: {}, lastChecked: null }
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const settings = config.settings || {};
|
|
41
|
+
let activeIndex = config.activeIndex || 0;
|
|
42
|
+
|
|
43
|
+
// Clamp activeIndex to valid range
|
|
44
|
+
if (activeIndex >= accounts.length) {
|
|
45
|
+
activeIndex = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logger.info(`[AccountManager] Loaded ${accounts.length} account(s) from config`);
|
|
49
|
+
|
|
50
|
+
return { accounts, settings, activeIndex };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === 'ENOENT') {
|
|
53
|
+
// No config file - return empty
|
|
54
|
+
logger.info('[AccountManager] No config file found. Using Antigravity database (single account mode)');
|
|
55
|
+
} else {
|
|
56
|
+
logger.error('[AccountManager] Failed to load config:', error.message);
|
|
57
|
+
}
|
|
58
|
+
return { accounts: [], settings: {}, activeIndex: 0 };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load the default account from Antigravity's database
|
|
64
|
+
*
|
|
65
|
+
* @param {string} dbPath - Optional path to the database
|
|
66
|
+
* @returns {{accounts: Array, tokenCache: Map}}
|
|
67
|
+
*/
|
|
68
|
+
export function loadDefaultAccount(dbPath) {
|
|
69
|
+
try {
|
|
70
|
+
const authData = getAuthStatus(dbPath);
|
|
71
|
+
if (authData?.apiKey) {
|
|
72
|
+
const account = {
|
|
73
|
+
email: authData.email || 'default@antigravity',
|
|
74
|
+
source: 'database',
|
|
75
|
+
lastUsed: null,
|
|
76
|
+
modelRateLimits: {}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const tokenCache = new Map();
|
|
80
|
+
tokenCache.set(account.email, {
|
|
81
|
+
token: authData.apiKey,
|
|
82
|
+
extractedAt: Date.now()
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
logger.info(`[AccountManager] Loaded default account: ${account.email}`);
|
|
86
|
+
|
|
87
|
+
return { accounts: [account], tokenCache };
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('[AccountManager] Failed to load default account:', error.message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { accounts: [], tokenCache: new Map() };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Save account configuration to disk
|
|
98
|
+
*
|
|
99
|
+
* @param {string} configPath - Path to the config file
|
|
100
|
+
* @param {Array} accounts - Array of account objects
|
|
101
|
+
* @param {Object} settings - Settings object
|
|
102
|
+
* @param {number} activeIndex - Current active account index
|
|
103
|
+
*/
|
|
104
|
+
export async function saveAccounts(configPath, accounts, settings, activeIndex) {
|
|
105
|
+
try {
|
|
106
|
+
// Ensure directory exists
|
|
107
|
+
const dir = dirname(configPath);
|
|
108
|
+
await mkdir(dir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const config = {
|
|
111
|
+
accounts: accounts.map(acc => ({
|
|
112
|
+
email: acc.email,
|
|
113
|
+
source: acc.source,
|
|
114
|
+
enabled: acc.enabled !== false, // Persist enabled state
|
|
115
|
+
dbPath: acc.dbPath || null,
|
|
116
|
+
refreshToken: acc.source === 'oauth' ? acc.refreshToken : undefined,
|
|
117
|
+
apiKey: acc.source === 'manual' ? acc.apiKey : undefined,
|
|
118
|
+
projectId: acc.projectId || undefined,
|
|
119
|
+
addedAt: acc.addedAt || undefined,
|
|
120
|
+
isInvalid: acc.isInvalid || false,
|
|
121
|
+
invalidReason: acc.invalidReason || null,
|
|
122
|
+
modelRateLimits: acc.modelRateLimits || {},
|
|
123
|
+
lastUsed: acc.lastUsed,
|
|
124
|
+
// Persist subscription and quota data
|
|
125
|
+
subscription: acc.subscription || { tier: 'unknown', projectId: null, detectedAt: null },
|
|
126
|
+
quota: acc.quota || { models: {}, lastChecked: null }
|
|
127
|
+
})),
|
|
128
|
+
settings: settings,
|
|
129
|
+
activeIndex: activeIndex
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error('[AccountManager] Failed to save config:', error.message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* @typedef {Object} SelectionResult
|
|
10
|
+
* @property {Object|null} account - The selected account or null if none available
|
|
11
|
+
* @property {number} index - The index of the selected account
|
|
12
|
+
* @property {number} [waitMs] - Optional wait time before account becomes available
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class BaseStrategy {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new BaseStrategy
|
|
18
|
+
* @param {Object} config - Strategy configuration
|
|
19
|
+
*/
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
if (new.target === BaseStrategy) {
|
|
22
|
+
throw new Error('BaseStrategy is abstract and cannot be instantiated directly');
|
|
23
|
+
}
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Select an account for a request
|
|
29
|
+
* @param {Array} accounts - Array of account objects
|
|
30
|
+
* @param {string} modelId - The model ID for the request
|
|
31
|
+
* @param {Object} options - Additional options
|
|
32
|
+
* @param {number} options.currentIndex - Current account index
|
|
33
|
+
* @param {string} [options.sessionId] - Session ID for cache continuity
|
|
34
|
+
* @param {Function} [options.onSave] - Callback to save changes
|
|
35
|
+
* @returns {SelectionResult} The selected account and index
|
|
36
|
+
*/
|
|
37
|
+
selectAccount(accounts, modelId, options = {}) {
|
|
38
|
+
throw new Error('selectAccount must be implemented by subclass');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Called after a successful request
|
|
43
|
+
* @param {Object} account - The account that was used
|
|
44
|
+
* @param {string} modelId - The model ID that was used
|
|
45
|
+
*/
|
|
46
|
+
onSuccess(account, modelId) {
|
|
47
|
+
// Default: no-op, override in subclass if needed
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Called when a request is rate-limited
|
|
52
|
+
* @param {Object} account - The account that was rate-limited
|
|
53
|
+
* @param {string} modelId - The model ID that was rate-limited
|
|
54
|
+
*/
|
|
55
|
+
onRateLimit(account, modelId) {
|
|
56
|
+
// Default: no-op, override in subclass if needed
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Called when a request fails (non-rate-limit error)
|
|
61
|
+
* @param {Object} account - The account that failed
|
|
62
|
+
* @param {string} modelId - The model ID that failed
|
|
63
|
+
*/
|
|
64
|
+
onFailure(account, modelId) {
|
|
65
|
+
// Default: no-op, override in subclass if needed
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if an account is usable for a specific model
|
|
70
|
+
* @param {Object} account - Account object
|
|
71
|
+
* @param {string} modelId - Model ID to check
|
|
72
|
+
* @returns {boolean} True if account is usable
|
|
73
|
+
*/
|
|
74
|
+
isAccountUsable(account, modelId) {
|
|
75
|
+
if (!account || account.isInvalid) return false;
|
|
76
|
+
|
|
77
|
+
// Skip disabled accounts
|
|
78
|
+
if (account.enabled === false) return false;
|
|
79
|
+
|
|
80
|
+
// Check model-specific rate limit
|
|
81
|
+
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
82
|
+
const limit = account.modelRateLimits[modelId];
|
|
83
|
+
if (limit.isRateLimited && limit.resetTime > Date.now()) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all usable accounts for a model
|
|
93
|
+
* @param {Array} accounts - Array of account objects
|
|
94
|
+
* @param {string} modelId - Model ID to check
|
|
95
|
+
* @returns {Array} Array of usable accounts with their original indices
|
|
96
|
+
*/
|
|
97
|
+
getUsableAccounts(accounts, modelId) {
|
|
98
|
+
return accounts
|
|
99
|
+
.map((account, index) => ({ account, index }))
|
|
100
|
+
.filter(({ account }) => this.isAccountUsable(account, modelId));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default BaseStrategy;
|