@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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. 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;