@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
@@ -3,11 +3,9 @@ import { join } from 'path';
3
3
  import { CONFIG_DIR } from './account-manager.js';
4
4
 
5
5
  const SETTINGS_FILE = join(CONFIG_DIR, 'settings.json');
6
- const MULTI_ACCOUNT_ROTATION_ENV = 'CODEX_CLAUDE_PROXY_ENABLE_MULTI_ACCOUNT_ROTATION';
7
6
 
8
7
  const DEFAULT_SETTINGS = {
9
8
  haikuKiloModel: 'minimax/minimax-m2.5:free',
10
- accountStrategy: 'sticky',
11
9
  configureClaudeOnStartup: false,
12
10
  modelMappings: {
13
11
  opus: 'gpt-5.5',
@@ -21,6 +19,30 @@ const DEFAULT_SETTINGS = {
21
19
  }
22
20
  };
23
21
 
22
+ export function normalizeSettings(data = {}) {
23
+ const modelMappings = data?.modelMappings && typeof data.modelMappings === 'object' && !Array.isArray(data.modelMappings)
24
+ ? data.modelMappings
25
+ : {};
26
+ const reasoningMappings = data?.reasoningMappings && typeof data.reasoningMappings === 'object' && !Array.isArray(data.reasoningMappings)
27
+ ? data.reasoningMappings
28
+ : {};
29
+
30
+ return {
31
+ haikuKiloModel: typeof data.haikuKiloModel === 'string'
32
+ ? data.haikuKiloModel
33
+ : DEFAULT_SETTINGS.haikuKiloModel,
34
+ configureClaudeOnStartup: data.configureClaudeOnStartup === true,
35
+ modelMappings: {
36
+ ...DEFAULT_SETTINGS.modelMappings,
37
+ ...modelMappings
38
+ },
39
+ reasoningMappings: {
40
+ ...DEFAULT_SETTINGS.reasoningMappings,
41
+ ...reasoningMappings
42
+ }
43
+ };
44
+ }
45
+
24
46
  function ensureConfigDir() {
25
47
  if (!existsSync(CONFIG_DIR)) {
26
48
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
@@ -31,62 +53,32 @@ export function getServerSettings() {
31
53
  ensureConfigDir();
32
54
 
33
55
  if (!existsSync(SETTINGS_FILE)) {
34
- return {
35
- ...DEFAULT_SETTINGS,
36
- modelMappings: { ...DEFAULT_SETTINGS.modelMappings },
37
- reasoningMappings: { ...DEFAULT_SETTINGS.reasoningMappings }
38
- };
56
+ return normalizeSettings();
39
57
  }
40
58
 
41
59
  try {
42
60
  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
- };
61
+ return normalizeSettings(data);
61
62
  } catch (error) {
62
63
  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
- };
64
+ return normalizeSettings();
68
65
  }
69
66
  }
70
67
 
71
68
  export function setServerSettings(patch = {}) {
72
69
  const current = getServerSettings();
73
- const next = { ...current, ...patch };
70
+ const next = normalizeSettings({ ...current, ...patch });
74
71
 
75
72
  ensureConfigDir();
76
73
  writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
77
74
  return next;
78
75
  }
79
76
 
80
- export function isMultiAccountRotationEnabled(env = process.env) {
81
- return env[MULTI_ACCOUNT_ROTATION_ENV] === 'true';
82
- }
83
-
84
- export { SETTINGS_FILE, MULTI_ACCOUNT_ROTATION_ENV };
77
+ export { SETTINGS_FILE };
85
78
 
86
79
  export default {
87
80
  getServerSettings,
88
81
  setServerSettings,
89
- isMultiAccountRotationEnabled,
90
- MULTI_ACCOUNT_ROTATION_ENV,
82
+ normalizeSettings,
91
83
  SETTINGS_FILE
92
84
  };
@@ -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
 
package/docs/ACCOUNTS.md DELETED
@@ -1,202 +0,0 @@
1
- # Account Management
2
-
3
- ## Storage Structure
4
-
5
- ### Main Registry
6
-
7
- **Location:** `~/.codex-claude-proxy/accounts.json`
8
-
9
- ```json
10
- {
11
- "accounts": [
12
- {
13
- "email": "user@gmail.com",
14
- "accountId": "d41e9636-16d8-42be-91da-7ea8773bfb7e",
15
- "planType": "plus",
16
- "accessToken": "eyJhbGciOiJSUzI1NiIs...",
17
- "refreshToken": "rt_WpTMn1...",
18
- "idToken": "eyJhbGciOiJSUzI1NiIs...",
19
- "expiresAt": 1770886178000,
20
- "addedAt": "2026-02-13T04:00:00.000Z",
21
- "lastUsed": "2026-02-13T04:30:00.000Z",
22
- "quota": {
23
- "usage": {...},
24
- "account": {...},
25
- "lastChecked": "2026-02-14T10:00:00.000Z"
26
- }
27
- }
28
- ],
29
- "activeAccount": "user@gmail.com",
30
- "version": 1
31
- }
32
- ```
33
-
34
- ### Per-Account Tokens
35
-
36
- **Location:** `~/.codex-claude-proxy/accounts/<email>/auth.json`
37
-
38
- ```json
39
- {
40
- "auth_mode": "chatgpt",
41
- "OPENAI_API_KEY": null,
42
- "tokens": {
43
- "id_token": "...",
44
- "access_token": "...",
45
- "refresh_token": "...",
46
- "account_id": "..."
47
- },
48
- "last_refresh": "2026-02-14T10:00:00.000Z"
49
- }
50
- ```
51
-
52
- ## Operations
53
-
54
- ### Add Account (OAuth)
55
-
56
- ```bash
57
- curl -X POST http://localhost:8081/accounts/add
58
-
59
- # Returns OAuth URL to open in browser
60
- ```
61
-
62
- ### Import from Codex App
63
-
64
- ```bash
65
- curl -X POST http://localhost:8081/accounts/import
66
-
67
- # Imports from ~/.codex/auth.json
68
- ```
69
-
70
- ### List Accounts
71
-
72
- ```bash
73
- curl http://localhost:8081/accounts
74
-
75
- # Response
76
- {
77
- "accounts": [
78
- {
79
- "email": "user@gmail.com",
80
- "accountId": "...",
81
- "planType": "plus",
82
- "addedAt": "...",
83
- "lastUsed": "...",
84
- "isActive": true,
85
- "tokenExpired": false,
86
- "quota": {...}
87
- }
88
- ],
89
- "activeAccount": "user@gmail.com",
90
- "total": 1
91
- }
92
- ```
93
-
94
- ### Switch Active Account
95
-
96
- ```bash
97
- curl -X POST http://localhost:8081/accounts/switch \
98
- -H "Content-Type: application/json" \
99
- -d '{"email":"other@gmail.com"}'
100
- ```
101
-
102
- Switching:
103
- 1. Updates `activeAccount` in `accounts.json`
104
- 2. Updates auth file for the account
105
- 3. Next API calls use new account's credentials
106
-
107
- ### Remove Account
108
-
109
- ```bash
110
- curl -X DELETE http://localhost:8081/accounts/user@gmail.com
111
- ```
112
-
113
- Removes:
114
- - Account from registry
115
- - Per-account token directory
116
-
117
- ### Refresh Tokens
118
-
119
- ```bash
120
- # Active account
121
- curl -X POST http://localhost:8081/accounts/refresh
122
-
123
- # Specific account
124
- curl -X POST http://localhost:8081/accounts/user@gmail.com/refresh
125
-
126
- # All accounts
127
- curl -X POST http://localhost:8081/accounts/refresh/all
128
- ```
129
-
130
- ## Token Lifecycle
131
-
132
- ### Expiration
133
-
134
- - Access tokens expire in ~1 hour (3600 seconds)
135
- - Refresh tokens are long-lived (weeks/months)
136
-
137
- ### Auto-Refresh
138
-
139
- - Background refresh every **55 minutes**
140
- - Startup refresh 2 seconds after server start
141
- - Proactive refresh 5 minutes before expiry
142
-
143
- ### Token Validation
144
-
145
- Before each API call:
146
- 1. Check if token is expired or expiring within 5 minutes
147
- 2. If yes, refresh using refresh token
148
- 3. Use new access token for the call
149
-
150
- ## Quota Tracking
151
-
152
- ### Fetch Quota
153
-
154
- ```bash
155
- curl http://localhost:8081/accounts/quota
156
-
157
- # Response
158
- {
159
- "success": true,
160
- "email": "user@gmail.com",
161
- "quota": {
162
- "usage": {
163
- "totalTokenUsage": 15,
164
- "limit": 100,
165
- "remaining": 85,
166
- "percentage": 15,
167
- "resetAt": "..."
168
- },
169
- "account": {...}
170
- },
171
- "cached": false
172
- }
173
- ```
174
-
175
- ### Web UI Quota Display Rules
176
-
177
- - The Accounts table displays **remaining quota** as a percentage.
178
- - Remaining percentage is normalized to `0-100` to avoid broken UI values.
179
- - If `limitReached=true` or `allowed=false`, UI shows quota as exhausted even when percentage data is missing.
180
- - If usage data is unavailable, UI shows `-` instead of rendering a broken bar.
181
- - Reset window is shown using `usage.resetAt` (with fallback to `usage.raw.rate_limit.primary_window.reset_at`).
182
- - UI also shows a relative countdown (e.g. `Resets in 6d 13h`) when reset data is available.
183
-
184
- ### Refresh All Quotas
185
-
186
- ```bash
187
- curl http://localhost:8081/accounts/quota/all
188
- ```
189
-
190
- ## Account Persistence
191
-
192
- On server startup:
193
- 1. `ensureAccountsPersist()` loads accounts
194
- 2. Restores active account's auth
195
- 3. Starts auto-refresh timer
196
-
197
- ## Security
198
-
199
- - Tokens stored locally in `~/.codex-claude-proxy/`
200
- - Directory permissions: user read/write only
201
- - Never logged or exposed in API responses
202
- - Per-account isolation via separate directories
Binary file
@@ -1,93 +0,0 @@
1
- import {
2
- markRateLimited,
3
- markInvalid,
4
- clearInvalid,
5
- isAllRateLimited,
6
- getMinWaitTimeMs,
7
- clearExpiredLimits
8
- } from './rate-limits.js';
9
-
10
- import { createStrategy, STRATEGIES } from './strategies/index.js';
11
-
12
- export class AccountRotator {
13
- constructor(accountManager, strategyName = 'sticky') {
14
- this.accountManager = accountManager;
15
- this.strategy = createStrategy(strategyName);
16
- }
17
-
18
- selectAccount(modelId, options = {}) {
19
- const { accounts } = this.accountManager.listAccounts();
20
- return this.strategy.selectAccount(accounts, modelId, options);
21
- }
22
-
23
- markRateLimited(email, resetMs, modelId) {
24
- const { accounts } = this.accountManager.listAccounts();
25
- markRateLimited(accounts, email, resetMs, modelId);
26
- this.accountManager.save();
27
- }
28
-
29
- markInvalid(email, reason) {
30
- const { accounts } = this.accountManager.listAccounts();
31
- markInvalid(accounts, email, reason);
32
- this.accountManager.save();
33
- }
34
-
35
- clearInvalid(email) {
36
- const { accounts } = this.accountManager.listAccounts();
37
- clearInvalid(accounts, email);
38
- this.accountManager.save();
39
- }
40
-
41
- isAllRateLimited(modelId) {
42
- const { accounts } = this.accountManager.listAccounts();
43
- return isAllRateLimited(accounts, modelId);
44
- }
45
-
46
- getMinWaitTimeMs(modelId) {
47
- const { accounts } = this.accountManager.listAccounts();
48
- return getMinWaitTimeMs(accounts, modelId);
49
- }
50
-
51
- notifySuccess(account, modelId) {
52
- if (this.strategy.notifySuccess) {
53
- this.strategy.notifySuccess(account, modelId);
54
- }
55
- }
56
-
57
- notifyRateLimit(account, modelId) {
58
- if (this.strategy.notifyRateLimit) {
59
- this.strategy.notifyRateLimit(account, modelId);
60
- }
61
- }
62
-
63
- notifyFailure(account, modelId) {
64
- if (this.strategy.notifyFailure) {
65
- this.strategy.notifyFailure(account, modelId);
66
- }
67
- }
68
-
69
- clearExpiredLimits() {
70
- const { accounts } = this.accountManager.listAccounts();
71
- clearExpiredLimits(accounts);
72
- this.accountManager.save();
73
- }
74
-
75
- getStrategyName() {
76
- return this.strategy.name;
77
- }
78
-
79
- getStrategyLabel() {
80
- return this.strategy.label;
81
- }
82
- }
83
-
84
- export {
85
- createStrategy,
86
- STRATEGIES,
87
- markRateLimited,
88
- markInvalid,
89
- clearInvalid,
90
- isAllRateLimited,
91
- getMinWaitTimeMs,
92
- clearExpiredLimits
93
- };