@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/routes/api-routes.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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('/
|
|
72
|
-
app.get('/
|
|
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('/
|
|
87
|
-
app.get('/
|
|
88
|
-
app.get('/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
app.post('/
|
|
92
|
-
app.post('/
|
|
93
|
-
app.post('/
|
|
94
|
-
app.post('/
|
|
95
|
-
|
|
96
|
-
app.
|
|
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);
|
package/src/routes/chat-route.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
57
|
+
return sendAuthError(res);
|
|
126
58
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
84
|
+
async function _streamDirect(res, anthropicRequest, creds, responseModel, startTime) {
|
|
247
85
|
initSSEResponse(res);
|
|
248
|
-
const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId
|
|
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
|
|
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,
|
|
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,
|
|
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:')
|
|
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 /
|
|
6
|
-
* GET /
|
|
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
|
|
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 /
|
|
65
|
-
* Returns the raw model list for the
|
|
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 =
|
|
68
|
+
const account = getActiveAccount();
|
|
69
69
|
|
|
70
70
|
if (!account) {
|
|
71
71
|
return res.status(404).json({
|
|
72
72
|
success: false,
|
|
73
|
-
error:
|
|
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 /
|
|
88
|
-
* Returns usage stats for the
|
|
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 =
|
|
91
|
+
const account = getActiveAccount();
|
|
92
92
|
|
|
93
93
|
if (!account) {
|
|
94
94
|
return res.status(404).json({
|
|
95
95
|
success: false,
|
|
96
|
-
error:
|
|
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,
|
|
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
|
};
|