@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
@@ -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';
@@ -21,8 +21,6 @@ import {
21
21
  handleGetKiloModels,
22
22
  handleGetModelMappings,
23
23
  handleSetModelMappings,
24
- handleGetAccountStrategy,
25
- handleSetAccountStrategy,
26
24
  handleGetClaudeProxySetting,
27
25
  handleSetClaudeProxySetting
28
26
  } from './settings-route.js';
@@ -30,20 +28,16 @@ import { handleGetLogs, handleStreamLogs } from './logs-route.js';
30
28
  import { handleGetMetricsRecent, handleGetMetricsStorage, handleGetMetricsSummary } from './metrics-route.js';
31
29
  import { handleGetClaudeConfig, handleSetProxyMode, handleSetDirectMode, handleSetClaudeApiEndpoint } from './claude-config-route.js';
32
30
  import {
33
- handleListAccounts,
31
+ handleGetAccount,
34
32
  handleAccountStatus,
35
33
  handleOAuthCleanup,
36
34
  handleAddAccount,
37
35
  handleAddAccountManual,
38
- handleSwitchAccount,
39
36
  handleRefreshAccount,
40
- handleRefreshAllAccounts,
41
- handleRefreshActiveAccount,
42
37
  handleRemoveAccount,
43
38
  handleImportAccount,
44
- handleGetQuota,
45
- handleGetAllQuotas
46
- } from './accounts-route.js';
39
+ handleGetQuota
40
+ } from './account-route.js';
47
41
 
48
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
49
43
  const require = createRequire(import.meta.url);
