@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
package/src/server-settings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
MULTI_ACCOUNT_ROTATION_ENV,
|
|
82
|
+
normalizeSettings,
|
|
91
83
|
SETTINGS_FILE
|
|
92
84
|
};
|
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
|
|
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
|
|
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
|
-
};
|