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.
@@ -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
- const rule = this.findMatchingRule(route.id, req);
157
- if (!rule) {
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
- const service = this.services.get(rule.targetServiceId);
161
- if (!service) {
162
- return res.status(500).json({ error: 'Target service not configured' });
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
- yield this.proxyRequest(req, res, route, rule, service);
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 (((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
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
- const rule = this.findMatchingRule(route.id, req);
210
- if (!rule) {
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
- const service = this.services.get(rule.targetServiceId);
214
- if (!service) {
215
- return res.status(500).json({ error: 'Target service not configured' });
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
- yield this.proxyRequest(req, res, route, rule, service);
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 (((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
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 undefined;
437
+ return [];
258
438
  const body = req.body;
259
439
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
260
- // 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
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
- if (modelMappingRules.length > 0) {
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
- if (contentTypeRules.length > 0) {
273
- return contentTypeRules[0]; // 已按 sortOrder 降序排序
274
- }
275
- // 3. 最后返回 default 规则
451
+ candidates.push(...contentTypeRules);
452
+ // 3. Default rules
276
453
  const defaultRules = rules.filter(rule => rule.contentType === 'default');
277
- if (defaultRules.length > 0) {
278
- return defaultRules[0]; // 已按 sortOrder 降序排序
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 chunkCollector = new chunk_collector_1.ChunkCollectorTransform();
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
- // 收集stream chunks
759
- streamChunksForLog = chunkCollector.getChunks();
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, chunkCollector, converter, serializer, res, (error) => {
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 chunkCollector = new chunk_collector_1.ChunkCollectorTransform();
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
- streamChunksForLog = chunkCollector.getChunks();
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, chunkCollector, converter, serializer, res, (error) => {
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 chunkCollector = new chunk_collector_1.ChunkCollectorTransform();
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
- streamChunksForLog = chunkCollector.getChunks();
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, chunkCollector, converter, serializer, res, (error) => {
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 chunkCollector = new chunk_collector_1.ChunkCollectorTransform();
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
- streamChunksForLog = chunkCollector.getChunks();
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, chunkCollector, toClaude, toResponses, serializer, res, (error) => {
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 chunkCollector = new chunk_collector_1.ChunkCollectorTransform();
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 = chunkCollector.getChunks();
848
- // 尝试从stream chunks中解析usage信息
849
- if (streamChunksForLog && streamChunksForLog.length > 0) {
850
- // 合并所有chunks并尝试解析usage
851
- const allChunks = streamChunksForLog.join('');
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, chunkCollector, res, (error) => {
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;