@pikoloo/codex-proxy 1.1.0 → 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.
@@ -9,7 +9,7 @@ import { join, dirname } from 'path';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { createRequire } from 'module';
11
11
 
12
- import { getStatus, ACCOUNTS_FILE } from '../account-manager.js';
12
+ import { getStatus, ACCOUNT_FILE } from '../account-manager.js';
13
13
 
14
14
  // Route handlers
15
15
  import { handleMessages } from './messages-route.js';
@@ -28,20 +28,16 @@ import { handleGetLogs, handleStreamLogs } from './logs-route.js';
28
28
  import { handleGetMetricsRecent, handleGetMetricsStorage, handleGetMetricsSummary } from './metrics-route.js';
29
29
  import { handleGetClaudeConfig, handleSetProxyMode, handleSetDirectMode, handleSetClaudeApiEndpoint } from './claude-config-route.js';
30
30
  import {
31
- handleListAccounts,
31
+ handleGetAccount,
32
32
  handleAccountStatus,
33
33
  handleOAuthCleanup,
34
34
  handleAddAccount,
35
35
  handleAddAccountManual,
36
- handleSwitchAccount,
37
36
  handleRefreshAccount,
38
- handleRefreshAllAccounts,
39
- handleRefreshActiveAccount,
40
37
  handleRemoveAccount,
41
38
  handleImportAccount,
42
- handleGetQuota,
43
- handleGetAllQuotas
44
- } from './accounts-route.js';
39
+ handleGetQuota
40
+ } from './account-route.js';
45
41
 
46
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
47
43
  const require = createRequire(import.meta.url);
