codex-claude-proxy 1.0.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +274 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +201 -0
- package/docs/OPENCLAW.md +338 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +44 -0
- package/public/css/style.css +791 -0
- package/public/index.html +783 -0
- package/public/js/app.js +511 -0
- package/src/account-manager.js +483 -0
- package/src/claude-config.js +143 -0
- package/src/cli/accounts.js +413 -0
- package/src/cli/index.js +66 -0
- package/src/direct-api.js +123 -0
- package/src/format-converter.js +331 -0
- package/src/index.js +41 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +270 -0
- package/src/kilo-streamer.js +198 -0
- package/src/model-api.js +189 -0
- package/src/oauth.js +554 -0
- package/src/response-streamer.js +329 -0
- package/src/routes/api-routes.js +1035 -0
- package/src/server-settings.js +48 -0
- package/src/server.js +30 -0
- package/src/utils/logger.js +156 -0
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Routes
|
|
3
|
+
* All HTTP route wiring and handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getActiveAccount,
|
|
12
|
+
setActiveAccount,
|
|
13
|
+
removeAccount,
|
|
14
|
+
listAccounts,
|
|
15
|
+
refreshActiveAccount,
|
|
16
|
+
refreshAccountToken,
|
|
17
|
+
refreshAllAccounts,
|
|
18
|
+
importFromCodex,
|
|
19
|
+
getStatus,
|
|
20
|
+
loadAccounts,
|
|
21
|
+
saveAccounts,
|
|
22
|
+
updateAccountAuth,
|
|
23
|
+
updateAccountQuota,
|
|
24
|
+
getAccountQuota,
|
|
25
|
+
isTokenExpiredOrExpiringSoon,
|
|
26
|
+
ACCOUNTS_FILE
|
|
27
|
+
} from '../account-manager.js';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
getAuthorizationUrl,
|
|
31
|
+
generatePKCE,
|
|
32
|
+
generateState,
|
|
33
|
+
startCallbackServer,
|
|
34
|
+
exchangeCodeForTokens,
|
|
35
|
+
OAUTH_CONFIG,
|
|
36
|
+
extractCodeFromInput,
|
|
37
|
+
extractAccountInfo
|
|
38
|
+
} from '../oauth.js';
|
|
39
|
+
|
|
40
|
+
import { sendMessageStream, sendMessage } from '../direct-api.js';
|
|
41
|
+
import { sendKiloMessageStream, sendKiloMessage } from '../kilo-api.js';
|
|
42
|
+
import { formatSSEEvent } from '../response-streamer.js';
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
fetchModels,
|
|
46
|
+
fetchUsage,
|
|
47
|
+
getAccountQuota as fetchAccountQuota,
|
|
48
|
+
getModelsAndQuota
|
|
49
|
+
} from '../model-api.js';
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
readClaudeConfig,
|
|
53
|
+
setProxyMode,
|
|
54
|
+
setDirectMode,
|
|
55
|
+
getClaudeConfigPath
|
|
56
|
+
} from '../claude-config.js';
|
|
57
|
+
|
|
58
|
+
import { convertAnthropicToResponsesAPI } from '../format-converter.js';
|
|
59
|
+
import { logger } from '../utils/logger.js';
|
|
60
|
+
import { getServerSettings, setServerSettings } from '../server-settings.js';
|
|
61
|
+
|
|
62
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
|
|
64
|
+
const CLAUDE_MODEL_MAP = {
|
|
65
|
+
'claude-opus-4-5': 'gpt-5.3-codex',
|
|
66
|
+
'claude-opus-4-5-20250514': 'gpt-5.3-codex',
|
|
67
|
+
'claude-sonnet-4-5': 'gpt-5.2',
|
|
68
|
+
'claude-sonnet-4-5-20250514': 'gpt-5.2',
|
|
69
|
+
'claude-sonnet-4-20250514': 'gpt-5.2',
|
|
70
|
+
'claude-haiku-4-20250514': 'kilo',
|
|
71
|
+
'claude-haiku-3-5-20250514': 'kilo',
|
|
72
|
+
'claude-3-5-sonnet-20240620': 'gpt-5.2',
|
|
73
|
+
'claude-3-opus-20240229': 'gpt-5.3-codex',
|
|
74
|
+
'claude-3-sonnet-20240229': 'gpt-5.2',
|
|
75
|
+
'claude-3-haiku-20240307': 'kilo',
|
|
76
|
+
'sonnet': 'gpt-5.2',
|
|
77
|
+
'opus': 'gpt-5.3-codex',
|
|
78
|
+
'haiku': 'kilo',
|
|
79
|
+
'gpt-5.3-codex': 'gpt-5.3-codex',
|
|
80
|
+
'gpt-5.2-codex': 'gpt-5.2-codex',
|
|
81
|
+
'gpt-5.1-codex-max': 'gpt-5.1-codex-max',
|
|
82
|
+
'gpt-5.1-codex': 'gpt-5.1-codex',
|
|
83
|
+
'gpt-5-codex': 'gpt-5-codex',
|
|
84
|
+
'gpt-5.2': 'gpt-5.2',
|
|
85
|
+
'gpt-5.1': 'gpt-5.1',
|
|
86
|
+
'gpt-5': 'gpt-5',
|
|
87
|
+
'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini',
|
|
88
|
+
'gpt-5-codex-mini': 'gpt-5-codex-mini'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function mapClaudeModel(model) {
|
|
92
|
+
const modelLower = model.toLowerCase();
|
|
93
|
+
|
|
94
|
+
if (CLAUDE_MODEL_MAP[model]) {
|
|
95
|
+
return CLAUDE_MODEL_MAP[model];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (modelLower.startsWith('claude-')) {
|
|
99
|
+
const cleanModel = modelLower.replace(/^claude-/, '');
|
|
100
|
+
if (cleanModel.includes('opus')) return 'gpt-5.3-codex';
|
|
101
|
+
if (cleanModel.includes('sonnet')) return 'gpt-5.2';
|
|
102
|
+
if (cleanModel.includes('haiku')) return 'kilo';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [key, value] of Object.entries(CLAUDE_MODEL_MAP)) {
|
|
106
|
+
if (modelLower.includes(key.toLowerCase())) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return 'gpt-5.2';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isKiloModel(mappedModel) {
|
|
115
|
+
return mappedModel === 'kilo';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveKiloModel() {
|
|
119
|
+
const settings = getServerSettings();
|
|
120
|
+
if (settings.haikuKiloModel === 'minimax-2.5') {
|
|
121
|
+
return 'minimax/minimax-m2.5:free';
|
|
122
|
+
}
|
|
123
|
+
return 'z-ai/glm-5:free';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getCredentialsOrError() {
|
|
127
|
+
const account = getActiveAccount();
|
|
128
|
+
if (!account) {
|
|
129
|
+
logger.info('No active account found');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (!account.accessToken || !account.accountId) {
|
|
133
|
+
logger.info(`Account ${account.email} missing token or accountId`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isTokenExpiredOrExpiringSoon(account)) {
|
|
138
|
+
logger.info(`Token expired/expiring soon for ${account.email}, refreshing...`);
|
|
139
|
+
const result = await refreshAccountToken(account.email);
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
logger.error(`Failed to refresh token: ${result.message}`);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const refreshedAccount = getActiveAccount();
|
|
145
|
+
if (!refreshedAccount) {
|
|
146
|
+
logger.error('Failed to get refreshed account');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
logger.info(`Using refreshed token for ${refreshedAccount.email}`);
|
|
150
|
+
return {
|
|
151
|
+
accessToken: refreshedAccount.accessToken,
|
|
152
|
+
accountId: refreshedAccount.accountId,
|
|
153
|
+
email: refreshedAccount.email
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
accessToken: account.accessToken,
|
|
159
|
+
accountId: account.accountId,
|
|
160
|
+
email: account.email
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleMessages(req, res) {
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
const body = req.body;
|
|
167
|
+
const requestedModel = body.model || 'gpt-5.2';
|
|
168
|
+
|
|
169
|
+
const mappedModel = mapClaudeModel(requestedModel);
|
|
170
|
+
const isStreaming = body.stream !== false;
|
|
171
|
+
|
|
172
|
+
const isKilo = isKiloModel(mappedModel);
|
|
173
|
+
const kiloTarget = isKilo ? resolveKiloModel() : null;
|
|
174
|
+
const upstreamModel = isKilo ? kiloTarget : mappedModel;
|
|
175
|
+
const responseModelForMessages = requestedModel;
|
|
176
|
+
|
|
177
|
+
let model = upstreamModel;
|
|
178
|
+
|
|
179
|
+
if (!isKilo) {
|
|
180
|
+
const creds = await getCredentialsOrError();
|
|
181
|
+
if (!creds) {
|
|
182
|
+
logger.response(401, { error: 'No active account' });
|
|
183
|
+
return res.status(401).json({
|
|
184
|
+
type: 'error',
|
|
185
|
+
error: {
|
|
186
|
+
type: 'authentication_error',
|
|
187
|
+
message: 'No active account with valid credentials. Add an account via /accounts/add'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
logger.request('POST', '/v1/messages', {
|
|
193
|
+
model: upstreamModel,
|
|
194
|
+
account: creds.email,
|
|
195
|
+
stream: isStreaming,
|
|
196
|
+
messages: body.messages?.length || 0,
|
|
197
|
+
tools: body.tools?.length || 0
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const anthropicRequest = {
|
|
201
|
+
...body,
|
|
202
|
+
model: upstreamModel
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (!isStreaming) {
|
|
206
|
+
try {
|
|
207
|
+
const response = await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
|
|
208
|
+
const duration = Date.now() - startTime;
|
|
209
|
+
const tokens = response.usage?.output_tokens || 0;
|
|
210
|
+
logger.response(200, { model: upstreamModel, tokens, duration });
|
|
211
|
+
res.json({
|
|
212
|
+
...response,
|
|
213
|
+
model: responseModelForMessages
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const duration = Date.now() - startTime;
|
|
217
|
+
logger.response(500, { model, error: error.message, duration });
|
|
218
|
+
|
|
219
|
+
if (error.message.includes('AUTH_EXPIRED')) {
|
|
220
|
+
return res.status(401).json({
|
|
221
|
+
type: 'error',
|
|
222
|
+
error: {
|
|
223
|
+
type: 'authentication_error',
|
|
224
|
+
message: 'Token expired. Please refresh or re-authenticate.'
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (error.message.includes('RATE_LIMITED')) {
|
|
230
|
+
return res.status(429).json({
|
|
231
|
+
type: 'error',
|
|
232
|
+
error: {
|
|
233
|
+
type: 'rate_limit_error',
|
|
234
|
+
message: error.message
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
res.status(500).json({
|
|
240
|
+
type: 'error',
|
|
241
|
+
error: { type: 'api_error', message: error.message }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
248
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
249
|
+
res.setHeader('Connection', 'keep-alive');
|
|
250
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
251
|
+
res.flushHeaders();
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const eventStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId);
|
|
255
|
+
|
|
256
|
+
for await (const event of eventStream) {
|
|
257
|
+
res.write(formatSSEEvent(event));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
res.write('data: [DONE]\n\n');
|
|
261
|
+
res.end();
|
|
262
|
+
|
|
263
|
+
const duration = Date.now() - startTime;
|
|
264
|
+
logger.response(200, { model, duration });
|
|
265
|
+
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const duration = Date.now() - startTime;
|
|
268
|
+
logger.response(500, { model, error: error.message, duration });
|
|
269
|
+
|
|
270
|
+
if (!res.headersSent) {
|
|
271
|
+
if (error.message.includes('AUTH_EXPIRED')) {
|
|
272
|
+
return res.status(401).json({
|
|
273
|
+
type: 'error',
|
|
274
|
+
error: { type: 'authentication_error', message: 'Token expired' }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
res.status(500).json({
|
|
278
|
+
type: 'error',
|
|
279
|
+
error: { type: 'api_error', message: error.message }
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'api_error', message: error.message } })}\n\n`);
|
|
283
|
+
res.end();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
logger.request('POST', '/v1/messages', {
|
|
290
|
+
model: kiloTarget,
|
|
291
|
+
account: 'kilo',
|
|
292
|
+
stream: isStreaming,
|
|
293
|
+
messages: body.messages?.length || 0,
|
|
294
|
+
tools: body.tools?.length || 0
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const anthropicRequest = {
|
|
298
|
+
...body,
|
|
299
|
+
model: upstreamModel
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (!isStreaming) {
|
|
303
|
+
try {
|
|
304
|
+
const response = await sendKiloMessage(anthropicRequest, kiloTarget);
|
|
305
|
+
const duration = Date.now() - startTime;
|
|
306
|
+
const tokens = response.usage?.output_tokens || 0;
|
|
307
|
+
logger.response(200, { model: kiloTarget, tokens, duration });
|
|
308
|
+
res.json({
|
|
309
|
+
id: response.id || anthropicRequest.id || undefined,
|
|
310
|
+
type: 'message',
|
|
311
|
+
role: 'assistant',
|
|
312
|
+
content: response.content,
|
|
313
|
+
model: responseModelForMessages,
|
|
314
|
+
stop_reason: response.stopReason,
|
|
315
|
+
stop_sequence: null,
|
|
316
|
+
usage: response.usage
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const duration = Date.now() - startTime;
|
|
320
|
+
logger.response(500, { model: kiloTarget, error: error.message, duration });
|
|
321
|
+
|
|
322
|
+
res.status(500).json({
|
|
323
|
+
type: 'error',
|
|
324
|
+
error: { type: 'api_error', message: error.message }
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
331
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
332
|
+
res.setHeader('Connection', 'keep-alive');
|
|
333
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
334
|
+
res.flushHeaders();
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const eventStream = sendKiloMessageStream(anthropicRequest, kiloTarget);
|
|
338
|
+
|
|
339
|
+
for await (const event of eventStream) {
|
|
340
|
+
res.write(formatSSEEvent(event));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
res.write('data: [DONE]\n\n');
|
|
344
|
+
res.end();
|
|
345
|
+
|
|
346
|
+
const duration = Date.now() - startTime;
|
|
347
|
+
logger.response(200, { model: kiloTarget, duration });
|
|
348
|
+
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const duration = Date.now() - startTime;
|
|
351
|
+
logger.response(500, { model: kiloTarget, error: error.message, duration });
|
|
352
|
+
|
|
353
|
+
if (!res.headersSent) {
|
|
354
|
+
res.status(500).json({
|
|
355
|
+
type: 'error',
|
|
356
|
+
error: { type: 'api_error', message: error.message }
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'api_error', message: error.message } })}\n\n`);
|
|
360
|
+
res.end();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function handleChatCompletion(req, res) {
|
|
366
|
+
const startTime = Date.now();
|
|
367
|
+
const body = req.body;
|
|
368
|
+
const requestedModel = body.model || 'gpt-5.2';
|
|
369
|
+
|
|
370
|
+
const mappedModel = mapClaudeModel(requestedModel);
|
|
371
|
+
|
|
372
|
+
const isKilo = isKiloModel(mappedModel);
|
|
373
|
+
const kiloTarget = isKilo ? resolveKiloModel() : null;
|
|
374
|
+
const upstreamModel = isKilo ? kiloTarget : mappedModel;
|
|
375
|
+
let creds = null;
|
|
376
|
+
|
|
377
|
+
const responseModel = requestedModel;
|
|
378
|
+
|
|
379
|
+
if (!isKilo) {
|
|
380
|
+
creds = await getCredentialsOrError();
|
|
381
|
+
if (!creds) {
|
|
382
|
+
logger.response(401, { error: 'No active account' });
|
|
383
|
+
return res.status(401).json({
|
|
384
|
+
type: 'error',
|
|
385
|
+
error: {
|
|
386
|
+
type: 'authentication_error',
|
|
387
|
+
message: 'No active account. Add an account via /accounts/add'
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const anthropicRequest = {
|
|
394
|
+
model: upstreamModel,
|
|
395
|
+
messages: [],
|
|
396
|
+
system: null,
|
|
397
|
+
stream: false
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (body.messages) {
|
|
401
|
+
const systemMsg = body.messages.find(m => m.role === 'system');
|
|
402
|
+
if (systemMsg) {
|
|
403
|
+
anthropicRequest.system = systemMsg.content;
|
|
404
|
+
}
|
|
405
|
+
anthropicRequest.messages = body.messages
|
|
406
|
+
.filter(m => m.role !== 'system')
|
|
407
|
+
.map(m => {
|
|
408
|
+
if (m.role === 'tool') {
|
|
409
|
+
return {
|
|
410
|
+
role: 'user',
|
|
411
|
+
content: [{
|
|
412
|
+
type: 'tool_result',
|
|
413
|
+
tool_use_id: m.tool_call_id,
|
|
414
|
+
content: m.content
|
|
415
|
+
}]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
420
|
+
const content = [{ type: 'text', text: m.content || '' }];
|
|
421
|
+
for (const call of m.tool_calls) {
|
|
422
|
+
content.push({
|
|
423
|
+
type: 'tool_use',
|
|
424
|
+
id: call.id,
|
|
425
|
+
name: call.function.name,
|
|
426
|
+
input: JSON.parse(call.function.arguments)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return { role: 'assistant', content };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return m;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (body.tools) {
|
|
437
|
+
anthropicRequest.tools = body.tools.map(t => ({
|
|
438
|
+
name: t.function.name,
|
|
439
|
+
description: t.function.description,
|
|
440
|
+
input_schema: t.function.parameters
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
logger.request('POST', '/v1/chat/completions', {
|
|
445
|
+
model: upstreamModel,
|
|
446
|
+
account: isKilo ? 'kilo' : creds.email,
|
|
447
|
+
messages: body.messages?.length || 0,
|
|
448
|
+
tools: body.tools?.length || 0
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const response = isKilo
|
|
453
|
+
? await sendKiloMessage(anthropicRequest, kiloTarget)
|
|
454
|
+
: await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
|
|
455
|
+
|
|
456
|
+
const content = response.content || [];
|
|
457
|
+
const textContent = content.find(c => c.type === 'text');
|
|
458
|
+
const toolUses = content.filter(c => c.type === 'tool_use');
|
|
459
|
+
|
|
460
|
+
const message = {
|
|
461
|
+
role: 'assistant',
|
|
462
|
+
content: textContent?.text || ''
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (toolUses.length > 0) {
|
|
466
|
+
message.tool_calls = toolUses.map(t => ({
|
|
467
|
+
id: t.id,
|
|
468
|
+
type: 'function',
|
|
469
|
+
function: {
|
|
470
|
+
name: t.name,
|
|
471
|
+
arguments: JSON.stringify(t.input)
|
|
472
|
+
}
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const duration = Date.now() - startTime;
|
|
477
|
+
const tokens = response.usage?.output_tokens || 0;
|
|
478
|
+
logger.response(200, { model: upstreamModel, tokens, duration });
|
|
479
|
+
|
|
480
|
+
res.json({
|
|
481
|
+
id: response.id,
|
|
482
|
+
object: 'chat.completion',
|
|
483
|
+
created: Math.floor(Date.now() / 1000),
|
|
484
|
+
model: responseModel,
|
|
485
|
+
choices: [{
|
|
486
|
+
index: 0,
|
|
487
|
+
message: message,
|
|
488
|
+
finish_reason: toolUses.length > 0 ? 'tool_calls' : 'stop'
|
|
489
|
+
}],
|
|
490
|
+
usage: {
|
|
491
|
+
prompt_tokens: response.usage?.input_tokens || 0,
|
|
492
|
+
completion_tokens: response.usage?.output_tokens || 0,
|
|
493
|
+
total_tokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0)
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const duration = Date.now() - startTime;
|
|
499
|
+
logger.response(500, { model: upstreamModel, error: error.message, duration });
|
|
500
|
+
res.status(500).json({
|
|
501
|
+
type: 'error',
|
|
502
|
+
error: { type: 'api_error', message: error.message }
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function registerApiRoutes(app, { port }) {
|
|
510
|
+
app.use(express.static(join(__dirname, '..', '..', 'public')));
|
|
511
|
+
|
|
512
|
+
app.get('/health', (req, res) => {
|
|
513
|
+
const status = getStatus();
|
|
514
|
+
res.json({
|
|
515
|
+
status: 'ok',
|
|
516
|
+
...status,
|
|
517
|
+
configPath: ACCOUNTS_FILE
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
app.post('/v1/chat/completions', handleChatCompletion);
|
|
522
|
+
app.post('/v1/messages', handleMessages);
|
|
523
|
+
|
|
524
|
+
app.post('/v1/messages/count_tokens', (req, res) => {
|
|
525
|
+
const body = req.body;
|
|
526
|
+
let text = '';
|
|
527
|
+
|
|
528
|
+
if (body.system) {
|
|
529
|
+
if (typeof body.system === 'string') {
|
|
530
|
+
text += body.system + ' ';
|
|
531
|
+
} else if (Array.isArray(body.system)) {
|
|
532
|
+
for (const block of body.system) {
|
|
533
|
+
if (block.type === 'text') {
|
|
534
|
+
text += block.text + ' ';
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (body.tools) {
|
|
541
|
+
for (const tool of body.tools) {
|
|
542
|
+
text += JSON.stringify(tool) + ' ';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (body.messages && body.messages.length > 0) {
|
|
547
|
+
for (const msg of body.messages) {
|
|
548
|
+
if (typeof msg.content === 'string') {
|
|
549
|
+
text += msg.content + ' ';
|
|
550
|
+
} else if (Array.isArray(msg.content)) {
|
|
551
|
+
for (const block of msg.content) {
|
|
552
|
+
if (block.type === 'text') {
|
|
553
|
+
text += block.text + ' ';
|
|
554
|
+
} else if (block.type === 'tool_use' || block.type === 'tool_result') {
|
|
555
|
+
text += JSON.stringify(block) + ' ';
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const approxTokens = Math.ceil(text.length / 4);
|
|
563
|
+
res.json({ input_tokens: approxTokens });
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Settings
|
|
567
|
+
app.get('/settings/haiku-model', (req, res) => {
|
|
568
|
+
const settings = getServerSettings();
|
|
569
|
+
res.json({
|
|
570
|
+
success: true,
|
|
571
|
+
haikuKiloModel: settings.haikuKiloModel
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
app.post('/settings/haiku-model', (req, res) => {
|
|
576
|
+
const { haikuKiloModel } = req.body || {};
|
|
577
|
+
if (!['glm-5', 'minimax-2.5'].includes(haikuKiloModel)) {
|
|
578
|
+
return res.status(400).json({
|
|
579
|
+
success: false,
|
|
580
|
+
error: 'Invalid haikuKiloModel. Use glm-5 or minimax-2.5.'
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const settings = setServerSettings({ haikuKiloModel });
|
|
584
|
+
res.json({ success: true, haikuKiloModel: settings.haikuKiloModel });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Account Management API
|
|
588
|
+
app.get('/accounts', (req, res) => {
|
|
589
|
+
res.json(listAccounts());
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
app.get('/accounts/status', (req, res) => {
|
|
593
|
+
res.json(getStatus());
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const activeCallbackServers = new Map();
|
|
597
|
+
|
|
598
|
+
app.post('/accounts/oauth/cleanup', (req, res) => {
|
|
599
|
+
for (const [port, server] of activeCallbackServers) {
|
|
600
|
+
try { server.close(); } catch (e) {}
|
|
601
|
+
}
|
|
602
|
+
activeCallbackServers.clear();
|
|
603
|
+
res.json({ success: true, message: 'OAuth servers cleaned up' });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
app.post('/accounts/add', async (req, res) => {
|
|
607
|
+
const { port } = req.body || {};
|
|
608
|
+
const callbackPort = port || OAUTH_CONFIG.callbackPort;
|
|
609
|
+
|
|
610
|
+
const { verifier } = generatePKCE();
|
|
611
|
+
const state = generateState();
|
|
612
|
+
|
|
613
|
+
const oauthUrl = getAuthorizationUrl(verifier, state, callbackPort);
|
|
614
|
+
|
|
615
|
+
let serverResult;
|
|
616
|
+
try {
|
|
617
|
+
for (const [p, s] of activeCallbackServers) {
|
|
618
|
+
if (p === callbackPort) {
|
|
619
|
+
try { s.close(); } catch (e) {}
|
|
620
|
+
activeCallbackServers.delete(p);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
serverResult = startCallbackServer(callbackPort, state, 120000);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
return res.status(500).json({
|
|
627
|
+
error: 'Failed to start OAuth callback server',
|
|
628
|
+
message: err.message,
|
|
629
|
+
status: 'error'
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
activeCallbackServers.set(callbackPort, serverResult.server);
|
|
634
|
+
|
|
635
|
+
serverResult.promise.then((result) => {
|
|
636
|
+
activeCallbackServers.delete(callbackPort);
|
|
637
|
+
|
|
638
|
+
if (result && result.code) {
|
|
639
|
+
exchangeCodeForTokens(result.code, verifier)
|
|
640
|
+
.then(tokens => {
|
|
641
|
+
const accountInfo = extractAccountInfo(tokens);
|
|
642
|
+
|
|
643
|
+
const currentData = loadAccounts();
|
|
644
|
+
|
|
645
|
+
const existingIndex = currentData.accounts.findIndex(a => a.email === accountInfo.email);
|
|
646
|
+
if (existingIndex >= 0) {
|
|
647
|
+
currentData.accounts[existingIndex] = {
|
|
648
|
+
...currentData.accounts[existingIndex],
|
|
649
|
+
...accountInfo
|
|
650
|
+
};
|
|
651
|
+
} else {
|
|
652
|
+
currentData.accounts.push(accountInfo);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
currentData.activeAccount = accountInfo.email;
|
|
656
|
+
|
|
657
|
+
saveAccounts(currentData);
|
|
658
|
+
updateAccountAuth(accountInfo);
|
|
659
|
+
|
|
660
|
+
logger.info(`Added account: ${accountInfo.email}`);
|
|
661
|
+
})
|
|
662
|
+
.catch(err => {
|
|
663
|
+
logger.error(`OAuth token exchange failed: ${err.message}`);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}).catch(() => {
|
|
667
|
+
activeCallbackServers.delete(callbackPort);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
res.json({
|
|
671
|
+
status: 'oauth_url',
|
|
672
|
+
oauth_url: oauthUrl,
|
|
673
|
+
state,
|
|
674
|
+
callback_port: callbackPort
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
app.post('/accounts/switch', (req, res) => {
|
|
679
|
+
const { email } = req.body || {};
|
|
680
|
+
if (!email) {
|
|
681
|
+
return res.status(400).json({ success: false, message: 'Email is required' });
|
|
682
|
+
}
|
|
683
|
+
const result = setActiveAccount(email);
|
|
684
|
+
if (result.success) {
|
|
685
|
+
logger.info(`Switched to account: ${email}`);
|
|
686
|
+
}
|
|
687
|
+
res.json(result);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
app.post('/accounts/:email/refresh', async (req, res) => {
|
|
691
|
+
const email = decodeURIComponent(req.params.email);
|
|
692
|
+
const result = await refreshAccountToken(email);
|
|
693
|
+
if (result.success) {
|
|
694
|
+
logger.info(`Refreshed token for: ${email}`);
|
|
695
|
+
}
|
|
696
|
+
res.json(result);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
app.post('/accounts/refresh/all', async (req, res) => {
|
|
700
|
+
const result = await refreshAllAccounts();
|
|
701
|
+
res.json(result);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
app.delete('/accounts/:email', (req, res) => {
|
|
705
|
+
const email = decodeURIComponent(req.params.email);
|
|
706
|
+
const result = removeAccount(email);
|
|
707
|
+
if (result.success) {
|
|
708
|
+
logger.info(`Removed account: ${email}`);
|
|
709
|
+
}
|
|
710
|
+
res.json(result);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
app.post('/accounts/import', (req, res) => {
|
|
714
|
+
const result = importFromCodex();
|
|
715
|
+
res.json(result);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
app.post('/accounts/refresh', async (req, res) => {
|
|
719
|
+
const result = await refreshActiveAccount();
|
|
720
|
+
res.json(result);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
app.post('/accounts/add/manual', async (req, res) => {
|
|
724
|
+
const { code, verifier } = req.body || {};
|
|
725
|
+
if (!code) {
|
|
726
|
+
return res.status(400).json({ success: false, error: 'Code is required' });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const extractedCode = extractCodeFromInput(code);
|
|
731
|
+
const tokens = await exchangeCodeForTokens(extractedCode, verifier);
|
|
732
|
+
const accountInfo = extractAccountInfo(tokens);
|
|
733
|
+
|
|
734
|
+
const currentData = loadAccounts();
|
|
735
|
+
const existingIndex = currentData.accounts.findIndex(a => a.email === accountInfo.email);
|
|
736
|
+
|
|
737
|
+
if (existingIndex >= 0) {
|
|
738
|
+
currentData.accounts[existingIndex] = {
|
|
739
|
+
...currentData.accounts[existingIndex],
|
|
740
|
+
...accountInfo
|
|
741
|
+
};
|
|
742
|
+
} else {
|
|
743
|
+
currentData.accounts.push(accountInfo);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
currentData.activeAccount = accountInfo.email;
|
|
747
|
+
saveAccounts(currentData);
|
|
748
|
+
updateAccountAuth(accountInfo);
|
|
749
|
+
|
|
750
|
+
logger.info(`Added account via manual OAuth: ${accountInfo.email}`);
|
|
751
|
+
res.json({ success: true, message: `Account ${accountInfo.email} added successfully` });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
logger.error(`Manual OAuth failed: ${err.message}`);
|
|
754
|
+
res.status(400).json({ success: false, error: err.message });
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
app.get('/accounts/quota/all', async (req, res) => {
|
|
759
|
+
const accounts = listAccounts();
|
|
760
|
+
const results = { accounts: [] };
|
|
761
|
+
|
|
762
|
+
for (const account of accounts.accounts || []) {
|
|
763
|
+
try {
|
|
764
|
+
const quota = await getAccountQuota(account.email);
|
|
765
|
+
results.accounts.push({
|
|
766
|
+
email: account.email,
|
|
767
|
+
quota: quota || null
|
|
768
|
+
});
|
|
769
|
+
} catch (err) {
|
|
770
|
+
results.accounts.push({
|
|
771
|
+
email: account.email,
|
|
772
|
+
quota: null
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
res.json(results);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
app.get('/accounts/quota', async (req, res) => {
|
|
781
|
+
const { email, refresh } = req.query;
|
|
782
|
+
const data = loadAccounts();
|
|
783
|
+
|
|
784
|
+
let account;
|
|
785
|
+
if (email) {
|
|
786
|
+
account = data.accounts.find(a => a.email === email);
|
|
787
|
+
} else {
|
|
788
|
+
account = getActiveAccount();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!account) {
|
|
792
|
+
return res.status(404).json({
|
|
793
|
+
success: false,
|
|
794
|
+
error: email ? `Account not found: ${email}` : 'No active account'
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const cachedQuota = getAccountQuota(account.email);
|
|
799
|
+
const isStale = !cachedQuota ||
|
|
800
|
+
(Date.now() - new Date(cachedQuota.lastChecked).getTime() > 5 * 60 * 1000);
|
|
801
|
+
|
|
802
|
+
if (refresh === 'true' || isStale) {
|
|
803
|
+
try {
|
|
804
|
+
const quotaData = await fetchAccountQuota(account.accessToken, account.accountId);
|
|
805
|
+
updateAccountQuota(account.email, quotaData);
|
|
806
|
+
|
|
807
|
+
res.json({
|
|
808
|
+
success: true,
|
|
809
|
+
email: account.email,
|
|
810
|
+
quota: quotaData,
|
|
811
|
+
cached: false
|
|
812
|
+
});
|
|
813
|
+
} catch (error) {
|
|
814
|
+
logger.error(`Failed to fetch quota: ${error.message}`);
|
|
815
|
+
|
|
816
|
+
if (cachedQuota) {
|
|
817
|
+
res.json({
|
|
818
|
+
success: true,
|
|
819
|
+
email: account.email,
|
|
820
|
+
quota: cachedQuota,
|
|
821
|
+
cached: true,
|
|
822
|
+
warning: 'Using cached data due to fetch error'
|
|
823
|
+
});
|
|
824
|
+
} else {
|
|
825
|
+
res.status(500).json({
|
|
826
|
+
success: false,
|
|
827
|
+
error: error.message
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
res.json({
|
|
833
|
+
success: true,
|
|
834
|
+
email: account.email,
|
|
835
|
+
quota: cachedQuota,
|
|
836
|
+
cached: true
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
app.get('/accounts/models', async (req, res) => {
|
|
842
|
+
const { email } = req.query;
|
|
843
|
+
const data = loadAccounts();
|
|
844
|
+
|
|
845
|
+
let account;
|
|
846
|
+
if (email) {
|
|
847
|
+
account = data.accounts.find(a => a.email === email);
|
|
848
|
+
} else {
|
|
849
|
+
account = getActiveAccount();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!account) {
|
|
853
|
+
return res.status(404).json({
|
|
854
|
+
success: false,
|
|
855
|
+
error: email ? `Account not found: ${email}` : 'No active account'
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
const models = await fetchModels(account.accessToken, account.accountId);
|
|
861
|
+
res.json({
|
|
862
|
+
success: true,
|
|
863
|
+
email: account.email,
|
|
864
|
+
models
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logger.error(`Failed to fetch models: ${error.message}`);
|
|
868
|
+
res.status(500).json({
|
|
869
|
+
success: false,
|
|
870
|
+
error: error.message
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
app.get('/accounts/usage', async (req, res) => {
|
|
876
|
+
const { email } = req.query;
|
|
877
|
+
const data = loadAccounts();
|
|
878
|
+
|
|
879
|
+
let account;
|
|
880
|
+
if (email) {
|
|
881
|
+
account = data.accounts.find(a => a.email === email);
|
|
882
|
+
} else {
|
|
883
|
+
account = getActiveAccount();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (!account) {
|
|
887
|
+
return res.status(404).json({
|
|
888
|
+
success: false,
|
|
889
|
+
error: email ? `Account not found: ${email}` : 'No active account'
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
try {
|
|
894
|
+
const usage = await fetchUsage(account.accessToken, account.accountId);
|
|
895
|
+
res.json({
|
|
896
|
+
success: true,
|
|
897
|
+
email: account.email,
|
|
898
|
+
usage
|
|
899
|
+
});
|
|
900
|
+
} catch (error) {
|
|
901
|
+
logger.error(`Failed to fetch usage: ${error.message}`);
|
|
902
|
+
res.status(500).json({
|
|
903
|
+
success: false,
|
|
904
|
+
error: error.message
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
app.get('/v1/models', async (req, res) => {
|
|
910
|
+
const creds = await getCredentialsOrError();
|
|
911
|
+
if (!creds) {
|
|
912
|
+
return res.json({
|
|
913
|
+
object: 'list',
|
|
914
|
+
data: [
|
|
915
|
+
{ id: 'gpt-5.3-codex', object: 'model', owned_by: 'openai' },
|
|
916
|
+
{ id: 'gpt-5.2-codex', object: 'model', owned_by: 'openai' },
|
|
917
|
+
{ id: 'gpt-5.1-codex', object: 'model', owned_by: 'openai' },
|
|
918
|
+
{ id: 'gpt-5.2', object: 'model', owned_by: 'openai' },
|
|
919
|
+
{ id: 'claude-opus-4-5-20250514', object: 'model', owned_by: 'anthropic' },
|
|
920
|
+
{ id: 'claude-sonnet-4-5-20250514', object: 'model', owned_by: 'anthropic' },
|
|
921
|
+
{ id: 'claude-haiku-4-20250514', object: 'model', owned_by: 'anthropic' }
|
|
922
|
+
]
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const models = await fetchModels(creds.accessToken, creds.accountId);
|
|
928
|
+
const modelList = models.map(m => ({
|
|
929
|
+
id: m.id,
|
|
930
|
+
object: 'model',
|
|
931
|
+
created: Math.floor(Date.now() / 1000),
|
|
932
|
+
owned_by: 'openai',
|
|
933
|
+
description: m.description
|
|
934
|
+
}));
|
|
935
|
+
res.json({ object: 'list', data: modelList });
|
|
936
|
+
} catch (error) {
|
|
937
|
+
logger.error(`Failed to fetch models: ${error.message}`);
|
|
938
|
+
res.json({
|
|
939
|
+
object: 'list',
|
|
940
|
+
data: [
|
|
941
|
+
{ id: 'gpt-5.3-codex', object: 'model', owned_by: 'openai' },
|
|
942
|
+
{ id: 'gpt-5.2-codex', object: 'model', owned_by: 'openai' },
|
|
943
|
+
{ id: 'gpt-5.1-codex', object: 'model', owned_by: 'openai' },
|
|
944
|
+
{ id: 'gpt-5.2', object: 'model', owned_by: 'openai' }
|
|
945
|
+
]
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Claude CLI Configuration
|
|
951
|
+
|
|
952
|
+
app.get('/claude/config', async (req, res) => {
|
|
953
|
+
try {
|
|
954
|
+
const config = await readClaudeConfig();
|
|
955
|
+
const configPath = getClaudeConfigPath();
|
|
956
|
+
res.json({
|
|
957
|
+
success: true,
|
|
958
|
+
configPath,
|
|
959
|
+
config
|
|
960
|
+
});
|
|
961
|
+
} catch (error) {
|
|
962
|
+
res.status(500).json({ success: false, error: error.message });
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
app.post('/claude/config/proxy', async (req, res) => {
|
|
967
|
+
try {
|
|
968
|
+
const proxyUrl = `http://localhost:${port}`;
|
|
969
|
+
const models = {
|
|
970
|
+
default: 'claude-sonnet-4-5',
|
|
971
|
+
opus: 'claude-opus-4-5',
|
|
972
|
+
sonnet: 'claude-sonnet-4-5',
|
|
973
|
+
haiku: 'claude-haiku-4'
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const config = await setProxyMode(proxyUrl, models);
|
|
977
|
+
res.json({
|
|
978
|
+
success: true,
|
|
979
|
+
message: `Claude CLI configured to use proxy at ${proxyUrl}`,
|
|
980
|
+
config
|
|
981
|
+
});
|
|
982
|
+
} catch (error) {
|
|
983
|
+
res.status(500).json({ success: false, error: error.message });
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
app.post('/claude/config/direct', async (req, res) => {
|
|
988
|
+
try {
|
|
989
|
+
const { apiKey } = req.body;
|
|
990
|
+
if (!apiKey) {
|
|
991
|
+
return res.status(400).json({ success: false, error: 'API key required' });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const config = await setDirectMode(apiKey);
|
|
995
|
+
res.json({
|
|
996
|
+
success: true,
|
|
997
|
+
message: 'Claude CLI configured to use direct Anthropic API',
|
|
998
|
+
config
|
|
999
|
+
});
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
res.status(500).json({ success: false, error: error.message });
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Logs API
|
|
1006
|
+
app.get('/api/logs', (req, res) => {
|
|
1007
|
+
res.json({
|
|
1008
|
+
status: 'ok',
|
|
1009
|
+
logs: logger.getHistory()
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
app.get('/api/logs/stream', (req, res) => {
|
|
1014
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1015
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1016
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1017
|
+
|
|
1018
|
+
const sendLog = (log) => {
|
|
1019
|
+
res.write(`data: ${JSON.stringify(log)}\n\n`);
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
if (req.query.history === 'true') {
|
|
1023
|
+
const history = logger.getHistory();
|
|
1024
|
+
history.forEach(log => sendLog(log));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
logger.on('log', sendLog);
|
|
1028
|
+
|
|
1029
|
+
req.on('close', () => {
|
|
1030
|
+
logger.off('log', sendLog);
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export default { registerApiRoutes };
|