@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +28 -11
  3. package/bin/cli.js +15 -15
  4. package/docs/ACCOUNT.md +104 -0
  5. package/docs/API.md +21 -29
  6. package/docs/ARCHITECTURE.md +9 -9
  7. package/docs/CLAUDE_INTEGRATION.md +3 -3
  8. package/docs/OAUTH.md +13 -13
  9. package/docs/OPENCLAW.md +1 -1
  10. package/docs/legal.md +6 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/readme-cover.png +0 -0
  13. package/images/settings-screenshot.png +0 -0
  14. package/package.json +19 -10
  15. package/public/css/style.css +802 -22
  16. package/public/index.html +236 -338
  17. package/public/js/app.js +140 -118
  18. package/src/account-manager.js +210 -292
  19. package/src/cli/account.js +236 -0
  20. package/src/direct-api.js +7 -9
  21. package/src/index.js +7 -7
  22. package/src/middleware/credentials.js +6 -47
  23. package/src/oauth.js +2 -1
  24. package/src/routes/{accounts-route.js → account-route.js} +25 -109
  25. package/src/routes/api-routes.js +18 -30
  26. package/src/routes/chat-route.js +3 -3
  27. package/src/routes/messages-route.js +37 -199
  28. package/src/routes/models-route.js +11 -21
  29. package/src/routes/settings-route.js +1 -41
  30. package/src/security.js +1 -1
  31. package/src/server-settings.js +30 -38
  32. package/src/utils/logger.js +14 -1
  33. package/docs/ACCOUNTS.md +0 -202
  34. package/images/demo-screenshot.png +0 -0
  35. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  36. package/src/account-rotation/index.js +0 -93
  37. package/src/account-rotation/rate-limits.js +0 -293
  38. package/src/account-rotation/strategies/base-strategy.js +0 -48
  39. package/src/account-rotation/strategies/index.js +0 -31
  40. package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
  41. package/src/account-rotation/strategies/sticky-strategy.js +0 -97
  42. 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
- }