@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.
@@ -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
- const modelMappings = data?.modelMappings && typeof data.modelMappings === 'object' && !Array.isArray(data.modelMappings)
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
@@ -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 (details.tokens) parts.push(`tokens=${details.tokens}`);
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
@@ -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
- }