@pikoloo/codex-proxy 1.0.7 → 1.1.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/docs/API.md +0 -15
- package/images/dashboard-screenshot.png +0 -0
- package/images/readme-cover.png +0 -0
- package/images/settings-screenshot.png +0 -0
- package/package.json +10 -3
- package/public/css/style.css +832 -22
- package/public/index.html +149 -190
- package/public/js/app.js +119 -62
- package/src/account-rotation/index.js +64 -27
- package/src/index.js +1 -1
- package/src/routes/api-routes.js +0 -4
- package/src/routes/chat-route.js +1 -1
- package/src/routes/messages-route.js +14 -16
- package/src/routes/settings-route.js +1 -41
- package/src/server-settings.js +29 -30
- package/src/utils/logger.js +14 -1
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- 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/server-settings.js
CHANGED
|
@@ -7,7 +7,6 @@ const MULTI_ACCOUNT_ROTATION_ENV = 'CODEX_CLAUDE_PROXY_ENABLE_MULTI_ACCOUNT_ROTA
|
|
|
7
7
|
|
|
8
8
|
const DEFAULT_SETTINGS = {
|
|
9
9
|
haikuKiloModel: 'minimax/minimax-m2.5:free',
|
|
10
|
-
accountStrategy: 'sticky',
|
|
11
10
|
configureClaudeOnStartup: false,
|
|
12
11
|
modelMappings: {
|
|
13
12
|
opus: 'gpt-5.5',
|
|
@@ -21,6 +20,30 @@ const DEFAULT_SETTINGS = {
|
|
|
21
20
|
}
|
|
22
21
|
};
|
|
23
22
|
|
|
23
|
+
export function normalizeSettings(data = {}) {
|
|
24
|
+
const modelMappings = data?.modelMappings && typeof data.modelMappings === 'object' && !Array.isArray(data.modelMappings)
|
|
25
|
+
? data.modelMappings
|
|
26
|
+
: {};
|
|
27
|
+
const reasoningMappings = data?.reasoningMappings && typeof data.reasoningMappings === 'object' && !Array.isArray(data.reasoningMappings)
|
|
28
|
+
? data.reasoningMappings
|
|
29
|
+
: {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
haikuKiloModel: typeof data.haikuKiloModel === 'string'
|
|
33
|
+
? data.haikuKiloModel
|
|
34
|
+
: DEFAULT_SETTINGS.haikuKiloModel,
|
|
35
|
+
configureClaudeOnStartup: data.configureClaudeOnStartup === true,
|
|
36
|
+
modelMappings: {
|
|
37
|
+
...DEFAULT_SETTINGS.modelMappings,
|
|
38
|
+
...modelMappings
|
|
39
|
+
},
|
|
40
|
+
reasoningMappings: {
|
|
41
|
+
...DEFAULT_SETTINGS.reasoningMappings,
|
|
42
|
+
...reasoningMappings
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
function ensureConfigDir() {
|
|
25
48
|
if (!existsSync(CONFIG_DIR)) {
|
|
26
49
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
@@ -31,46 +54,21 @@ export function getServerSettings() {
|
|
|
31
54
|
ensureConfigDir();
|
|
32
55
|
|
|
33
56
|
if (!existsSync(SETTINGS_FILE)) {
|
|
34
|
-
return
|
|
35
|
-
...DEFAULT_SETTINGS,
|
|
36
|
-
modelMappings: { ...DEFAULT_SETTINGS.modelMappings },
|
|
37
|
-
reasoningMappings: { ...DEFAULT_SETTINGS.reasoningMappings }
|
|
38
|
-
};
|
|
57
|
+
return normalizeSettings();
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
try {
|
|
42
61
|
const data = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
|
|
43
|
-
|
|
44
|
-
? data.modelMappings
|
|
45
|
-
: {};
|
|
46
|
-
const reasoningMappings = data?.reasoningMappings && typeof data.reasoningMappings === 'object' && !Array.isArray(data.reasoningMappings)
|
|
47
|
-
? data.reasoningMappings
|
|
48
|
-
: {};
|
|
49
|
-
return {
|
|
50
|
-
...DEFAULT_SETTINGS,
|
|
51
|
-
...data,
|
|
52
|
-
modelMappings: {
|
|
53
|
-
...DEFAULT_SETTINGS.modelMappings,
|
|
54
|
-
...modelMappings
|
|
55
|
-
},
|
|
56
|
-
reasoningMappings: {
|
|
57
|
-
...DEFAULT_SETTINGS.reasoningMappings,
|
|
58
|
-
...reasoningMappings
|
|
59
|
-
}
|
|
60
|
-
};
|
|
62
|
+
return normalizeSettings(data);
|
|
61
63
|
} catch (error) {
|
|
62
64
|
console.error('[ServerSettings] Failed to read settings:', error.message);
|
|
63
|
-
return
|
|
64
|
-
...DEFAULT_SETTINGS,
|
|
65
|
-
modelMappings: { ...DEFAULT_SETTINGS.modelMappings },
|
|
66
|
-
reasoningMappings: { ...DEFAULT_SETTINGS.reasoningMappings }
|
|
67
|
-
};
|
|
65
|
+
return normalizeSettings();
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
export function setServerSettings(patch = {}) {
|
|
72
70
|
const current = getServerSettings();
|
|
73
|
-
const next = { ...current, ...patch };
|
|
71
|
+
const next = normalizeSettings({ ...current, ...patch });
|
|
74
72
|
|
|
75
73
|
ensureConfigDir();
|
|
76
74
|
writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
@@ -86,6 +84,7 @@ export { SETTINGS_FILE, MULTI_ACCOUNT_ROTATION_ENV };
|
|
|
86
84
|
export default {
|
|
87
85
|
getServerSettings,
|
|
88
86
|
setServerSettings,
|
|
87
|
+
normalizeSettings,
|
|
89
88
|
isMultiAccountRotationEnabled,
|
|
90
89
|
MULTI_ACCOUNT_ROTATION_ENV,
|
|
91
90
|
SETTINGS_FILE
|
package/src/utils/logger.js
CHANGED
|
@@ -140,8 +140,21 @@ class Logger extends EventEmitter {
|
|
|
140
140
|
|
|
141
141
|
response(status, details = {}) {
|
|
142
142
|
const parts = [`status=${status}`];
|
|
143
|
+
const usage = details.usage || {};
|
|
144
|
+
const inputTokens = usage.input_tokens ?? usage.prompt_tokens;
|
|
145
|
+
const outputTokens = usage.output_tokens ?? usage.completion_tokens;
|
|
146
|
+
const cacheTokens = usage.cache_read_input_tokens ?? usage.prompt_tokens_details?.cached_tokens;
|
|
147
|
+
const totalTokens = details.tokens ?? usage.total_tokens ?? (
|
|
148
|
+
Number.isFinite(inputTokens) && Number.isFinite(outputTokens)
|
|
149
|
+
? inputTokens + outputTokens
|
|
150
|
+
: undefined
|
|
151
|
+
);
|
|
152
|
+
|
|
143
153
|
if (details.model) parts.push(`model=${details.model}`);
|
|
144
|
-
if (
|
|
154
|
+
if (Number.isFinite(totalTokens)) parts.push(`tokens=${totalTokens}`);
|
|
155
|
+
if (Number.isFinite(inputTokens)) parts.push(`input=${inputTokens}`);
|
|
156
|
+
if (Number.isFinite(outputTokens)) parts.push(`output=${outputTokens}`);
|
|
157
|
+
if (Number.isFinite(cacheTokens)) parts.push(`cache=${cacheTokens}`);
|
|
145
158
|
if (details.duration) parts.push(`${details.duration}ms`);
|
|
146
159
|
if (details.error) parts.push(`error=${details.error}`);
|
|
147
160
|
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
}
|