@@ -56,7 +50,7 @@ export function registerApiRoutes(app, { port }) {
56
50
 
57
51
  // ─── Health ────────────────────────────────────────────────────────────────
58
52
  app.get('/health', (req, res) => {
59
- res.json({ status: 'ok', ...getStatus(), configPath: ACCOUNTS_FILE });
53
+ res.json({ status: 'ok', ...getStatus(), configPath: ACCOUNT_FILE });
60
54
  });
61
55
 
62
56
  // ─── Anthropic Messages API ────────────────────────────────────────────────
@@ -68,8 +62,8 @@ export function registerApiRoutes(app, { port }) {
68
62
 
69
63
  // ─── Models ────────────────────────────────────────────────────────────────
70
64
  app.get('/v1/models', handleListModels);
71
- app.get('/accounts/models', handleAccountModels);
72
- app.get('/accounts/usage', handleAccountUsage);
65
+ app.get('/account/models', handleAccountModels);
66
+ app.get('/account/usage', handleAccountUsage);
73
67
 
74
68
  // ─── Settings ──────────────────────────────────────────────────────────────
75
69
  app.get('/settings/haiku-model', handleGetHaikuModel);
@@ -77,27 +71,21 @@ export function registerApiRoutes(app, { port }) {
77
71
  app.get('/settings/kilo-models', handleGetKiloModels);
78
72
  app.get('/settings/model-mappings', handleGetModelMappings);
79
73
  app.post('/settings/model-mappings', handleSetModelMappings);
80
- app.get('/settings/account-strategy', handleGetAccountStrategy);
81
- app.post('/settings/account-strategy', handleSetAccountStrategy);
82
74
  app.get('/settings/claude-proxy', handleGetClaudeProxySetting);
83
75
  app.post('/settings/claude-proxy', handleSetClaudeProxySetting);
84
76
 
85
77
  // ─── Account Management ───────────────────────────────────────────────────
86
- app.get('/accounts', handleListAccounts);
87
- app.get('/accounts/status', handleAccountStatus);
88
- app.get('/accounts/quota', handleGetQuota);
89
- app.get('/accounts/quota/all', handleGetAllQuotas);
90
-
91
- app.post('/accounts/add', handleAddAccount);
92
- app.post('/accounts/add/manual', handleAddAccountManual);
93
- app.post('/accounts/switch', handleSwitchAccount);
94
- app.post('/accounts/import', handleImportAccount);
95
- app.post('/accounts/refresh', handleRefreshActiveAccount);
96
- app.post('/accounts/refresh/all', handleRefreshAllAccounts);
97
- app.post('/accounts/oauth/cleanup', handleOAuthCleanup);
98
- app.post('/accounts/:email/refresh', handleRefreshAccount);
99
-
100
- 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);
101
89
 
102
90
  // ─── Claude CLI Configuration ──────────────────────────────────────────────
103
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
 
@@ -77,7 +77,7 @@ export async function handleChatCompletion(req, res) {
77
77
  : await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
78
78
 
79
79
  const duration = Date.now() - startTime;
80
- logger.response(200, { model: upstreamModel, tokens: response.usage?.output_tokens || 0, duration });
80
+ logger.response(200, { model: upstreamModel, usage: response.usage, duration });
81
81
  recordChatMetric({
82
82
  body,
83
83
  requestedModel,
@@ -1,37 +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, getActiveAccount, save } from '../account-manager.js';
9
- import { getServerSettings, 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
- let currentStrategy = null;
18
-
19
- function getAccountRotator() {
20
- const settings = getServerSettings();
21
- const strategy = settings.accountStrategy || 'sticky';
22
-
23
- if (!accountRotator || currentStrategy !== strategy) {
24
- accountRotator = new AccountRotator({
25
- listAccounts,
26
- save,
27
- getActiveAccount
28
- }, strategy);
29
- currentStrategy = strategy;
30
- logger.info(`[Messages] Account strategy: ${strategy}`);
31
- }
32
- return accountRotator;
33
- }
34
-
35
9
  export async function handleMessages(req, res) {
36
10
  const startTime = Date.now();
37
11
  const body = req.body;
@@ -68,50 +42,8 @@ export async function handleMessages(req, res) {
68
42
  : _sendKilo(res, { ...body, model: upstreamModel }, kiloTarget, requestedModel, startTime);
69
43
  }
70
44
 
71
- if (!isMultiAccountRotationEnabled()) {
72
- const creds = await getCredentialsOrError();
73
- if (!creds) {
74
- recordMessageMetric({
75
- body,
76
- endpoint: '/v1/messages',
77
- requestedModel,
78
- upstreamModel,
79
- provider: 'openai',
80
- startTime,
81
- status: 401,
82
- errorType: 'auth_error'
83
- });
84
- return sendAuthError(res);
85
- }
86
-
87
- const anthropicRequest = { ...body, model: upstreamModel, ...(reasoningLevel ? { reasoningLevel } : {}) };
88
- try {
89
- if (isStreaming) {
90
- await _streamDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, null);
91
- } else {
92
- await _sendDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, null);
93
- }
94
- return;
95
- } catch (error) {
96
- recordMessageMetric({
97
- body,
98
- endpoint: '/v1/messages',
99
- requestedModel,
100
- upstreamModel,
101
- provider: 'openai',
102
- accountLabel: creds.email,
103
- startTime,
104
- status: error.status || 500,
105
- errorType: classifyMetricError(error)
106
- });
107
- return handleStreamError(res, error, requestedModel, startTime);
108
- }
109
- }
110
-
111
- const rotator = getAccountRotator();
112
- const accountSnapshot = listAccounts();
113
-
114
- if (accountSnapshot.total === 0) {
45
+ const creds = await getCredentialsOrError();
46
+ if (!creds) {
115
47
  recordMessageMetric({
116
48
  body,
117
49
  endpoint: '/v1/messages',
@@ -122,131 +54,39 @@ export async function handleMessages(req, res) {
122
54
  status: 401,
123
55
  errorType: 'auth_error'
124
56
  });
125
- return sendAuthError(res, 'No active account with valid credentials. Add an account via /accounts/add');
57
+ return sendAuthError(res);
126
58
  }
127
-
128
- rotator.clearExpiredLimits();
129
-
130
- const maxAttempts = Math.max(MAX_RETRIES, accountSnapshot.total);
131
-
132
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
133
- if (rotator.isAllRateLimited(upstreamModel)) {
134
- const minWait = rotator.getMinWaitTimeMs(upstreamModel);
135
-
136
- if (minWait > MAX_WAIT_BEFORE_ERROR_MS) {
137
- recordMessageMetric({
138
- body,
139
- endpoint: '/v1/messages',
140
- requestedModel,
141
- upstreamModel,
142
- provider: 'openai',
143
- startTime,
144
- status: 429,
145
- errorType: 'rate_limited'
146
- });
147
- return handleStreamError(res, new Error(`RESOURCE_EXHAUSTED: All accounts rate-limited. Wait ${Math.round(minWait/1000)}s`), requestedModel, startTime);
148
- }
149
-
150
- logger.info(`[Messages] All accounts rate-limited, waiting ${Math.round(minWait/1000)}s...`);
151
- await sleep(minWait + 500);
152
- rotator.clearExpiredLimits();
153
- attempt--;
154
- continue;
155
- }
156
-
157
- const { account, waitMs } = rotator.selectAccount(upstreamModel);
158
-
159
- if (!account) {
160
- if (waitMs > 0) {
161
- await sleep(waitMs);
162
- attempt--;
163
- continue;
164
- }
165
- recordMessageMetric({
166
- body,
167
- endpoint: '/v1/messages',
168
- requestedModel,
169
- upstreamModel,
170
- provider: 'openai',
171
- startTime,
172
- status: 401,
173
- errorType: 'auth_error'
174
- });
175
- return sendAuthError(res, 'No available accounts');
176
- }
177
-
178
- const creds = await getCredentialsForAccount(account.email);
179
- if (!creds) {
180
- rotator.markInvalid(account.email, 'Failed to get credentials');
181
- continue;
182
- }
183
-
184
- const anthropicRequest = { ...body, model: upstreamModel, ...(reasoningLevel ? { reasoningLevel } : {}) };
185
-
186
- try {
187
- if (isStreaming) {
188
- await _streamDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, rotator);
189
- } else {
190
- await _sendDirectWithRotation(res, anthropicRequest, creds, requestedModel, startTime, rotator);
191
- }
192
- rotator.notifySuccess(account, upstreamModel);
193
- return;
194
- } catch (error) {
195
- if (error.message.startsWith('RATE_LIMITED:')) {
196
- const parts = error.message.split(':');
197
- const resetMs = parseInt(parts[1], 10);
198
- const errorText = parts.slice(2).join(':');
199
-
200
- rotator.notifyRateLimit(account, upstreamModel);
201
-
202
- if (resetMs <= SHORT_RATE_LIMIT_THRESHOLD_MS) {
203
- logger.info(`[Messages] Short rate limit on ${account.email}, waiting ${resetMs}ms...`);
204
- await sleep(resetMs);
205
- attempt--;
206
- continue;
207
- }
208
-
209
- logger.info(`[Messages] Rate limit on ${account.email}, switching account...`);
210
- continue;
211
- }
212
-
213
- if (error.message.includes('AUTH_EXPIRED')) {
214
- rotator.markInvalid(account.email, 'Auth expired');
215
- continue;
216
- }
217
-
218
- recordMessageMetric({
219
- body,
220
- endpoint: '/v1/messages',
221
- requestedModel,
222
- upstreamModel,
223
- provider: 'openai',
224
- accountLabel: account.email,
225
- startTime,
226
- status: error.status || 500,
227
- errorType: classifyMetricError(error)
228
- });
229
- 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);
230
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);
231
81
  }
232
-
233
- recordMessageMetric({
234
- body,
235
- endpoint: '/v1/messages',
236
- requestedModel,
237
- upstreamModel,
238
- provider: 'openai',
239
- startTime,
240
- status: 500,
241
- errorType: 'max_retries'
242
- });
243
- return handleStreamError(res, new Error('Max retries exceeded'), requestedModel, startTime);
244
82
  }
245
83
 
246
- async function _streamDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
84
+ async function _streamDirect(res, anthropicRequest, creds, responseModel, startTime) {
247
85
  initSSEResponse(res);
248
- const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId, rotator, creds.email);
86
+ const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId);
87
+ let finalUsage = null;
249
88
  const stream = tapUsageEventStream(sourceStream, (usage) => {
89
+ finalUsage = usage;
250
90
  recordMessageMetric({
251
91
  body: anthropicRequest,
252
92
  endpoint: '/v1/messages',
@@ -261,13 +101,13 @@ async function _streamDirectWithRotation(res, anthropicRequest, creds, responseM
261
101
  });
262
102
  });
263
103
  await pipeSSEStream(res, stream);
264
- logger.response(200, { model: anthropicRequest.model, duration: Date.now() - startTime });
104
+ logger.response(200, { model: anthropicRequest.model, usage: finalUsage, duration: Date.now() - startTime });
265
105
  }
266
106
 
267
- async function _sendDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
107
+ async function _sendDirect(res, anthropicRequest, creds, responseModel, startTime) {
268
108
  const response = await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
269
109
  const duration = Date.now() - startTime;
270
- logger.response(200, { model: anthropicRequest.model, tokens: response.usage?.output_tokens || 0, duration });
110
+ logger.response(200, { model: anthropicRequest.model, usage: response.usage, duration });
271
111
  recordMessageMetric({
272
112
  body: anthropicRequest,
273
113
  endpoint: '/v1/messages',
@@ -287,7 +127,9 @@ async function _sendDirectWithRotation(res, anthropicRequest, creds, responseMod
287
127
  async function _streamKilo(res, anthropicRequest, kiloTarget, responseModel, startTime) {
288
128
  initSSEResponse(res);
289
129
  const sourceStream = sendKiloMessageStream(anthropicRequest, kiloTarget);
130
+ let finalUsage = null;
290
131
  const stream = tapUsageEventStream(sourceStream, (usage) => {
132
+ finalUsage = usage;
291
133
  recordMessageMetric({
292
134
  body: anthropicRequest,
293
135
  endpoint: '/v1/messages',
@@ -302,13 +144,13 @@ async function _streamKilo(res, anthropicRequest, kiloTarget, responseModel, sta
302
144
  });
303
145
  });
304
146
  await pipeSSEStream(res, stream);
305
- logger.response(200, { model: kiloTarget, duration: Date.now() - startTime });
147
+ logger.response(200, { model: kiloTarget, usage: finalUsage, duration: Date.now() - startTime });
306
148
  }
307
149
 
308
150
  async function _sendKilo(res, anthropicRequest, kiloTarget, responseModel, startTime) {
309
151
  const response = await sendKiloMessage(anthropicRequest, kiloTarget);
310
152
  const duration = Date.now() - startTime;
311
- logger.response(200, { model: kiloTarget, tokens: response.usage?.output_tokens || 0, duration });
153
+ logger.response(200, { model: kiloTarget, usage: response.usage, duration });
312
154
  recordMessageMetric({
313
155
  body: anthropicRequest,
314
156
  endpoint: '/v1/messages',
@@ -334,10 +176,6 @@ async function _sendKilo(res, anthropicRequest, kiloTarget, responseModel, start
334
176
  });
335
177
  }
336
178
 
337
- function sleep(ms) {
338
- return new Promise(resolve => setTimeout(resolve, ms));
339
- }
340
-
341
179
  function recordMessageMetric(options) {
342
180
  const body = options.body || {};
343
181
  recordUsageEventSafe({
@@ -360,7 +198,7 @@ function recordMessageMetric(options) {
360
198
 
361
199
  function classifyMetricError(error) {
362
200
  const message = error?.message || '';
363
- if (message.startsWith('RATE_LIMITED:') || message.startsWith('RESOURCE_EXHAUSTED:')) return 'rate_limited';
201
+ if (message.startsWith('RATE_LIMITED:')) return 'rate_limited';
364
202
  if (message.includes('AUTH_EXPIRED')) return 'auth_expired';
365
203
  if (message.startsWith('CLOUDFLARE_BLOCKED:')) return 'cloudflare_blocked';
366
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 };
@@ -3,14 +3,12 @@
3
3
  * Handles server settings endpoints:
4
4
  * GET /settings/haiku-model
5
5
  * POST /settings/haiku-model
6
- * GET /settings/account-strategy
7
- * POST /settings/account-strategy
8
6
  * GET /settings/claude-proxy
9
7
  * POST /settings/claude-proxy
10
8
  * GET /settings/kilo-models
11
9
  */
12
10
 
13
- import { getServerSettings, isMultiAccountRotationEnabled, setServerSettings } from '../server-settings.js';
11
+ import { getServerSettings, setServerSettings } from '../server-settings.js';
14
12
  import { fetchFreeModels } from '../kilo-models.js';
15
13
  import {
16
14
  CLAUDE_MODEL_ALIASES,
@@ -23,7 +21,6 @@ import {
23
21
  normalizeReasoningMappings
24
22
  } from '../model-mapper.js';
25
23
 
26
- const VALID_STRATEGIES = ['sticky', 'round-robin'];
27
24
  const VALID_OPENAI_MODEL_IDS = new Set(OPENAI_MODEL_OPTIONS.map((model) => model.id));
28
25
  const VALID_REASONING_LEVEL_IDS = new Set(REASONING_LEVEL_OPTIONS.map((level) => level.id));
29
26
 
@@ -208,41 +205,6 @@ export function handleSetModelMappings(req, res) {
208
205
  res.json(modelMappingsPayload(nextSettings.modelMappings, nextSettings.reasoningMappings));
209
206
  }
210
207
 
211
- /**
212
- * GET /settings/account-strategy
213
- * Returns the current account selection strategy.
214
- */
215
- export function handleGetAccountStrategy(req, res) {
216
- const settings = getServerSettings();
217
- res.json({
218
- success: true,
219
- accountStrategy: settings.accountStrategy,
220
- rotationEnabled: isMultiAccountRotationEnabled()
221
- });
222
- }
223
-
224
- /**
225
- * POST /settings/account-strategy
226
- * Updates the account selection strategy.
227
- */
228
- export function handleSetAccountStrategy(req, res) {
229
- const { accountStrategy } = req.body || {};
230
-
231
- if (!VALID_STRATEGIES.includes(accountStrategy)) {
232
- return res.status(400).json({
233
- success: false,
234
- error: `Invalid accountStrategy. Use one of: ${VALID_STRATEGIES.join(', ')}`
235
- });
236
- }
237
-
238
- const settings = setServerSettings({ accountStrategy });
239
- res.json({
240
- success: true,
241
- accountStrategy: settings.accountStrategy,
242
- rotationEnabled: isMultiAccountRotationEnabled()
243
- });
244
- }
245
-
246
208
  /**
247
209
  * GET /settings/claude-proxy
248
210
  * Returns Claude proxy configuration preferences.
@@ -282,8 +244,6 @@ export default {
282
244
  handleGetKiloModels,
283
245
  handleGetModelMappings,
284
246
  handleSetModelMappings,
285
- handleGetAccountStrategy,
286
- handleSetAccountStrategy,
287
247
  handleGetClaudeProxySetting,
288
248
  handleSetClaudeProxySetting
289
249
  };
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',