@pikoloo/codex-proxy 1.0.7 → 1.2.2
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/CHANGELOG.md +76 -0
- package/README.md +28 -11
- package/bin/cli.js +15 -15
- package/docs/ACCOUNT.md +104 -0
- package/docs/API.md +21 -29
- package/docs/ARCHITECTURE.md +9 -9
- package/docs/CLAUDE_INTEGRATION.md +3 -3
- package/docs/OAUTH.md +13 -13
- package/docs/OPENCLAW.md +1 -1
- package/docs/legal.md +6 -0
- package/images/dashboard-screenshot.png +0 -0
- package/images/readme-cover.png +0 -0
- package/images/settings-screenshot.png +0 -0
- package/package.json +19 -10
- package/public/css/style.css +802 -22
- package/public/index.html +236 -338
- package/public/js/app.js +140 -118
- package/src/account-manager.js +210 -292
- package/src/cli/account.js +236 -0
- package/src/direct-api.js +7 -9
- package/src/index.js +7 -7
- package/src/middleware/credentials.js +6 -47
- package/src/oauth.js +2 -1
- package/src/routes/{accounts-route.js → account-route.js} +25 -109
- package/src/routes/api-routes.js +18 -30
- package/src/routes/chat-route.js +3 -3
- package/src/routes/messages-route.js +37 -199
- package/src/routes/models-route.js +11 -21
- package/src/routes/settings-route.js +1 -41
- package/src/security.js +1 -1
- package/src/server-settings.js +30 -38
- package/src/utils/logger.js +14 -1
- package/docs/ACCOUNTS.md +0 -202
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/src/account-rotation/index.js +0 -93
- package/src/account-rotation/rate-limits.js +0 -293
- package/src/account-rotation/strategies/base-strategy.js +0 -48
- package/src/account-rotation/strategies/index.js +0 -31
- package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
- package/src/account-rotation/strategies/sticky-strategy.js +0 -97
- package/src/cli/accounts.js +0 -557
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { logger } from '../utils/logger.js';
|
|
2
|
-
|
|
3
|
-
export const CooldownReason = {
|
|
4
|
-
RATE_LIMIT: 'RATE_LIMIT',
|
|
5
|
-
AUTH_FAILURE: 'AUTH_FAILURE',
|
|
6
|
-
CONSECUTIVE_FAILURES: 'CONSECUTIVE_FAILURES',
|
|
7
|
-
SERVER_ERROR: 'SERVER_ERROR'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const DEFAULT_COOLDOWN_MS = 60000;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Check if all accounts are rate-limited for a specific model
|
|
14
|
-
* @param {Array} accounts - Array of account objects
|
|
15
|
-
* @param {string} modelId - Model identifier
|
|
16
|
-
* @returns {boolean} True if all accounts are rate-limited
|
|
17
|
-
*/
|
|
18
|
-
export function isAllRateLimited(accounts, modelId) {
|
|
19
|
-
if (!accounts || accounts.length === 0) return true;
|
|
20
|
-
return accounts.every(account => {
|
|
21
|
-
const rateLimit = account.modelRateLimits?.[modelId];
|
|
22
|
-
return rateLimit?.isRateLimited && rateLimit.resetTime > Date.now();
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get list of accounts that are not rate-limited for a specific model
|
|
28
|
-
* @param {Array} accounts - Array of account objects
|
|
29
|
-
* @param {string} modelId - Model identifier
|
|
30
|
-
* @returns {Array} Array of available account objects
|
|
31
|
-
*/
|
|
32
|
-
export function getAvailableAccounts(accounts, modelId) {
|
|
33
|
-
if (!accounts || accounts.length === 0) return [];
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
return accounts.filter(account => {
|
|
36
|
-
if (account.isInvalid) return false;
|
|
37
|
-
if (isAccountCoolingDown(account)) return false;
|
|
38
|
-
const rateLimit = account.modelRateLimits?.[modelId];
|
|
39
|
-
return !rateLimit?.isRateLimited || rateLimit.resetTime <= now;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Clear all expired rate limits across all accounts
|
|
45
|
-
* @param {Array} accounts - Array of account objects
|
|
46
|
-
* @returns {number} Count of cleared rate limits
|
|
47
|
-
*/
|
|
48
|
-
export function clearExpiredLimits(accounts) {
|
|
49
|
-
if (!accounts || accounts.length === 0) return 0;
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
let clearedCount = 0;
|
|
52
|
-
|
|
53
|
-
for (const account of accounts) {
|
|
54
|
-
if (!account.modelRateLimits) continue;
|
|
55
|
-
for (const modelId of Object.keys(account.modelRateLimits)) {
|
|
56
|
-
const rateLimit = account.modelRateLimits[modelId];
|
|
57
|
-
if (rateLimit?.isRateLimited && rateLimit.resetTime <= now) {
|
|
58
|
-
account.modelRateLimits[modelId] = {
|
|
59
|
-
isRateLimited: false,
|
|
60
|
-
resetTime: null,
|
|
61
|
-
actualResetMs: null
|
|
62
|
-
};
|
|
63
|
-
clearedCount++;
|
|
64
|
-
logger.debug(`Cleared expired rate limit for ${account.email} model ${modelId}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return clearedCount;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Reset all rate limits for all accounts (optimistic retry)
|
|
74
|
-
* @param {Array} accounts - Array of account objects
|
|
75
|
-
*/
|
|
76
|
-
export function resetAllRateLimits(accounts) {
|
|
77
|
-
if (!accounts || accounts.length === 0) return;
|
|
78
|
-
for (const account of accounts) {
|
|
79
|
-
account.modelRateLimits = {};
|
|
80
|
-
}
|
|
81
|
-
logger.info('Reset all rate limits for all accounts (optimistic retry)');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Mark an account as rate-limited for a specific model
|
|
86
|
-
* @param {Array} accounts - Array of account objects
|
|
87
|
-
* @param {string} email - Account email
|
|
88
|
-
* @param {number} resetMs - Reset time in milliseconds
|
|
89
|
-
* @param {string} modelId - Model identifier
|
|
90
|
-
*/
|
|
91
|
-
export function markRateLimited(accounts, email, resetMs, modelId) {
|
|
92
|
-
const account = accounts?.find(a => a.email === email);
|
|
93
|
-
if (!account) {
|
|
94
|
-
logger.warn(`Account not found: ${email}`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!account.modelRateLimits) {
|
|
99
|
-
account.modelRateLimits = {};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
account.modelRateLimits[modelId] = {
|
|
103
|
-
isRateLimited: true,
|
|
104
|
-
resetTime: Date.now() + resetMs,
|
|
105
|
-
actualResetMs: resetMs
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
logger.debug(`Rate limited ${email} for model ${modelId} for ${resetMs}ms`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Mark an account as invalid
|
|
113
|
-
* @param {Array} accounts - Array of account objects
|
|
114
|
-
* @param {string} email - Account email
|
|
115
|
-
* @param {string} reason - Reason for invalid status
|
|
116
|
-
*/
|
|
117
|
-
export function markInvalid(accounts, email, reason) {
|
|
118
|
-
const account = accounts?.find(a => a.email === email);
|
|
119
|
-
if (!account) {
|
|
120
|
-
logger.warn(`Account not found: ${email}`);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
account.isInvalid = true;
|
|
125
|
-
account.invalidReason = reason;
|
|
126
|
-
account.invalidAt = Date.now();
|
|
127
|
-
|
|
128
|
-
logger.warn(`Marked account ${email} as invalid: ${reason}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Clear invalid status for an account
|
|
133
|
-
* @param {Array} accounts - Array of account objects
|
|
134
|
-
* @param {string} email - Account email
|
|
135
|
-
*/
|
|
136
|
-
export function clearInvalid(accounts, email) {
|
|
137
|
-
const account = accounts?.find(a => a.email === email);
|
|
138
|
-
if (!account) {
|
|
139
|
-
logger.warn(`Account not found: ${email}`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
account.isInvalid = false;
|
|
144
|
-
account.invalidReason = null;
|
|
145
|
-
account.invalidAt = null;
|
|
146
|
-
|
|
147
|
-
logger.info(`Cleared invalid status for account ${email}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get minimum wait time until any account is available for a model
|
|
152
|
-
* @param {Array} accounts - Array of account objects
|
|
153
|
-
* @param {string} modelId - Model identifier
|
|
154
|
-
* @returns {number} Minimum wait time in milliseconds, 0 if any account available
|
|
155
|
-
*/
|
|
156
|
-
export function getMinWaitTimeMs(accounts, modelId) {
|
|
157
|
-
if (!accounts || accounts.length === 0) return 0;
|
|
158
|
-
|
|
159
|
-
const available = getAvailableAccounts(accounts, modelId);
|
|
160
|
-
if (available.length > 0) return 0;
|
|
161
|
-
|
|
162
|
-
const now = Date.now();
|
|
163
|
-
let minWait = Infinity;
|
|
164
|
-
|
|
165
|
-
for (const account of accounts) {
|
|
166
|
-
if (account.isInvalid) continue;
|
|
167
|
-
|
|
168
|
-
const cooldownRemaining = getCooldownRemaining(account);
|
|
169
|
-
if (cooldownRemaining > 0 && cooldownRemaining < minWait) {
|
|
170
|
-
minWait = cooldownRemaining;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const rateLimit = account.modelRateLimits?.[modelId];
|
|
174
|
-
if (rateLimit?.isRateLimited && rateLimit.resetTime > now) {
|
|
175
|
-
const waitTime = rateLimit.resetTime - now;
|
|
176
|
-
if (waitTime < minWait) {
|
|
177
|
-
minWait = waitTime;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return minWait === Infinity ? 0 : minWait;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get rate limit info for a specific account and model
|
|
187
|
-
* @param {Array} accounts - Array of account objects
|
|
188
|
-
* @param {string} email - Account email
|
|
189
|
-
* @param {string} modelId - Model identifier
|
|
190
|
-
* @returns {Object|null} Rate limit info object or null
|
|
191
|
-
*/
|
|
192
|
-
export function getRateLimitInfo(accounts, email, modelId) {
|
|
193
|
-
const account = accounts?.find(a => a.email === email);
|
|
194
|
-
if (!account) return null;
|
|
195
|
-
|
|
196
|
-
return account.modelRateLimits?.[modelId] || null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check if an account is currently cooling down
|
|
201
|
-
* @param {Object} account - Account object
|
|
202
|
-
* @returns {boolean} True if account is cooling down
|
|
203
|
-
*/
|
|
204
|
-
export function isAccountCoolingDown(account) {
|
|
205
|
-
if (!account?.cooldownUntil) return false;
|
|
206
|
-
return account.cooldownUntil > Date.now();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Mark an account as cooling down
|
|
211
|
-
* @param {Array} accounts - Array of account objects
|
|
212
|
-
* @param {string} email - Account email
|
|
213
|
-
* @param {number} cooldownMs - Cooldown duration in milliseconds
|
|
214
|
-
* @param {string} reason - Reason for cooldown (from CooldownReason)
|
|
215
|
-
*/
|
|
216
|
-
export function markAccountCoolingDown(accounts, email, cooldownMs = DEFAULT_COOLDOWN_MS, reason) {
|
|
217
|
-
const account = accounts?.find(a => a.email === email);
|
|
218
|
-
if (!account) {
|
|
219
|
-
logger.warn(`Account not found: ${email}`);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
account.cooldownUntil = Date.now() + cooldownMs;
|
|
224
|
-
account.cooldownReason = reason;
|
|
225
|
-
|
|
226
|
-
logger.debug(`Account ${email} cooling down for ${cooldownMs}ms: ${reason}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Clear cooldown status for an account
|
|
231
|
-
* @param {Object} account - Account object
|
|
232
|
-
*/
|
|
233
|
-
export function clearAccountCooldown(account) {
|
|
234
|
-
if (!account) return;
|
|
235
|
-
account.cooldownUntil = null;
|
|
236
|
-
account.cooldownReason = null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Get remaining cooldown time for an account
|
|
241
|
-
* @param {Object} account - Account object
|
|
242
|
-
* @returns {number} Remaining cooldown time in milliseconds, 0 if not cooling down
|
|
243
|
-
*/
|
|
244
|
-
export function getCooldownRemaining(account) {
|
|
245
|
-
if (!account?.cooldownUntil) return 0;
|
|
246
|
-
const remaining = account.cooldownUntil - Date.now();
|
|
247
|
-
return remaining > 0 ? remaining : 0;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Get consecutive failure count for an account
|
|
252
|
-
* @param {Array} accounts - Array of account objects
|
|
253
|
-
* @param {string} email - Account email
|
|
254
|
-
* @returns {number} Consecutive failure count
|
|
255
|
-
*/
|
|
256
|
-
export function getConsecutiveFailures(accounts, email) {
|
|
257
|
-
const account = accounts?.find(a => a.email === email);
|
|
258
|
-
return account?.consecutiveFailures || 0;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Reset consecutive failure count for an account
|
|
263
|
-
* @param {Array} accounts - Array of account objects
|
|
264
|
-
* @param {string} email - Account email
|
|
265
|
-
*/
|
|
266
|
-
export function resetConsecutiveFailures(accounts, email) {
|
|
267
|
-
const account = accounts?.find(a => a.email === email);
|
|
268
|
-
if (account) {
|
|
269
|
-
account.consecutiveFailures = 0;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Increment consecutive failure count for an account
|
|
275
|
-
* @param {Array} accounts - Array of account objects
|
|
276
|
-
* @param {string} email - Account email
|
|
277
|
-
* @returns {number} New failure count
|
|
278
|
-
*/
|
|
279
|
-
export function incrementConsecutiveFailures(accounts, email) {
|
|
280
|
-
const account = accounts?.find(a => a.email === email);
|
|
281
|
-
if (!account) {
|
|
282
|
-
logger.warn(`Account not found: ${email}`);
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (typeof account.consecutiveFailures !== 'number') {
|
|
287
|
-
account.consecutiveFailures = 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
account.consecutiveFailures++;
|
|
291
|
-
logger.debug(`Account ${email} consecutive failures: ${account.consecutiveFailures}`);
|
|
292
|
-
return account.consecutiveFailures;
|
|
293
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { isAccountCoolingDown } from '../rate-limits.js';
|
|
2
|
-
|
|
3
|
-
export class BaseStrategy {
|
|
4
|
-
constructor(config, name = 'base') {
|
|
5
|
-
if (this.constructor === BaseStrategy) {
|
|
6
|
-
throw new Error('BaseStrategy is abstract and cannot be instantiated directly');
|
|
7
|
-
}
|
|
8
|
-
this.config = config;
|
|
9
|
-
this.name = name;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
selectAccount(accounts, modelId, options) {
|
|
13
|
-
throw new Error('selectAccount must be implemented by subclass');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
onSuccess(account, modelId) {}
|
|
17
|
-
|
|
18
|
-
onRateLimit(account, modelId) {}
|
|
19
|
-
|
|
20
|
-
onFailure(account, modelId) {}
|
|
21
|
-
|
|
22
|
-
isAccountUsable(account, modelId) {
|
|
23
|
-
if (!account) return false;
|
|
24
|
-
if (account.isInvalid) return false;
|
|
25
|
-
if (account.enabled === false) return false;
|
|
26
|
-
if (isAccountCoolingDown(account)) return false;
|
|
27
|
-
|
|
28
|
-
// Check model-specific rate limit
|
|
29
|
-
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
30
|
-
const limit = account.modelRateLimits[modelId];
|
|
31
|
-
if (limit.isRateLimited && limit.resetTime > Date.now()) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getUsableAccounts(accounts, modelId) {
|
|
40
|
-
const usable = [];
|
|
41
|
-
for (let i = 0; i < accounts.length; i++) {
|
|
42
|
-
if (this.isAccountUsable(accounts[i], modelId)) {
|
|
43
|
-
usable.push({ account: accounts[i], index: i });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return usable;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { BaseStrategy } from './base-strategy.js';
|
|
2
|
-
import { StickyStrategy } from './sticky-strategy.js';
|
|
3
|
-
import { RoundRobinStrategy } from './round-robin-strategy.js';
|
|
4
|
-
|
|
5
|
-
export { BaseStrategy, StickyStrategy, RoundRobinStrategy };
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_STRATEGY = 'sticky';
|
|
8
|
-
|
|
9
|
-
export const STRATEGIES = {
|
|
10
|
-
STICKY: 'sticky',
|
|
11
|
-
ROUND_ROBIN: 'round-robin',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const strategyMap = {
|
|
15
|
-
sticky: StickyStrategy,
|
|
16
|
-
'round-robin': RoundRobinStrategy,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const strategyLabels = {
|
|
20
|
-
sticky: 'Sticky (Cache-Optimized)',
|
|
21
|
-
'round-robin': 'Round-Robin (Load-Balanced)',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function createStrategy(name, config) {
|
|
25
|
-
const StrategyClass = strategyMap[name] || StickyStrategy;
|
|
26
|
-
return new StrategyClass(config);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getStrategyLabel(name) {
|
|
30
|
-
return strategyLabels[name] || strategyLabels[DEFAULT_STRATEGY];
|
|
31
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { BaseStrategy } from './base-strategy.js';
|
|
2
|
-
import { logger } from '../../utils/logger.js';
|
|
3
|
-
|
|
4
|
-
export class RoundRobinStrategy extends BaseStrategy {
|
|
5
|
-
constructor(config) {
|
|
6
|
-
super(config, 'round-robin');
|
|
7
|
-
this.cursor = 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
selectAccount(accounts, modelId, options = {}) {
|
|
11
|
-
if (!accounts || accounts.length === 0) {
|
|
12
|
-
return { account: null, index: 0, waitMs: 0 };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Clamp cursor
|
|
16
|
-
if (this.cursor >= accounts.length) {
|
|
17
|
-
this.cursor = 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Start from next position after cursor
|
|
21
|
-
const startIndex = (this.cursor + 1) % accounts.length;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < accounts.length; i++) {
|
|
24
|
-
const checkIndex = (startIndex + i) % accounts.length;
|
|
25
|
-
const account = accounts[checkIndex];
|
|
26
|
-
|
|
27
|
-
if (this.isAccountUsable(account, modelId)) {
|
|
28
|
-
account.lastUsed = Date.now();
|
|
29
|
-
this.cursor = checkIndex;
|
|
30
|
-
logger.debug(`RoundRobinStrategy: Using account at index ${checkIndex}`);
|
|
31
|
-
return { account, index: checkIndex, waitMs: 0 };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// No usable accounts
|
|
36
|
-
return { account: null, index: this.cursor, waitMs: 0 };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
resetCursor() {
|
|
40
|
-
this.cursor = 0;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { BaseStrategy } from './base-strategy.js';
|
|
2
|
-
import { logger } from '../../utils/logger.js';
|
|
3
|
-
|
|
4
|
-
const MAX_WAIT_BEFORE_ERROR_MS = 120000;
|
|
5
|
-
|
|
6
|
-
export class StickyStrategy extends BaseStrategy {
|
|
7
|
-
constructor(config) {
|
|
8
|
-
super(config, 'sticky');
|
|
9
|
-
this.currentIndex = 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
selectAccount(accounts, modelId, options = {}) {
|
|
13
|
-
const { currentIndex = 0, onSave } = options;
|
|
14
|
-
|
|
15
|
-
if (!accounts || accounts.length === 0) {
|
|
16
|
-
return { account: null, index: currentIndex, waitMs: 0 };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const clampedIndex = Math.max(0, Math.min(currentIndex, accounts.length - 1));
|
|
20
|
-
const currentAccount = accounts[clampedIndex];
|
|
21
|
-
|
|
22
|
-
if (this.isAccountUsable(currentAccount, modelId)) {
|
|
23
|
-
currentAccount.lastUsed = Date.now();
|
|
24
|
-
if (onSave) onSave();
|
|
25
|
-
logger.debug(`StickyStrategy: Using sticky account at index ${clampedIndex}`);
|
|
26
|
-
return { account: currentAccount, index: clampedIndex, waitMs: 0 };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Try to find another usable account
|
|
30
|
-
const usableAccounts = this.getUsableAccounts(accounts, modelId);
|
|
31
|
-
|
|
32
|
-
if (usableAccounts.length > 0) {
|
|
33
|
-
const nextResult = this.#pickNext(accounts, clampedIndex, modelId, onSave);
|
|
34
|
-
if (nextResult) {
|
|
35
|
-
this.currentIndex = nextResult.index;
|
|
36
|
-
return nextResult;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if we should wait for the current account
|
|
41
|
-
const { shouldWait, waitMs } = this.#shouldWaitForAccount(currentAccount, modelId);
|
|
42
|
-
if (shouldWait && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
|
|
43
|
-
logger.debug(`StickyStrategy: Waiting ${waitMs}ms for sticky account`);
|
|
44
|
-
return { account: null, index: clampedIndex, waitMs };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// No usable accounts, return null
|
|
48
|
-
return { account: null, index: clampedIndex, waitMs: 0 };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#shouldWaitForAccount(account, modelId) {
|
|
52
|
-
if (!account) {
|
|
53
|
-
return { shouldWait: false, waitMs: 0 };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (account.isInvalid) {
|
|
57
|
-
return { shouldWait: false, waitMs: 0 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (account.enabled === false) {
|
|
61
|
-
return { shouldWait: false, waitMs: 0 };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Check model-specific rate limit
|
|
65
|
-
if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
|
|
66
|
-
const limit = account.modelRateLimits[modelId];
|
|
67
|
-
if (limit.isRateLimited && limit.resetTime > Date.now()) {
|
|
68
|
-
const waitMs = limit.resetTime - Date.now();
|
|
69
|
-
if (waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
|
|
70
|
-
return { shouldWait: true, waitMs };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { shouldWait: false, waitMs: 0 };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
#pickNext(accounts, currentIndex, modelId, onSave) {
|
|
79
|
-
const startIndex = (currentIndex + 1) % accounts.length;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < accounts.length; i++) {
|
|
82
|
-
const checkIndex = (startIndex + i) % accounts.length;
|
|
83
|
-
const account = accounts[checkIndex];
|
|
84
|
-
|
|
85
|
-
if (checkIndex === currentIndex) continue;
|
|
86
|
-
|
|
87
|
-
if (this.isAccountUsable(account, modelId)) {
|
|
88
|
-
account.lastUsed = Date.now();
|
|
89
|
-
if (onSave) onSave();
|
|
90
|
-
logger.debug(`StickyStrategy: Switched to account at index ${checkIndex}`);
|
|
91
|
-
return { account, index: checkIndex, waitMs: 0 };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|