@@ -54,7 +50,7 @@ export function registerApiRoutes(app, { port }) {
54
50
 
55
51
  // ─── Health ────────────────────────────────────────────────────────────────
56
52
  app.get('/health', (req, res) => {
57
- res.json({ status: 'ok', ...getStatus(), configPath: ACCOUNTS_FILE });
53
+ res.json({ status: 'ok', ...getStatus(), configPath: ACCOUNT_FILE });
58
54
  });
59
55
 
60
56
  // ─── Anthropic Messages API ────────────────────────────────────────────────
@@ -66,8 +62,8 @@ export function registerApiRoutes(app, { port }) {
66
62
 
67
63
  // ─── Models ────────────────────────────────────────────────────────────────
68
64
  app.get('/v1/models', handleListModels);
69
- app.get('/accounts/models', handleAccountModels);
70
- app.get('/accounts/usage', handleAccountUsage);
65
+ app.get('/account/models', handleAccountModels);
66
+ app.get('/account/usage', handleAccountUsage);
71
67
 
72
68
  // ─── Settings ──────────────────────────────────────────────────────────────
73
69
  app.get('/settings/haiku-model', handleGetHaikuModel);
@@ -79,21 +75,17 @@ export function registerApiRoutes(app, { port }) {
79
75
  app.post('/settings/claude-proxy', handleSetClaudeProxySetting);
80
76
 
81
77
  // ─── Account Management ───────────────────────────────────────────────────
82
- app.get('/accounts', handleListAccounts);
83
- app.get('/accounts/status', handleAccountStatus);
84
- app.get('/accounts/quota', handleGetQuota);
85
- app.get('/accounts/quota/all', handleGetAllQuotas);
86
-
87
- app.post('/accounts/add', handleAddAccount);
88
- app.post('/accounts/add/manual', handleAddAccountManual);
89
- app.post('/accounts/switch', handleSwitchAccount);
90
- app.post('/accounts/import', handleImportAccount);
91
- app.post('/accounts/refresh', handleRefreshActiveAccount);
92
- app.post('/accounts/refresh/all', handleRefreshAllAccounts);
93
- app.post('/accounts/oauth/cleanup', handleOAuthCleanup);
94
- app.post('/accounts/:email/refresh', handleRefreshAccount);
95
-
96
- app.delete('/accounts/:email', handleRemoveAccount);
78
+ app.get('/account', handleGetAccount);
79
+ app.get('/account/status', handleAccountStatus);
80
+ app.get('/account/quota', handleGetQuota);
81
+
82
+ app.post('/account/add', handleAddAccount);
83
+ app.post('/account/add/manual', handleAddAccountManual);
84
+ app.post('/account/import', handleImportAccount);
85
+ app.post('/account/refresh', handleRefreshAccount);
86
+ app.post('/account/oauth/cleanup', handleOAuthCleanup);
87
+
88
+ app.delete('/account', handleRemoveAccount);
97
89
 
98
90
  // ─── Claude CLI Configuration ──────────────────────────────────────────────
99
91
  app.get('/claude/config', handleGetClaudeConfig);
@@ -48,7 +48,7 @@ export async function handleChatCompletion(req, res) {
48
48
  if (!isKilo) {
49
49
  creds = await getCredentialsOrError();
50
50
  if (!creds) {
51
- logger.response(401, { error: 'No active account' });
51
+ logger.response(401, { error: 'No configured account' });
52
52
  recordChatMetric({
53
53
  body,
54
54
  requestedModel,
@@ -58,7 +58,7 @@ export async function handleChatCompletion(req, res) {
58
58
  status: 401,
59
59
  errorType: 'auth_error'
60
60
  });
61
- return sendAuthError(res, 'No active account. Add an account via /accounts/add');
61
+ return sendAuthError(res, 'No configured account. Add an account via /account/add');
62
62
  }
63
63
  }
64
64
 
@@ -1,31 +1,11 @@
1
1
  import { sendMessageStream, sendMessage } from '../direct-api.js';
2
2
  import { sendKiloMessageStream, sendKiloMessage } from '../kilo-api.js';
3
3
  import { DEFAULT_OPENAI_MODEL, isKiloEnabled, resolveModelRouting } from '../model-mapper.js';
4
- import { sendAuthError, getCredentialsOrError, getCredentialsForAccount } from '../middleware/credentials.js';
4
+ import { sendAuthError, getCredentialsOrError } from '../middleware/credentials.js';
5
5
  import { initSSEResponse, pipeSSEStream, handleStreamError } from '../middleware/sse.js';
6
6
  import { logger } from '../utils/logger.js';
7
- import { AccountRotator } from '../account-rotation/index.js';
8
- import { listAccounts, save } from '../account-manager.js';
9
- import { isMultiAccountRotationEnabled } from '../server-settings.js';
10
7
  import { recordUsageEventSafe, tapUsageEventStream } from '../usage-metrics.js';
11
8
 
12
- const MAX_RETRIES = 5;
13
- const MAX_WAIT_BEFORE_ERROR_MS = 120000;
14
- const SHORT_RATE_LIMIT_THRESHOLD_MS = 5000;
15
-
16
- let accountRotator = null;
17
-
18
- function getAccountRotator() {
19
- if (!accountRotator) {
20
- accountRotator = new AccountRotator({
21
- listAccounts,
22
- save
23
- });
24
- logger.info('[Messages] Account rotation enabled');
25
- }
26
- return accountRotator;
27
- }
28
-
29
9
  export async function handleMessages(req, res) {
30
10
  const startTime = Date.now();
31
11
  const body = req.body;
@@ -62,50 +42,8 @@ export async function handleMessages(req, res) {
62
42
  : _sendKilo(res, { ...body, model: upstreamModel }, kiloTarget, requestedModel, startTime);
63
43
  }
64
44
 
65
- if (!isMultiAccountRotationEnabled()) {
66
- const creds = await getCredentialsOrError();
67
- if (!creds) {
68
- recordMessageMetric({
69
- body,
70
- endpoint: '/v1/messages',
71
- requestedModel,
72
- upstreamModel,
73
- provider: 'openai',
74
- startTime,
75
- status: 401,
76
- errorType: 'auth_error'
77
- });
78
- return sendAuthError(res);
79
- }
80
-
81
- const anthropicRequest = { ...body, model: upstreamModel, ...(reasoningLevel ? { reasoningLevel } : {}) };
82
- try {
83
- if (isStreaming) {
84
- await _streamDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, null);
85
- } else {
86
- await _sendDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, null);
87
- }
88
- return;
89
- } catch (error) {
90
- recordMessageMetric({
91
- body,
92
- endpoint: '/v1/messages',
93
- requestedModel,
94
- upstreamModel,
95
- provider: 'openai',
96
- accountLabel: creds.email,
97
- startTime,
98
- status: error.status || 500,
99
- errorType: classifyMetricError(error)
100
- });
101
- return handleStreamError(res, error, requestedModel, startTime);
102
- }
103
- }
104
-
105
- const rotator = getAccountRotator();
106
- const accountSnapshot = listAccounts();
107
-
108
- if (accountSnapshot.total === 0) {
45
+ const creds = await getCredentialsOrError();
46
+ if (!creds) {
109
47
  recordMessageMetric({
110
48
  body,
111
49
  endpoint: '/v1/messages',
@@ -116,130 +54,36 @@ export async function handleMessages(req, res) {
116
54
  status: 401,
117
55
  errorType: 'auth_error'
118
56
  });
119
- return sendAuthError(res, 'No active account with valid credentials. Add an account via /accounts/add');
57
+ return sendAuthError(res);
120
58
  }
121
-
122
- rotator.clearExpiredLimits();
123
-
124
- const maxAttempts = Math.max(MAX_RETRIES, accountSnapshot.total);
125
-
126
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
127
- if (rotator.isAllRateLimited(upstreamModel)) {
128
- const minWait = rotator.getMinWaitTimeMs(upstreamModel);
129
-
130
- if (minWait > MAX_WAIT_BEFORE_ERROR_MS) {
131
- recordMessageMetric({
132
- body,
133
- endpoint: '/v1/messages',
134
- requestedModel,
135
- upstreamModel,
136
- provider: 'openai',
137
- startTime,
138
- status: 429,
139
- errorType: 'rate_limited'
140
- });
141
- return handleStreamError(res, new Error(`RESOURCE_EXHAUSTED: All accounts rate-limited. Wait ${Math.round(minWait/1000)}s`), requestedModel, startTime);
142
- }
143
-
144
- logger.info(`[Messages] All accounts rate-limited, waiting ${Math.round(minWait/1000)}s...`);
145
- await sleep(minWait + 500);
146
- rotator.clearExpiredLimits();
147
- attempt--;
148
- continue;
149
- }
150
-
151
- const { account, waitMs } = rotator.selectAccount(upstreamModel);
152
-
153
- if (!account) {
154
- if (waitMs > 0) {
155
- await sleep(waitMs);
156
- attempt--;
157
- continue;
158
- }
159
- recordMessageMetric({
160
- body,
161
- endpoint: '/v1/messages',
162
- requestedModel,
163
- upstreamModel,
164
- provider: 'openai',
165
- startTime,
166
- status: 401,
167
- errorType: 'auth_error'
168
- });
169
- return sendAuthError(res, 'No available accounts');
170
- }
171
-
172
- const creds = await getCredentialsForAccount(account.email);
173
- if (!creds) {
174
- rotator.markInvalid(account.email, 'Failed to get credentials');
175
- continue;
176
- }
177
-
178
- const anthropicRequest = { ...body, model: upstreamModel, ...(reasoningLevel ? { reasoningLevel } : {}) };
179
-
180
- try {
181
- if (isStreaming) {
182
- await _streamDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, rotator);
183
- } else {
184
- await _sendDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, rotator);
185
- }
186
- rotator.notifySuccess(account, upstreamModel);
187
- return;
188
- } catch (error) {
189
- if (error.message.startsWith('RATE_LIMITED:')) {
190
- const parts = error.message.split(':');
191
- const resetMs = parseInt(parts[1], 10);
192
- const errorText = parts.slice(2).join(':');
193
-
194
- rotator.notifyRateLimit(account, upstreamModel);
195
-
196
- if (resetMs <= SHORT_RATE_LIMIT_THRESHOLD_MS) {
197
- logger.info(`[Messages] Short rate limit on ${account.email}, waiting ${resetMs}ms...`);
198
- await sleep(resetMs);
199
- attempt--;
200
- continue;
201
- }
202
-
203
- logger.info(`[Messages] Rate limit on ${account.email}, switching account...`);
204
- continue;
205
- }
206
-
207
- if (error.message.includes('AUTH_EXPIRED')) {
208
- rotator.markInvalid(account.email, 'Auth expired');
209
- continue;
210
- }
211
-
212
- recordMessageMetric({
213
- body,
214
- endpoint: '/v1/messages',
215
- requestedModel,
216
- upstreamModel,
217
- provider: 'openai',
218
- accountLabel: account.email,
219
- startTime,
220
- status: error.status || 500,
221
- errorType: classifyMetricError(error)
222
- });
223
- return handleStreamError(res, error, requestedModel, startTime);
59
+
60
+ const anthropicRequest = { ...body, model: upstreamModel, ...(reasoningLevel ? { reasoningLevel } : {}) };
61
+ try {
62
+ if (isStreaming) {
63
+ await _streamDirect(res, anthropicRequest, creds, requestedModel, startTime);
64
+ } else {
65
+ await _sendDirect(res, anthropicRequest, creds, requestedModel, startTime);
224
66
  }
67
+ return;
68
+ } catch (error) {
69
+ recordMessageMetric({
70
+ body,
71
+ endpoint: '/v1/messages',
72
+ requestedModel,
73
+ upstreamModel,
74
+ provider: 'openai',
75
+ accountLabel: creds.email,
76
+ startTime,
77
+ status: error.message?.startsWith('RATE_LIMITED:') ? 429 : error.status || 500,
78
+ errorType: classifyMetricError(error)
79
+ });
80
+ return handleStreamError(res, error, requestedModel, startTime);
225
81
  }
226
-
227
- recordMessageMetric({
228
- body,
229
- endpoint: '/v1/messages',
230
- requestedModel,
231
- upstreamModel,
232
- provider: 'openai',
233
- startTime,
234
- status: 500,
235
- errorType: 'max_retries'
236
- });
237
- return handleStreamError(res, new Error('Max retries exceeded'), requestedModel, startTime);
238
82
  }
239
83
 
240
- async function _streamDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
84
+ async function _streamDirect(res, anthropicRequest, creds, responseModel, startTime) {
241
85
  initSSEResponse(res);
242
- const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId, rotator, creds.email);
86
+ const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId);
243
87
  let finalUsage = null;
244
88
  const stream = tapUsageEventStream(sourceStream, (usage) => {
245
89
  finalUsage = usage;
@@ -260,7 +104,7 @@ async function _streamDirectWithRotation(res, anthropicRequest, creds, responseM
260
104
  logger.response(200, { model: anthropicRequest.model, usage: finalUsage, duration: Date.now() - startTime });
261
105
  }
262
106
 
263
- async function _sendDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
107
+ async function _sendDirect(res, anthropicRequest, creds, responseModel, startTime) {
264
108
  const response = await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
265
109
  const duration = Date.now() - startTime;
266
110
  logger.response(200, { model: anthropicRequest.model, usage: response.usage, duration });
@@ -332,10 +176,6 @@ async function _sendKilo(res, anthropicRequest, kiloTarget, responseModel, start
332
176
  });
333
177
  }
334
178
 
335
- function sleep(ms) {
336
- return new Promise(resolve => setTimeout(resolve, ms));
337
- }
338
-
339
179
  function recordMessageMetric(options) {
340
180
  const body = options.body || {};
341
181
  recordUsageEventSafe({
@@ -358,7 +198,7 @@ function recordMessageMetric(options) {
358
198
 
359
199
  function classifyMetricError(error) {
360
200
  const message = error?.message || '';
361
- if (message.startsWith('RATE_LIMITED:') || message.startsWith('RESOURCE_EXHAUSTED:')) return 'rate_limited';
201
+ if (message.startsWith('RATE_LIMITED:')) return 'rate_limited';
362
202
  if (message.includes('AUTH_EXPIRED')) return 'auth_expired';
363
203
  if (message.startsWith('CLOUDFLARE_BLOCKED:')) return 'cloudflare_blocked';
364
204
  if (message.startsWith('FORBIDDEN:')) return 'forbidden';
@@ -2,12 +2,12 @@
2
2
  * Models Route
3
3
  * Handles:
4
4
  * GET /v1/models — OpenAI-compatible model list
5
- * GET /accounts/models — Raw model list for the active/specified account
6
- * GET /accounts/usage — Usage stats for the active/specified account
5
+ * GET /account/models — Raw model list for the configured account
6
+ * GET /account/usage — Usage stats for the configured account
7
7
  */
8
8
 
9
9
  import { fetchModels, fetchUsage } from '../model-api.js';
10
- import { getActiveAccount, loadAccounts } from '../account-manager.js';
10
+ import { getActiveAccount } from '../account-manager.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { getCredentialsOrError } from '../middleware/credentials.js';
13
13
  import { DEFAULT_OPENAI_MODEL, DEFAULT_SMALL_OPENAI_MODEL, LATEST_CODEX_MODEL } from '../model-mapper.js';
@@ -61,16 +61,16 @@ export async function handleListModels(req, res) {
61
61
  }
62
62
 
63
63
  /**
64
- * GET /accounts/models
65
- * Returns the raw model list for the active or specified account.
64
+ * GET /account/models
65
+ * Returns the raw model list for the configured account.
66
66
  */
67
67
  export async function handleAccountModels(req, res) {
68
- const account = _resolveAccount(req.query.email);
68
+ const account = getActiveAccount();
69
69
 
70
70
  if (!account) {
71
71
  return res.status(404).json({
72
72
  success: false,
73
- error: req.query.email ? `Account not found: ${req.query.email}` : 'No active account'
73
+ error: 'No account configured'
74
74
  });
75
75
  }
76
76
 
@@ -84,16 +84,16 @@ export async function handleAccountModels(req, res) {
84
84
  }
85
85
 
86
86
  /**
87
- * GET /accounts/usage
88
- * Returns usage stats for the active or specified account.
87
+ * GET /account/usage
88
+ * Returns usage stats for the configured account.
89
89
  */
90
90
  export async function handleAccountUsage(req, res) {
91
- const account = _resolveAccount(req.query.email);
91
+ const account = getActiveAccount();
92
92
 
93
93
  if (!account) {
94
94
  return res.status(404).json({
95
95
  success: false,
96
- error: req.query.email ? `Account not found: ${req.query.email}` : 'No active account'
96
+ error: 'No account configured'
97
97
  });
98
98
  }
99
99
 
@@ -106,14 +106,4 @@ export async function handleAccountUsage(req, res) {
106
106
  }
107
107
  }
108
108
 
109
- // ─── Helpers ─────────────────────────────────────────────────────────────────
110
-
111
- function _resolveAccount(email) {
112
- if (email) {
113
- const data = loadAccounts();
114
- return data.accounts.find(a => a.email === email) || null;
115
- }
116
- return getActiveAccount();
117
- }
118
-
119
109
  export default { handleListModels, handleAccountModels, handleAccountUsage };
package/src/security.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
2
2
  const CONTROL_PATH_PREFIXES = [
3
- '/accounts',
3
+ '/account',
4
4
  '/settings',
5
5
  '/claude/config',
6
6
  '/api/logs',
@@ -3,7 +3,6 @@ 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',
@@ -75,17 +74,11 @@ export function setServerSettings(patch = {}) {
75
74
  return next;
76
75
  }
77
76
 
78
- export function isMultiAccountRotationEnabled(env = process.env) {
79
- return env[MULTI_ACCOUNT_ROTATION_ENV] === 'true';
80
- }
81
-
82
- export { SETTINGS_FILE, MULTI_ACCOUNT_ROTATION_ENV };
77
+ export { SETTINGS_FILE };
83
78
 
84
79
  export default {
85
80
  getServerSettings,
86
81
  setServerSettings,
87
82
  normalizeSettings,
88
- isMultiAccountRotationEnabled,
89
- MULTI_ACCOUNT_ROTATION_ENV,
90
83
  SETTINGS_FILE
91
84
  };
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