aicodeswitch 1.3.9 → 1.4.1
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/CLAUDE.md +10 -0
- package/README.md +8 -0
- package/bin/cli.js +3 -0
- package/bin/ui.js +173 -0
- package/dist/server/database.js +321 -0
- package/dist/server/main.js +22 -2
- package/dist/server/proxy-server.js +263 -67
- package/dist/server/transformers/chunk-collector.js +150 -1
- package/dist/server/version-check.js +114 -0
- package/dist/ui/assets/index-DJ5R6Vso.js +360 -0
- package/dist/ui/assets/{index-Cj2o6J8f.css → index-dcQX0zYo.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +3 -1
- package/dist/ui/assets/index-DcJFhVnN.js +0 -285
|
@@ -143,8 +143,8 @@ class ProxyServer {
|
|
|
143
143
|
this.app.use('/codex', this.createFixedRouteHandler('codex'));
|
|
144
144
|
// Dynamic proxy middleware
|
|
145
145
|
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
146
|
-
var _a;
|
|
147
|
-
// 根路径 /
|
|
146
|
+
var _a, _b, _c, _d;
|
|
147
|
+
// 根路径 / 不应该被代理中间件处理,应该传递给静态文件服务
|
|
148
148
|
if (req.path === '/') {
|
|
149
149
|
return next();
|
|
150
150
|
}
|
|
@@ -153,19 +153,88 @@ class ProxyServer {
|
|
|
153
153
|
if (!route) {
|
|
154
154
|
return res.status(404).json({ error: 'No matching route found' });
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// 检查是否启用故障切换
|
|
157
|
+
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
158
|
+
if (!enableFailover) {
|
|
159
|
+
// 故障切换已禁用,使用传统的单一规则匹配
|
|
160
|
+
const rule = yield this.findMatchingRule(route.id, req);
|
|
161
|
+
if (!rule) {
|
|
162
|
+
return res.status(404).json({ error: 'No matching rule found' });
|
|
163
|
+
}
|
|
164
|
+
const service = this.services.get(rule.targetServiceId);
|
|
165
|
+
if (!service) {
|
|
166
|
+
return res.status(500).json({ error: 'Target service not configured' });
|
|
167
|
+
}
|
|
168
|
+
yield this.proxyRequest(req, res, route, rule, service);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// 启用故障切换:获取所有候选规则
|
|
172
|
+
const allRules = this.getAllMatchingRules(route.id, req);
|
|
173
|
+
if (allRules.length === 0) {
|
|
158
174
|
return res.status(404).json({ error: 'No matching rule found' });
|
|
159
175
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
176
|
+
// 尝试每个规则,直到成功或全部失败
|
|
177
|
+
let lastError = null;
|
|
178
|
+
for (const rule of allRules) {
|
|
179
|
+
const service = this.services.get(rule.targetServiceId);
|
|
180
|
+
if (!service)
|
|
181
|
+
continue;
|
|
182
|
+
// 检查黑名单
|
|
183
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, route.id, rule.contentType);
|
|
184
|
+
if (isBlacklisted) {
|
|
185
|
+
console.log(`Service ${service.name} is blacklisted, skipping...`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
// 尝试代理请求
|
|
190
|
+
yield this.proxyRequest(req, res, route, rule, service);
|
|
191
|
+
return; // 成功,直接返回
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error(`Service ${service.name} failed:`, error.message);
|
|
195
|
+
lastError = error;
|
|
196
|
+
// 判断是否应该加入黑名单 (4xx + 5xx)
|
|
197
|
+
const statusCode = ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || 500;
|
|
198
|
+
if (statusCode >= 400) {
|
|
199
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode);
|
|
200
|
+
console.log(`Service ${service.name} added to blacklist (${route.id}:${rule.contentType}:${service.id})`);
|
|
201
|
+
}
|
|
202
|
+
// 继续尝试下一个服务
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// 所有服务都失败了
|
|
207
|
+
console.error('All services failed');
|
|
208
|
+
// 记录日志
|
|
209
|
+
if (((_c = this.config) === null || _c === void 0 ? void 0 : _c.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
210
|
+
yield this.dbManager.addLog({
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
method: req.method,
|
|
213
|
+
path: req.path,
|
|
214
|
+
headers: this.normalizeHeaders(req.headers),
|
|
215
|
+
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
216
|
+
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
217
|
+
});
|
|
163
218
|
}
|
|
164
|
-
|
|
219
|
+
// 记录错误日志
|
|
220
|
+
yield this.dbManager.addErrorLog({
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
method: req.method,
|
|
223
|
+
path: req.path,
|
|
224
|
+
statusCode: 503,
|
|
225
|
+
errorMessage: 'All services failed',
|
|
226
|
+
errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
|
|
227
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
228
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
229
|
+
});
|
|
230
|
+
res.status(503).json({
|
|
231
|
+
error: 'All services failed',
|
|
232
|
+
details: lastError === null || lastError === void 0 ? void 0 : lastError.message
|
|
233
|
+
});
|
|
165
234
|
}
|
|
166
235
|
catch (error) {
|
|
167
236
|
console.error('Proxy error:', error);
|
|
168
|
-
if (((
|
|
237
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
169
238
|
yield this.dbManager.addLog({
|
|
170
239
|
timestamp: Date.now(),
|
|
171
240
|
method: req.method,
|
|
@@ -192,7 +261,7 @@ class ProxyServer {
|
|
|
192
261
|
}
|
|
193
262
|
createFixedRouteHandler(targetType) {
|
|
194
263
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
195
|
-
var _a;
|
|
264
|
+
var _a, _b, _c, _d;
|
|
196
265
|
try {
|
|
197
266
|
// 检查API Key验证
|
|
198
267
|
if (this.config.apiKey) {
|
|
@@ -206,19 +275,88 @@ class ProxyServer {
|
|
|
206
275
|
if (!route) {
|
|
207
276
|
return res.status(404).json({ error: `No active route found for target type: ${targetType}` });
|
|
208
277
|
}
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
// 检查是否启用故障切换
|
|
279
|
+
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
280
|
+
if (!enableFailover) {
|
|
281
|
+
// 故障切换已禁用,使用传统的单一规则匹配
|
|
282
|
+
const rule = yield this.findMatchingRule(route.id, req);
|
|
283
|
+
if (!rule) {
|
|
284
|
+
return res.status(404).json({ error: 'No matching rule found' });
|
|
285
|
+
}
|
|
286
|
+
const service = this.services.get(rule.targetServiceId);
|
|
287
|
+
if (!service) {
|
|
288
|
+
return res.status(500).json({ error: 'Target service not configured' });
|
|
289
|
+
}
|
|
290
|
+
yield this.proxyRequest(req, res, route, rule, service);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// 启用故障切换:获取所有候选规则
|
|
294
|
+
const allRules = this.getAllMatchingRules(route.id, req);
|
|
295
|
+
if (allRules.length === 0) {
|
|
211
296
|
return res.status(404).json({ error: 'No matching rule found' });
|
|
212
297
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
298
|
+
// 尝试每个规则,直到成功或全部失败
|
|
299
|
+
let lastError = null;
|
|
300
|
+
for (const rule of allRules) {
|
|
301
|
+
const service = this.services.get(rule.targetServiceId);
|
|
302
|
+
if (!service)
|
|
303
|
+
continue;
|
|
304
|
+
// 检查黑名单
|
|
305
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, route.id, rule.contentType);
|
|
306
|
+
if (isBlacklisted) {
|
|
307
|
+
console.log(`Service ${service.name} is blacklisted, skipping...`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
// 尝试代理请求
|
|
312
|
+
yield this.proxyRequest(req, res, route, rule, service);
|
|
313
|
+
return; // 成功,直接返回
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
console.error(`Service ${service.name} failed:`, error.message);
|
|
317
|
+
lastError = error;
|
|
318
|
+
// 判断是否应该加入黑名单 (4xx + 5xx)
|
|
319
|
+
const statusCode = ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || 500;
|
|
320
|
+
if (statusCode >= 400) {
|
|
321
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode);
|
|
322
|
+
console.log(`Service ${service.name} added to blacklist (${route.id}:${rule.contentType}:${service.id})`);
|
|
323
|
+
}
|
|
324
|
+
// 继续尝试下一个服务
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
216
327
|
}
|
|
217
|
-
|
|
328
|
+
// 所有服务都失败了
|
|
329
|
+
console.error('All services failed');
|
|
330
|
+
// 记录日志
|
|
331
|
+
if (((_c = this.config) === null || _c === void 0 ? void 0 : _c.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
332
|
+
yield this.dbManager.addLog({
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
method: req.method,
|
|
335
|
+
path: req.path,
|
|
336
|
+
headers: this.normalizeHeaders(req.headers),
|
|
337
|
+
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
338
|
+
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// 记录错误日志
|
|
342
|
+
yield this.dbManager.addErrorLog({
|
|
343
|
+
timestamp: Date.now(),
|
|
344
|
+
method: req.method,
|
|
345
|
+
path: req.path,
|
|
346
|
+
statusCode: 503,
|
|
347
|
+
errorMessage: 'All services failed',
|
|
348
|
+
errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
|
|
349
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
350
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
351
|
+
});
|
|
352
|
+
res.status(503).json({
|
|
353
|
+
error: 'All services failed',
|
|
354
|
+
details: lastError === null || lastError === void 0 ? void 0 : lastError.message
|
|
355
|
+
});
|
|
218
356
|
}
|
|
219
357
|
catch (error) {
|
|
220
358
|
console.error(`Fixed route error for ${targetType}:`, error);
|
|
221
|
-
if (((
|
|
359
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
222
360
|
yield this.dbManager.addLog({
|
|
223
361
|
timestamp: Date.now(),
|
|
224
362
|
method: req.method,
|
|
@@ -252,32 +390,69 @@ class ProxyServer {
|
|
|
252
390
|
return this.routes.find(route => route.targetType === targetType && route.isActive);
|
|
253
391
|
}
|
|
254
392
|
findMatchingRule(routeId, req) {
|
|
393
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
394
|
+
const rules = this.rules.get(routeId);
|
|
395
|
+
if (!rules)
|
|
396
|
+
return undefined;
|
|
397
|
+
const body = req.body;
|
|
398
|
+
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
399
|
+
// 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
|
|
400
|
+
if (requestModel) {
|
|
401
|
+
const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
402
|
+
rule.replacedModel &&
|
|
403
|
+
requestModel.includes(rule.replacedModel));
|
|
404
|
+
// 过滤黑名单
|
|
405
|
+
for (const rule of modelMappingRules) {
|
|
406
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, rule.contentType);
|
|
407
|
+
if (!isBlacklisted) {
|
|
408
|
+
return rule;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// 2. 查找其他内容类型的规则
|
|
413
|
+
const contentType = this.determineContentType(req);
|
|
414
|
+
const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
|
|
415
|
+
// 过滤黑名单
|
|
416
|
+
for (const rule of contentTypeRules) {
|
|
417
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
|
|
418
|
+
if (!isBlacklisted) {
|
|
419
|
+
return rule;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// 3. 最后返回 default 规则
|
|
423
|
+
const defaultRules = rules.filter(rule => rule.contentType === 'default');
|
|
424
|
+
// 过滤黑名单
|
|
425
|
+
for (const rule of defaultRules) {
|
|
426
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, 'default');
|
|
427
|
+
if (!isBlacklisted) {
|
|
428
|
+
return rule;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return undefined;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
getAllMatchingRules(routeId, req) {
|
|
255
435
|
const rules = this.rules.get(routeId);
|
|
256
436
|
if (!rules)
|
|
257
|
-
return
|
|
437
|
+
return [];
|
|
258
438
|
const body = req.body;
|
|
259
439
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
260
|
-
|
|
440
|
+
const candidates = [];
|
|
441
|
+
// 1. Model mapping rules
|
|
261
442
|
if (requestModel) {
|
|
262
443
|
const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
263
444
|
rule.replacedModel &&
|
|
264
445
|
requestModel.includes(rule.replacedModel));
|
|
265
|
-
|
|
266
|
-
return modelMappingRules[0]; // 已按 sortOrder 降序排序
|
|
267
|
-
}
|
|
446
|
+
candidates.push(...modelMappingRules);
|
|
268
447
|
}
|
|
269
|
-
// 2.
|
|
448
|
+
// 2. Content type specific rules
|
|
270
449
|
const contentType = this.determineContentType(req);
|
|
271
450
|
const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
// 3. 最后返回 default 规则
|
|
451
|
+
candidates.push(...contentTypeRules);
|
|
452
|
+
// 3. Default rules
|
|
276
453
|
const defaultRules = rules.filter(rule => rule.contentType === 'default');
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
return undefined;
|
|
454
|
+
candidates.push(...defaultRules);
|
|
455
|
+
return candidates;
|
|
281
456
|
}
|
|
282
457
|
determineContentType(req) {
|
|
283
458
|
const body = req.body;
|
|
@@ -720,9 +895,17 @@ class ProxyServer {
|
|
|
720
895
|
}
|
|
721
896
|
}
|
|
722
897
|
const streamRequested = this.isStreamRequested(req, requestBody);
|
|
898
|
+
// Build the full URL by appending the request path to the service API URL
|
|
899
|
+
let pathToAppend = req.path;
|
|
900
|
+
if (route.targetType === 'claude-code' && req.path.startsWith('/claude-code')) {
|
|
901
|
+
pathToAppend = req.path.slice('/claude-code'.length);
|
|
902
|
+
}
|
|
903
|
+
else if (route.targetType === 'codex' && req.path.startsWith('/codex')) {
|
|
904
|
+
pathToAppend = req.path.slice('/codex'.length);
|
|
905
|
+
}
|
|
723
906
|
const config = {
|
|
724
907
|
method: req.method,
|
|
725
|
-
url: service.apiUrl
|
|
908
|
+
url: `${service.apiUrl}${pathToAppend}`,
|
|
726
909
|
headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
|
|
727
910
|
timeout: service.timeout || 30000,
|
|
728
911
|
validateStatus: () => true,
|
|
@@ -745,7 +928,7 @@ class ProxyServer {
|
|
|
745
928
|
res.setHeader('Cache-Control', 'no-cache');
|
|
746
929
|
res.setHeader('Connection', 'keep-alive');
|
|
747
930
|
const parser = new streaming_1.SSEParserTransform();
|
|
748
|
-
const
|
|
931
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
749
932
|
const converter = new streaming_1.OpenAIToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
750
933
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
751
934
|
// 收集响应头
|
|
@@ -755,11 +938,18 @@ class ProxyServer {
|
|
|
755
938
|
if (usage) {
|
|
756
939
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
757
940
|
}
|
|
758
|
-
|
|
759
|
-
|
|
941
|
+
else {
|
|
942
|
+
// 尝试从event collector中提取usage
|
|
943
|
+
const extractedUsage = eventCollector.extractUsage();
|
|
944
|
+
if (extractedUsage) {
|
|
945
|
+
usageForLog = this.extractTokenUsage(extractedUsage);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// 收集stream chunks(每个chunk是一个完整的SSE事件)
|
|
949
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
760
950
|
void finalizeLog(res.statusCode);
|
|
761
951
|
});
|
|
762
|
-
(0, stream_1.pipeline)(response.data, parser,
|
|
952
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => {
|
|
763
953
|
if (error) {
|
|
764
954
|
void finalizeLog(500, error.message);
|
|
765
955
|
}
|
|
@@ -771,7 +961,7 @@ class ProxyServer {
|
|
|
771
961
|
res.setHeader('Cache-Control', 'no-cache');
|
|
772
962
|
res.setHeader('Connection', 'keep-alive');
|
|
773
963
|
const parser = new streaming_1.SSEParserTransform();
|
|
774
|
-
const
|
|
964
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
775
965
|
const converter = new streaming_1.OpenAIResponsesToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
776
966
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
777
967
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
@@ -780,10 +970,17 @@ class ProxyServer {
|
|
|
780
970
|
if (usage) {
|
|
781
971
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
782
972
|
}
|
|
783
|
-
|
|
973
|
+
else {
|
|
974
|
+
// 尝试从event collector中提取usage
|
|
975
|
+
const extractedUsage = eventCollector.extractUsage();
|
|
976
|
+
if (extractedUsage) {
|
|
977
|
+
usageForLog = this.extractTokenUsage(extractedUsage);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
784
981
|
void finalizeLog(res.statusCode);
|
|
785
982
|
});
|
|
786
|
-
(0, stream_1.pipeline)(response.data, parser,
|
|
983
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => {
|
|
787
984
|
if (error) {
|
|
788
985
|
void finalizeLog(500, error.message);
|
|
789
986
|
}
|
|
@@ -795,7 +992,7 @@ class ProxyServer {
|
|
|
795
992
|
res.setHeader('Cache-Control', 'no-cache');
|
|
796
993
|
res.setHeader('Connection', 'keep-alive');
|
|
797
994
|
const parser = new streaming_1.SSEParserTransform();
|
|
798
|
-
const
|
|
995
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
799
996
|
const converter = new streaming_1.ClaudeToOpenAIResponsesEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
800
997
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
801
998
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
@@ -804,10 +1001,17 @@ class ProxyServer {
|
|
|
804
1001
|
if (usage) {
|
|
805
1002
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
806
1003
|
}
|
|
807
|
-
|
|
1004
|
+
else {
|
|
1005
|
+
// 尝试从event collector中提取usage
|
|
1006
|
+
const extractedUsage = eventCollector.extractUsage();
|
|
1007
|
+
if (extractedUsage) {
|
|
1008
|
+
usageForLog = this.extractTokenUsage(extractedUsage);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
808
1012
|
void finalizeLog(res.statusCode);
|
|
809
1013
|
});
|
|
810
|
-
(0, stream_1.pipeline)(response.data, parser,
|
|
1014
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => {
|
|
811
1015
|
if (error) {
|
|
812
1016
|
void finalizeLog(500, error.message);
|
|
813
1017
|
}
|
|
@@ -819,7 +1023,7 @@ class ProxyServer {
|
|
|
819
1023
|
res.setHeader('Cache-Control', 'no-cache');
|
|
820
1024
|
res.setHeader('Connection', 'keep-alive');
|
|
821
1025
|
const parser = new streaming_1.SSEParserTransform();
|
|
822
|
-
const
|
|
1026
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
823
1027
|
const toClaude = new streaming_1.OpenAIToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
824
1028
|
const toResponses = new streaming_1.ClaudeToOpenAIResponsesEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
825
1029
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
@@ -829,10 +1033,17 @@ class ProxyServer {
|
|
|
829
1033
|
if (usage) {
|
|
830
1034
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
831
1035
|
}
|
|
832
|
-
|
|
1036
|
+
else {
|
|
1037
|
+
// 尝试从event collector中提取usage
|
|
1038
|
+
const extractedUsage = eventCollector.extractUsage();
|
|
1039
|
+
if (extractedUsage) {
|
|
1040
|
+
usageForLog = this.extractTokenUsage(extractedUsage);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
833
1044
|
void finalizeLog(res.statusCode);
|
|
834
1045
|
});
|
|
835
|
-
(0, stream_1.pipeline)(response.data, parser,
|
|
1046
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, toClaude, toResponses, serializer, res, (error) => {
|
|
836
1047
|
if (error) {
|
|
837
1048
|
void finalizeLog(500, error.message);
|
|
838
1049
|
}
|
|
@@ -840,34 +1051,19 @@ class ProxyServer {
|
|
|
840
1051
|
return;
|
|
841
1052
|
}
|
|
842
1053
|
// 默认stream处理(无转换)
|
|
843
|
-
const
|
|
1054
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
844
1055
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
845
1056
|
this.copyResponseHeaders(responseHeaders, res);
|
|
846
1057
|
res.on('finish', () => {
|
|
847
|
-
streamChunksForLog =
|
|
848
|
-
// 尝试从
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
// 查找包含usage信息的部分
|
|
853
|
-
const usageMatch = allChunks.match(/usage[\s\S]*?\{[\s\S]*?\}/);
|
|
854
|
-
if (usageMatch) {
|
|
855
|
-
try {
|
|
856
|
-
// 尝试解析usage信息
|
|
857
|
-
const usageStr = usageMatch[0];
|
|
858
|
-
const jsonStart = usageStr.indexOf('{');
|
|
859
|
-
const jsonEnd = usageStr.lastIndexOf('}') + 1;
|
|
860
|
-
const usageJson = JSON.parse(usageStr.slice(jsonStart, jsonEnd));
|
|
861
|
-
usageForLog = this.extractTokenUsage(usageJson);
|
|
862
|
-
}
|
|
863
|
-
catch (e) {
|
|
864
|
-
console.error('Failed to parse usage from stream chunks:', e);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
1058
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
1059
|
+
// 尝试从event collector中提取usage信息
|
|
1060
|
+
const extractedUsage = eventCollector.extractUsage();
|
|
1061
|
+
if (extractedUsage) {
|
|
1062
|
+
usageForLog = this.extractTokenUsage(extractedUsage);
|
|
867
1063
|
}
|
|
868
1064
|
void finalizeLog(res.statusCode);
|
|
869
1065
|
});
|
|
870
|
-
(0, stream_1.pipeline)(response.data,
|
|
1066
|
+
(0, stream_1.pipeline)(response.data, eventCollector, res, (error) => {
|
|
871
1067
|
if (error) {
|
|
872
1068
|
void finalizeLog(500, error.message);
|
|
873
1069
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ChunkCollectorTransform = void 0;
|
|
3
|
+
exports.SSEEventCollectorTransform = exports.ChunkCollectorTransform = void 0;
|
|
4
4
|
const stream_1 = require("stream");
|
|
5
5
|
/**
|
|
6
6
|
* ChunkCollectorTransform - 收集stream chunks用于日志记录
|
|
@@ -37,3 +37,152 @@ class ChunkCollectorTransform extends stream_1.Transform {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
exports.ChunkCollectorTransform = ChunkCollectorTransform;
|
|
40
|
+
/**
|
|
41
|
+
* SSEEventCollectorTransform - 智能收集完整的SSE事件
|
|
42
|
+
* 这个Transform会解析SSE流并将每个完整的事件存储为一个单独的entry
|
|
43
|
+
* 确保每个chunk代表一条完整的消息,而不是随机的buffer片段
|
|
44
|
+
*/
|
|
45
|
+
class SSEEventCollectorTransform extends stream_1.Transform {
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
Object.defineProperty(this, "buffer", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: ''
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(this, "currentEvent", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: {
|
|
59
|
+
dataLines: [],
|
|
60
|
+
rawLines: []
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(this, "events", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
value: []
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
_transform(chunk, _encoding, callback) {
|
|
71
|
+
this.buffer += chunk.toString('utf8');
|
|
72
|
+
this.processBuffer();
|
|
73
|
+
// 将chunk传递给下一个stream
|
|
74
|
+
this.push(chunk);
|
|
75
|
+
callback();
|
|
76
|
+
}
|
|
77
|
+
_flush(callback) {
|
|
78
|
+
// 处理剩余的buffer
|
|
79
|
+
if (this.buffer.trim()) {
|
|
80
|
+
this.processBuffer();
|
|
81
|
+
}
|
|
82
|
+
// 刷新最后一个事件
|
|
83
|
+
this.flushEvent();
|
|
84
|
+
callback();
|
|
85
|
+
}
|
|
86
|
+
processBuffer() {
|
|
87
|
+
const lines = this.buffer.split('\n');
|
|
88
|
+
// 保留最后一行(可能不完整)
|
|
89
|
+
this.buffer = lines.pop() || '';
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
this.processLine(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
processLine(line) {
|
|
95
|
+
// 记录原始行
|
|
96
|
+
this.currentEvent.rawLines.push(line);
|
|
97
|
+
// 空行表示一个事件结束
|
|
98
|
+
if (!line.trim()) {
|
|
99
|
+
this.flushEvent();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith('event:')) {
|
|
103
|
+
this.currentEvent.event = line.slice(6).trim();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (line.startsWith('id:')) {
|
|
107
|
+
this.currentEvent.id = line.slice(3).trim();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (line.startsWith('data:')) {
|
|
111
|
+
this.currentEvent.dataLines.push(line.slice(5).trim());
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
flushEvent() {
|
|
116
|
+
// 只有当有内容时才创建事件
|
|
117
|
+
if (!this.currentEvent.event && this.currentEvent.dataLines.length === 0 && !this.currentEvent.id) {
|
|
118
|
+
this.currentEvent = { dataLines: [], rawLines: [] };
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const raw = this.currentEvent.rawLines.join('\n');
|
|
122
|
+
const event = {
|
|
123
|
+
event: this.currentEvent.event,
|
|
124
|
+
id: this.currentEvent.id,
|
|
125
|
+
data: this.currentEvent.dataLines.length > 0 ? this.currentEvent.dataLines.join('\n') : undefined,
|
|
126
|
+
raw
|
|
127
|
+
};
|
|
128
|
+
this.events.push(event);
|
|
129
|
+
this.currentEvent = { dataLines: [], rawLines: [] };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 获取收集的所有SSE事件
|
|
133
|
+
* 每个事件都是一个完整的SSE消息
|
|
134
|
+
*/
|
|
135
|
+
getEvents() {
|
|
136
|
+
return this.events;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 获取原始chunks(兼容旧接口)
|
|
140
|
+
*/
|
|
141
|
+
getChunks() {
|
|
142
|
+
return this.events.map(e => e.raw);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 清空已收集的事件
|
|
146
|
+
*/
|
|
147
|
+
clearEvents() {
|
|
148
|
+
this.events = [];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 从events中提取usage信息
|
|
152
|
+
*/
|
|
153
|
+
extractUsage() {
|
|
154
|
+
for (const event of this.events) {
|
|
155
|
+
if (!event.data)
|
|
156
|
+
continue;
|
|
157
|
+
try {
|
|
158
|
+
const data = JSON.parse(event.data);
|
|
159
|
+
// 尝试从不同的位置提取usage
|
|
160
|
+
// 1. message_delta事件中的usage
|
|
161
|
+
if (event.event === 'message_delta' && data.usage) {
|
|
162
|
+
return data.usage;
|
|
163
|
+
}
|
|
164
|
+
// 2. 直接在data中的usage
|
|
165
|
+
if (data.usage) {
|
|
166
|
+
return data.usage;
|
|
167
|
+
}
|
|
168
|
+
// 3. OpenAI格式: choices数组中最后一个元素的usage
|
|
169
|
+
if (Array.isArray(data.choices) && data.choices.length > 0) {
|
|
170
|
+
const lastChoice = data.choices[data.choices.length - 1];
|
|
171
|
+
if (lastChoice === null || lastChoice === void 0 ? void 0 : lastChoice.usage) {
|
|
172
|
+
return lastChoice.usage;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 4. 直接在顶级的usage字段
|
|
176
|
+
if (data.input_tokens !== undefined || data.output_tokens !== undefined ||
|
|
177
|
+
data.prompt_tokens !== undefined || data.completion_tokens !== undefined) {
|
|
178
|
+
return data;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (_a) {
|
|
182
|
+
// JSON解析失败,跳过
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.SSEEventCollectorTransform = SSEEventCollectorTransform;
|