aicodeswitch 4.0.2 → 4.0.4

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.
@@ -195,9 +195,8 @@ class ProxyServer {
195
195
  // 如果原始配置也不可用,返回错误
196
196
  return res.status(404).json({ error: 'No matching route found and no original config available' });
197
197
  }
198
- // 高智商请求判定:从消息结构推断是否启用,不再使用 !x 显式关闭语法
198
+ // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
199
199
  const forcedContentType = yield this.prepareHighIqRouting(req, route, route.targetType);
200
- // 检查是否启用故障切换
201
200
  const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
202
201
  if (!enableFailover) {
203
202
  // 故障切换已禁用,使用传统的单一规则匹配
@@ -294,7 +293,7 @@ class ProxyServer {
294
293
  }
295
294
  else {
296
295
  // HTTP错误,检查状态码
297
- const statusCode = ((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) || 500;
296
+ const statusCode = this.getErrorStatusCode(error, 500);
298
297
  if (statusCode >= 400) {
299
298
  yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
300
299
  console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
@@ -340,20 +339,28 @@ class ProxyServer {
340
339
  });
341
340
  // 确定目标类型
342
341
  const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
343
- // 记录错误日志 - 包含请求详情
342
+ // 记录错误日志 - 包含请求详情和最后失败的服务信息
343
+ const _lastFailedVendor = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
344
344
  yield this.dbManager.addErrorLog({
345
345
  timestamp: Date.now(),
346
346
  method: req.method,
347
347
  path: req.path,
348
348
  statusCode: 503,
349
- errorMessage: 'All services failed',
349
+ errorMessage: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
350
350
  errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
351
351
  requestHeaders: this.normalizeHeaders(req.headers),
352
352
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
353
353
  // 添加请求详情
354
354
  targetType,
355
- requestModel: (_d = req.body) === null || _d === void 0 ? void 0 : _d.model,
356
- responseTime: 0,
355
+ requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
356
+ responseTime: Date.now() - requestStartAt,
357
+ // 添加最后失败的服务信息
358
+ ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
359
+ targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
360
+ targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
361
+ targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
362
+ vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
363
+ vendorName: _lastFailedVendor === null || _lastFailedVendor === void 0 ? void 0 : _lastFailedVendor.name,
357
364
  });
358
365
  // 根据路径判断目标类型并返回适当的错误格式
359
366
  const isClaudeCode = req.path.startsWith('/claude-code/');
@@ -400,7 +407,7 @@ class ProxyServer {
400
407
  // 添加请求详情
401
408
  targetType,
402
409
  requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
403
- responseTime: 0,
410
+ responseTime: Date.now() - requestStartAt,
404
411
  });
405
412
  // 根据路径判断目标类型并返回适当的错误格式
406
413
  const isClaudeCode = req.path.startsWith('/claude-code/');
@@ -462,7 +469,7 @@ class ProxyServer {
462
469
  });
463
470
  return res.status(404).json({ error: `No active route found for target type: ${targetType}` });
464
471
  }
465
- // 高智商请求判定:从消息结构推断是否启用,不再使用 !x 显式关闭语法
472
+ // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
466
473
  const forcedContentType = yield this.prepareHighIqRouting(req, route, targetType);
467
474
  // 检查是否启用故障切换
468
475
  const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
@@ -561,7 +568,7 @@ class ProxyServer {
561
568
  }
562
569
  else {
563
570
  // HTTP错误,检查状态码
564
- const statusCode = ((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) || 500;
571
+ const statusCode = this.getErrorStatusCode(error, 500);
565
572
  if (statusCode >= 400) {
566
573
  yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
567
574
  console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
@@ -605,20 +612,28 @@ class ProxyServer {
605
612
  error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
606
613
  tags: this.buildRelayTags(hasRelayAttempt),
607
614
  });
608
- // 记录错误日志 - 包含请求详情(使用函数参数 targetType)
615
+ // 记录错误日志 - 包含请求详情和最后失败的服务信息(使用函数参数 targetType)
616
+ const _lastFailedVendor2 = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
609
617
  yield this.dbManager.addErrorLog({
610
618
  timestamp: Date.now(),
611
619
  method: req.method,
612
620
  path: req.path,
613
621
  statusCode: 503,
614
- errorMessage: 'All services failed',
622
+ errorMessage: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
615
623
  errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
616
624
  requestHeaders: this.normalizeHeaders(req.headers),
617
625
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
618
626
  // 添加请求详情
619
627
  targetType,
620
- requestModel: (_d = req.body) === null || _d === void 0 ? void 0 : _d.model,
621
- responseTime: 0,
628
+ requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
629
+ responseTime: Date.now() - requestStartAt,
630
+ // 添加最后失败的服务信息
631
+ ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
632
+ targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
633
+ targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
634
+ targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
635
+ vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
636
+ vendorName: _lastFailedVendor2 === null || _lastFailedVendor2 === void 0 ? void 0 : _lastFailedVendor2.name,
622
637
  });
623
638
  // 根据路径判断目标类型并返回适当的错误格式
624
639
  const isClaudeCode = req.path.startsWith('/claude-code/');
@@ -664,7 +679,7 @@ class ProxyServer {
664
679
  // 添加请求详情
665
680
  targetType,
666
681
  requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
667
- responseTime: 0,
682
+ responseTime: Date.now() - requestStartAt,
668
683
  });
669
684
  if (this.isResponseCommitted(res)) {
670
685
  return;
@@ -882,15 +897,62 @@ class ProxyServer {
882
897
  }
883
898
  return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
884
899
  }
900
+ /**
901
+ * 解析规则的有效超时时间(毫秒)。
902
+ * 优先级:rule.timeout > config.ruleGlobalTimeout * 1000 > 300000(5分钟)
903
+ */
904
+ resolveEffectiveTimeout(rule) {
905
+ if (rule.timeout && rule.timeout > 0) {
906
+ return rule.timeout;
907
+ }
908
+ const config = this.dbManager.getConfig();
909
+ if (config.ruleGlobalTimeout && config.ruleGlobalTimeout > 0) {
910
+ return config.ruleGlobalTimeout * 1000;
911
+ }
912
+ return 300000;
913
+ }
885
914
  createFailoverError(message, statusCode, originalError) {
886
915
  const failoverError = new Error(message);
887
916
  failoverError.isFailoverCandidate = true;
917
+ failoverError.statusCode = statusCode;
888
918
  failoverError.response = { status: statusCode };
889
919
  if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
890
920
  failoverError.stack = originalError.stack;
891
921
  }
892
922
  return failoverError;
893
923
  }
924
+ getErrorStatusCode(error, fallbackStatusCode = 500) {
925
+ var _a, _b, _c;
926
+ const statusCode = (_c = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.statusCode) !== null && _c !== void 0 ? _c : error === null || error === void 0 ? void 0 : error.status;
927
+ if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
928
+ return statusCode;
929
+ }
930
+ return fallbackStatusCode;
931
+ }
932
+ detectStreamFailure(events) {
933
+ var _a, _b;
934
+ for (const event of events) {
935
+ const eventType = (_a = event.event) === null || _a === void 0 ? void 0 : _a.trim();
936
+ if (!eventType)
937
+ continue;
938
+ if (eventType !== 'response.failed' && eventType !== 'error') {
939
+ continue;
940
+ }
941
+ const parsed = event.data ? this.safeJsonParse(event.data) : null;
942
+ const errorObj = ((_b = parsed === null || parsed === void 0 ? void 0 : parsed.response) === null || _b === void 0 ? void 0 : _b.error) || (parsed === null || parsed === void 0 ? void 0 : parsed.error) || parsed;
943
+ const errorCode = errorObj === null || errorObj === void 0 ? void 0 : errorObj.code;
944
+ const errorMessage = (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message)
945
+ || (parsed === null || parsed === void 0 ? void 0 : parsed.message)
946
+ || `Upstream stream returned ${eventType}`;
947
+ const normalizedMessage = `Upstream stream returned ${eventType}: ${errorMessage}`;
948
+ const statusCode = errorCode === 'server_is_overloaded' ? 503 : 502;
949
+ return {
950
+ statusCode,
951
+ errorMessage: normalizedMessage,
952
+ };
953
+ }
954
+ return null;
955
+ }
894
956
  isDownstreamClosed(res) {
895
957
  return res.destroyed || res.writableEnded || !res.writable;
896
958
  }
@@ -1390,7 +1452,7 @@ class ProxyServer {
1390
1452
  },
1391
1453
  {
1392
1454
  type: 'high-iq',
1393
- match: (_req, body) => this.hasHighIqSignal(body),
1455
+ match: (_req, body, _sessionId, routeId) => this.hasHighIqSignal(body, routeId),
1394
1456
  },
1395
1457
  {
1396
1458
  type: 'long-context',
@@ -1534,11 +1596,19 @@ class ProxyServer {
1534
1596
  ((_a = body === null || body === void 0 ? void 0 : body.reasoning) === null || _a === void 0 ? void 0 : _a.effort) ||
1535
1597
  ((_b = body === null || body === void 0 ? void 0 : body.reasoning) === null || _b === void 0 ? void 0 : _b.enabled));
1536
1598
  }
1537
- hasHighIqSignal(body) {
1599
+ hasHighIqRuleForRoute(routeId) {
1600
+ var _a;
1601
+ const rules = this.getRulesByRouteId(routeId);
1602
+ return (_a = rules === null || rules === void 0 ? void 0 : rules.some(rule => rule.contentType === 'high-iq' && !rule.isDisabled)) !== null && _a !== void 0 ? _a : false;
1603
+ }
1604
+ hasHighIqSignal(body, routeId) {
1605
+ if (routeId && !this.hasHighIqRuleForRoute(routeId))
1606
+ return false;
1538
1607
  return this.inferHighIqRouting(body, false).shouldUseHighIq;
1539
1608
  }
1540
1609
  inferHighIqRouting(body, previousMode) {
1541
1610
  const messages = this.extractConversationMessages(body);
1611
+ // 从消息列表末尾往前查找 [!] 或 [x] 标记,普通消息跳过继续搜索
1542
1612
  for (let i = messages.length - 1; i >= 0; i--) {
1543
1613
  const message = messages[i];
1544
1614
  if ((message === null || message === void 0 ? void 0 : message.role) !== 'user') {
@@ -1548,17 +1618,22 @@ class ProxyServer {
1548
1618
  if (!signal.hasHumanText) {
1549
1619
  continue;
1550
1620
  }
1621
+ // [x] 优先:同一消息中 [x] 覆盖 [!]
1622
+ if (signal.hasCancelPrefix) {
1623
+ return {
1624
+ shouldUseHighIq: false,
1625
+ decisionSource: 'human',
1626
+ };
1627
+ }
1551
1628
  if (signal.hasHighIqPrefix) {
1552
1629
  return {
1553
1630
  shouldUseHighIq: true,
1554
1631
  decisionSource: 'human',
1555
1632
  };
1556
1633
  }
1557
- return {
1558
- shouldUseHighIq: false,
1559
- decisionSource: 'human',
1560
- };
1634
+ // 普通消息(无 [!] 或 [x] 前缀),继续向前搜索
1561
1635
  }
1636
+ // 未找到 [!] 或 [x] 标记,回退到 session 持久化状态
1562
1637
  if (previousMode) {
1563
1638
  return {
1564
1639
  shouldUseHighIq: true,
@@ -1572,6 +1647,10 @@ class ProxyServer {
1572
1647
  }
1573
1648
  prepareHighIqRouting(req, route, targetType) {
1574
1649
  return __awaiter(this, void 0, void 0, function* () {
1650
+ // 无高智商规则时直接跳过,避免每次都检查消息前缀
1651
+ if (!this.hasHighIqRuleForRoute(route.id)) {
1652
+ return undefined;
1653
+ }
1575
1654
  const sessionId = this.defaultExtractSessionId(req, targetType);
1576
1655
  const session = sessionId ? this.dbManager.getSession(sessionId) : null;
1577
1656
  const previousMode = (session === null || session === void 0 ? void 0 : session.highIqMode) === true;
@@ -1583,7 +1662,7 @@ class ProxyServer {
1583
1662
  highIqRuleId: undefined,
1584
1663
  lastRequestAt: Date.now(),
1585
1664
  });
1586
- console.log(`[HIGH-IQ] Session ${sessionId} auto-disabled by latest human message`);
1665
+ console.log(`[HIGH-IQ] Session ${sessionId} cancelled by [x] prefix`);
1587
1666
  }
1588
1667
  return undefined;
1589
1668
  }
@@ -1641,6 +1720,7 @@ class ProxyServer {
1641
1720
  analyzeUserMessageForHighIq(message) {
1642
1721
  let hasHumanText = false;
1643
1722
  let hasHighIqPrefix = false;
1723
+ let hasCancelPrefix = false;
1644
1724
  const scanText = (text, treatAsHuman) => {
1645
1725
  const trimmed = text.trim();
1646
1726
  if (!trimmed) {
@@ -1652,11 +1732,14 @@ class ProxyServer {
1652
1732
  if (treatAsHuman && trimmed.startsWith('[!]')) {
1653
1733
  hasHighIqPrefix = true;
1654
1734
  }
1735
+ if (treatAsHuman && /^\[x]/i.test(trimmed)) {
1736
+ hasCancelPrefix = true;
1737
+ }
1655
1738
  };
1656
1739
  const content = message === null || message === void 0 ? void 0 : message.content;
1657
1740
  if (typeof content === 'string') {
1658
1741
  scanText(content, true);
1659
- return { hasHumanText, hasHighIqPrefix };
1742
+ return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
1660
1743
  }
1661
1744
  const blocks = Array.isArray(content) ? content : [content];
1662
1745
  for (const block of blocks) {
@@ -1692,7 +1775,7 @@ class ProxyServer {
1692
1775
  }
1693
1776
  }
1694
1777
  }
1695
- return { hasHumanText, hasHighIqPrefix };
1778
+ return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
1696
1779
  }
1697
1780
  /**
1698
1781
  * 查找可用的高智商规则
@@ -2134,7 +2217,7 @@ class ProxyServer {
2134
2217
  const headers = {};
2135
2218
  for (const [key, value] of Object.entries(req.headers)) {
2136
2219
  // 排除原始认证头,防止与代理设置的认证头冲突
2137
- if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key'].includes(key.toLowerCase())) {
2220
+ if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key', 'accept-encoding'].includes(key.toLowerCase())) {
2138
2221
  continue;
2139
2222
  }
2140
2223
  if (typeof value === 'string') {
@@ -2169,6 +2252,10 @@ class ProxyServer {
2169
2252
  if (streamRequested && !headers.accept) {
2170
2253
  headers.accept = 'text/event-stream';
2171
2254
  }
2255
+ // 流式场景显式禁用压缩,避免上游返回压缩字节流导致下游出现乱码
2256
+ if (streamRequested) {
2257
+ headers['accept-encoding'] = 'identity';
2258
+ }
2172
2259
  if (!headers.connection) {
2173
2260
  if (streamRequested) {
2174
2261
  headers.connection = 'keep-alive';
@@ -2280,6 +2367,26 @@ class ProxyServer {
2280
2367
  return null;
2281
2368
  }
2282
2369
  }
2370
+ cloneRequestBody(data) {
2371
+ if (data === null || data === undefined) {
2372
+ return data;
2373
+ }
2374
+ try {
2375
+ return JSON.parse(JSON.stringify(data));
2376
+ }
2377
+ catch (_a) {
2378
+ return data;
2379
+ }
2380
+ }
2381
+ isEmptyResponse(data) {
2382
+ if (data === null || data === undefined)
2383
+ return true;
2384
+ if (typeof data === 'string' && data.trim() === '')
2385
+ return true;
2386
+ if (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0)
2387
+ return true;
2388
+ return false;
2389
+ }
2283
2390
  /**
2284
2391
  * 从请求中提取 session ID(默认方法)
2285
2392
  * Claude Code: metadata.user_id
@@ -2692,7 +2799,7 @@ class ProxyServer {
2692
2799
  }
2693
2800
  proxyRequest(req, res, route, rule, service, options) {
2694
2801
  return __awaiter(this, void 0, void 0, function* () {
2695
- var _a, _b, _c, _d, _e, _f, _g;
2802
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
2696
2803
  res.locals.skipLog = true;
2697
2804
  const startTime = Date.now();
2698
2805
  const rawSourceType = service.sourceType || 'openai-chat';
@@ -2706,7 +2813,8 @@ class ProxyServer {
2706
2813
  const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
2707
2814
  const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
2708
2815
  let relayedForLog = !useOriginalConfig;
2709
- let requestBody = req.body || {};
2816
+ const originalToolRequestBody = this.cloneRequestBody(req.body || {});
2817
+ let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
2710
2818
  let usageForLog;
2711
2819
  let logged = false;
2712
2820
  const extraTagsForLog = [];
@@ -3065,8 +3173,9 @@ class ProxyServer {
3065
3173
  res.once('close', onResponseClosed);
3066
3174
  try {
3067
3175
  // 使用统一的请求转换方法
3068
- const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, requestBody, rule.targetModel);
3069
- requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : requestBody) !== null && _b !== void 0 ? _b : {};
3176
+ const payloadForTransform = this.cloneRequestBody(originalToolRequestBody);
3177
+ const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel);
3178
+ requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
3070
3179
  // 应用 max_output_tokens 限制
3071
3180
  requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
3072
3181
  if (this.shouldDefaultStreamingForClaudeBridge(req, targetType, sourceType, requestBody)
@@ -3091,7 +3200,7 @@ class ProxyServer {
3091
3200
  method: req.method,
3092
3201
  url: upstreamUrl,
3093
3202
  headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested, requestBody),
3094
- timeout: rule.timeout || 3000000, // 默认300秒
3203
+ timeout: this.resolveEffectiveTimeout(rule),
3095
3204
  validateStatus: () => true,
3096
3205
  responseType: streamRequested ? 'stream' : 'json',
3097
3206
  signal: upstreamAbortController.signal,
@@ -3242,6 +3351,7 @@ class ProxyServer {
3242
3351
  requestHeaders: this.normalizeHeaders(req.headers),
3243
3352
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
3244
3353
  upstreamRequest: upstreamRequestForLog,
3354
+ responseHeaders: responseHeadersForLog,
3245
3355
  // 添加请求详情
3246
3356
  ruleId: rule.id,
3247
3357
  targetType,
@@ -3347,6 +3457,7 @@ class ProxyServer {
3347
3457
  requestHeaders: this.normalizeHeaders(req.headers),
3348
3458
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
3349
3459
  upstreamRequest: upstreamRequestForLog,
3460
+ responseHeaders: responseHeadersForLog,
3350
3461
  // 添加请求详情
3351
3462
  ruleId: rule.id,
3352
3463
  targetType,
@@ -3441,6 +3552,7 @@ class ProxyServer {
3441
3552
  requestHeaders: this.normalizeHeaders(req.headers),
3442
3553
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
3443
3554
  upstreamRequest: upstreamRequestForLog,
3555
+ responseHeaders: responseHeadersForLog,
3444
3556
  ruleId: rule.id,
3445
3557
  targetType,
3446
3558
  targetServiceId: service.id,
@@ -3537,6 +3649,7 @@ class ProxyServer {
3537
3649
  requestHeaders: this.normalizeHeaders(req.headers),
3538
3650
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
3539
3651
  upstreamRequest: upstreamRequestForLog,
3652
+ responseHeaders: responseHeadersForLog,
3540
3653
  ruleId: rule.id,
3541
3654
  targetType,
3542
3655
  targetServiceId: service.id,
@@ -3599,94 +3712,142 @@ class ProxyServer {
3599
3712
  else if (extractedUsage) {
3600
3713
  usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
3601
3714
  }
3602
- void finalizeLog(res.statusCode);
3603
3715
  };
3604
- // 在下游 chunk 收集器完成后执行,确保拿到真正下发给客户端的完整文本
3605
- downstreamChunkCollector.on('finish', () => {
3606
- finalizeChunks();
3607
- });
3608
- // 备用:如果eventCollector的finish没有触发,监听res的finish
3609
- res.on('finish', () => {
3610
- if (!streamChunksForLog) {
3611
- finalizeChunks();
3612
- }
3613
- });
3614
3716
  // 监听 res 的错误事件
3615
3717
  res.on('error', (err) => {
3616
3718
  console.error('[Proxy] Response stream error:', err);
3617
3719
  });
3618
- // 构建 pipeline,根据是否需要转换选择不同的处理链
3619
- if (converter) {
3720
+ const runStreamPipeline = () => __awaiter(this, void 0, void 0, function* () {
3620
3721
  ensureResponseWritable();
3621
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => __awaiter(this, void 0, void 0, function* () {
3622
- if (error) {
3623
- if (this.isClientDisconnectError(error, res)) {
3624
- console.warn('[Proxy] Default stream pipeline closed because client disconnected');
3625
- yield finalizeLog(499, 'Client disconnected');
3626
- return;
3627
- }
3628
- console.error('[Proxy] Pipeline error (default stream with converter):', error);
3629
- // 记录到错误日志
3630
- try {
3631
- yield this.dbManager.addErrorLog({
3632
- timestamp: Date.now(),
3633
- method: req.method,
3634
- path: req.path,
3635
- statusCode: 500,
3636
- errorMessage: error.message || 'Stream processing error',
3637
- errorStack: error.stack,
3638
- requestHeaders: this.normalizeHeaders(req.headers),
3639
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3640
- upstreamRequest: upstreamRequestForLog,
3641
- });
3642
- }
3643
- catch (logError) {
3644
- console.error('[Proxy] Failed to log error:', logError);
3645
- }
3646
- yield finalizeLog(500, error.message);
3722
+ return yield new Promise((resolve, reject) => {
3723
+ if (converter) {
3724
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
3725
+ if (error) {
3726
+ reject(error);
3727
+ return;
3728
+ }
3729
+ resolve();
3730
+ });
3731
+ return;
3647
3732
  }
3648
- }));
3649
- }
3650
- else {
3651
- ensureResponseWritable();
3652
- (0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => __awaiter(this, void 0, void 0, function* () {
3653
- if (error) {
3654
- if (this.isClientDisconnectError(error, res)) {
3655
- console.warn('[Proxy] Default stream pipeline closed because client disconnected');
3656
- yield finalizeLog(499, 'Client disconnected');
3733
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
3734
+ if (error) {
3735
+ reject(error);
3657
3736
  return;
3658
3737
  }
3659
- console.error('[Proxy] Pipeline error (default stream):', error);
3660
- // 记录到错误日志
3661
- try {
3662
- yield this.dbManager.addErrorLog({
3663
- timestamp: Date.now(),
3664
- method: req.method,
3665
- path: req.path,
3666
- statusCode: 500,
3667
- errorMessage: error.message || 'Stream processing error',
3668
- errorStack: error.stack,
3669
- requestHeaders: this.normalizeHeaders(req.headers),
3670
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3671
- upstreamRequest: upstreamRequestForLog,
3672
- });
3673
- }
3674
- catch (logError) {
3675
- console.error('[Proxy] Failed to log error:', logError);
3676
- }
3677
- yield finalizeLog(500, error.message);
3678
- }
3679
- }));
3738
+ resolve();
3739
+ });
3740
+ });
3741
+ });
3742
+ try {
3743
+ yield runStreamPipeline();
3680
3744
  }
3745
+ catch (error) {
3746
+ if (this.isClientDisconnectError(error, res)) {
3747
+ console.warn('[Proxy] Default stream pipeline closed because client disconnected');
3748
+ yield finalizeLog(499, 'Client disconnected');
3749
+ return;
3750
+ }
3751
+ console.error('[Proxy] Pipeline error (default stream):', error);
3752
+ // 记录到错误日志
3753
+ try {
3754
+ yield this.dbManager.addErrorLog({
3755
+ timestamp: Date.now(),
3756
+ method: req.method,
3757
+ path: req.path,
3758
+ statusCode: 500,
3759
+ errorMessage: error.message || 'Stream processing error',
3760
+ errorStack: error.stack,
3761
+ requestHeaders: this.normalizeHeaders(req.headers),
3762
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
3763
+ upstreamRequest: upstreamRequestForLog,
3764
+ responseHeaders: responseHeadersForLog,
3765
+ ruleId: rule.id,
3766
+ targetType,
3767
+ targetServiceId: service.id,
3768
+ targetServiceName: service.name,
3769
+ targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
3770
+ vendorId: service.vendorId,
3771
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3772
+ requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
3773
+ responseTime: Date.now() - startTime,
3774
+ });
3775
+ }
3776
+ catch (logError) {
3777
+ console.error('[Proxy] Failed to log error:', logError);
3778
+ }
3779
+ yield finalizeLog(500, error.message);
3780
+ if (failoverEnabled && !this.isResponseCommitted(res)) {
3781
+ throw this.createFailoverError(error.message || 'Stream processing error', 500, error);
3782
+ }
3783
+ return;
3784
+ }
3785
+ finalizeChunks();
3786
+ // 检测空流:上游返回 SSE Content-Type 但没有发送任何事件数据
3787
+ const collectedEvents = eventCollector.getEvents();
3788
+ if (collectedEvents.length === 0) {
3789
+ const emptyStreamMsg = 'Upstream API returned an empty stream (HTTP 200, no SSE events)';
3790
+ console.warn(`[Proxy] ${emptyStreamMsg}`);
3791
+ yield finalizeLog(200, emptyStreamMsg);
3792
+ if (!res.writableEnded) {
3793
+ res.end();
3794
+ }
3795
+ return;
3796
+ }
3797
+ // 关键修复:识别 stream 内部的 response.failed / error 事件,归类为错误并触发 failover 交接
3798
+ const streamFailure = this.detectStreamFailure(collectedEvents);
3799
+ if (streamFailure) {
3800
+ try {
3801
+ yield this.dbManager.addErrorLog({
3802
+ timestamp: Date.now(),
3803
+ method: req.method,
3804
+ path: req.path,
3805
+ statusCode: streamFailure.statusCode,
3806
+ errorMessage: streamFailure.errorMessage,
3807
+ requestHeaders: this.normalizeHeaders(req.headers),
3808
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
3809
+ upstreamRequest: upstreamRequestForLog,
3810
+ responseHeaders: responseHeadersForLog,
3811
+ responseBody: responseBodyForLog,
3812
+ ruleId: rule.id,
3813
+ targetType,
3814
+ targetServiceId: service.id,
3815
+ targetServiceName: service.name,
3816
+ targetModel: rule.targetModel || ((_f = req.body) === null || _f === void 0 ? void 0 : _f.model),
3817
+ vendorId: service.vendorId,
3818
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3819
+ requestModel: (_g = req.body) === null || _g === void 0 ? void 0 : _g.model,
3820
+ responseTime: Date.now() - startTime,
3821
+ });
3822
+ }
3823
+ catch (logError) {
3824
+ console.error('[Proxy] Failed to log stream failure:', logError);
3825
+ }
3826
+ yield finalizeLog(streamFailure.statusCode, streamFailure.errorMessage);
3827
+ if (failoverEnabled && !this.isResponseCommitted(res)) {
3828
+ throw this.createFailoverError(streamFailure.errorMessage, streamFailure.statusCode);
3829
+ }
3830
+ return;
3831
+ }
3832
+ yield finalizeLog(res.statusCode);
3681
3833
  return;
3682
3834
  }
3683
3835
  let responseData = response.data;
3684
3836
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
3685
3837
  const raw = yield this.readStreamBody(response.data);
3686
- responseData = (_d = this.safeJsonParse(raw)) !== null && _d !== void 0 ? _d : raw;
3838
+ responseData = (_h = this.safeJsonParse(raw)) !== null && _h !== void 0 ? _h : raw;
3687
3839
  }
3688
3840
  // 收集响应头
3689
3841
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3842
+ // 检测上游空响应(HTTP 200 但 body 为空)— 透传 200
3843
+ if (this.isEmptyResponse(responseData)) {
3844
+ const emptyInfoMsg = 'Upstream API returned an empty response (HTTP 200), passing through';
3845
+ console.warn(`[Proxy] ${emptyInfoMsg}`);
3846
+ responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
3847
+ yield finalizeLog(200, emptyInfoMsg);
3848
+ res.status(200).end();
3849
+ return;
3850
+ }
3690
3851
  // 使用统一的响应转换方法
3691
3852
  const converted = this.transformResponseToTool(targetType, sourceType, responseData);
3692
3853
  // 提取 token usage(从原始响应数据中提取)
@@ -3719,14 +3880,62 @@ class ProxyServer {
3719
3880
  yield finalizeLog(499, 'Client disconnected');
3720
3881
  return;
3721
3882
  }
3883
+ // 特殊处理:count_tokens 请求无论如何都返回 200
3884
+ const isCountTokensRequest = this.isCountTokensPath(req.path) || this.isCountTokensPath(req.originalUrl);
3885
+ if (isCountTokensRequest) {
3886
+ console.warn('[Proxy] count_tokens request failed, falling back to local estimation:', error.message);
3887
+ // 使用本地估算返回结果
3888
+ const inputTokens = this.estimateClaudeCountTokens(requestBody);
3889
+ const localTokenResponse = { input_tokens: inputTokens };
3890
+ usageForLog = {
3891
+ inputTokens,
3892
+ outputTokens: 0,
3893
+ totalTokens: inputTokens,
3894
+ };
3895
+ responseHeadersForLog = {
3896
+ 'content-type': 'application/json; charset=utf-8',
3897
+ };
3898
+ responseBodyForLog = JSON.stringify(localTokenResponse);
3899
+ streamChunksForLog = undefined;
3900
+ relayedForLog = false;
3901
+ extraTagsForLog.push('上游失败-本地计算Token');
3902
+ // 记录错误日志(但不影响响应)
3903
+ const vendors = this.dbManager.getVendors();
3904
+ const vendor = vendors.find(v => v.id === service.vendorId);
3905
+ yield this.dbManager.addErrorLog({
3906
+ timestamp: Date.now(),
3907
+ method: req.method,
3908
+ path: req.path,
3909
+ statusCode: 200, // 实际返回 200
3910
+ errorMessage: `count_tokens upstream failed, used local estimation: ${error.message}`,
3911
+ errorStack: error.stack,
3912
+ requestHeaders: this.normalizeHeaders(req.headers),
3913
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
3914
+ ruleId: rule.id,
3915
+ targetType,
3916
+ targetServiceId: service.id,
3917
+ targetServiceName: service.name,
3918
+ targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
3919
+ vendorId: service.vendorId,
3920
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3921
+ requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
3922
+ upstreamRequest: upstreamRequestForLog,
3923
+ responseTime: Date.now() - startTime,
3924
+ });
3925
+ // 返回 200 状态码和本地估算结果
3926
+ res.status(200).json(localTokenResponse);
3927
+ yield finalizeLog(200);
3928
+ return;
3929
+ }
3722
3930
  if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
3723
3931
  throw error;
3724
3932
  }
3725
3933
  console.error('Proxy error:', error);
3726
3934
  // 检测是否是 timeout 错误
3727
3935
  const isTimeout = error.code === 'ECONNABORTED' ||
3728
- ((_e = error.message) === null || _e === void 0 ? void 0 : _e.toLowerCase().includes('timeout')) ||
3936
+ ((_l = error.message) === null || _l === void 0 ? void 0 : _l.toLowerCase().includes('timeout')) ||
3729
3937
  (error.errno && error.errno === 'ETIMEDOUT');
3938
+ const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
3730
3939
  const baseErrorMessage = isTimeout
3731
3940
  ? 'Request timeout - the upstream API took too long to respond'
3732
3941
  : (error.message || 'Internal server error');
@@ -3740,7 +3949,7 @@ class ProxyServer {
3740
3949
  timestamp: Date.now(),
3741
3950
  method: req.method,
3742
3951
  path: req.path,
3743
- statusCode: isTimeout ? 504 : 500,
3952
+ statusCode,
3744
3953
  errorMessage: errorMessage,
3745
3954
  errorStack: error.stack,
3746
3955
  requestHeaders: this.normalizeHeaders(req.headers),
@@ -3750,16 +3959,17 @@ class ProxyServer {
3750
3959
  targetType,
3751
3960
  targetServiceId: service.id,
3752
3961
  targetServiceName: service.name,
3753
- targetModel: rule.targetModel || ((_f = req.body) === null || _f === void 0 ? void 0 : _f.model),
3962
+ targetModel: rule.targetModel || ((_m = req.body) === null || _m === void 0 ? void 0 : _m.model),
3754
3963
  vendorId: service.vendorId,
3755
3964
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3756
- requestModel: (_g = req.body) === null || _g === void 0 ? void 0 : _g.model,
3965
+ requestModel: (_o = req.body) === null || _o === void 0 ? void 0 : _o.model,
3757
3966
  upstreamRequest: upstreamRequestForLog,
3967
+ responseHeaders: responseHeadersForLog,
3758
3968
  responseTime: Date.now() - startTime,
3759
3969
  });
3760
- yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
3970
+ yield finalizeLog(statusCode, errorMessage);
3761
3971
  if (failoverEnabled) {
3762
- throw this.createFailoverError(errorMessage, isTimeout ? 504 : 500, error);
3972
+ throw this.createFailoverError(errorMessage, statusCode, error);
3763
3973
  }
3764
3974
  if (this.isResponseCommitted(res)) {
3765
3975
  return;
@@ -3788,12 +3998,12 @@ class ProxyServer {
3788
3998
  }
3789
3999
  else {
3790
4000
  // 非流式请求:返回 JSON 格式
3791
- res.status(500).json(claudeError);
4001
+ res.status(statusCode).json(claudeError);
3792
4002
  }
3793
4003
  }
3794
4004
  else {
3795
4005
  // 对于 Codex,返回 JSON 格式的错误响应
3796
- res.status(500).json({ error: errorMessage });
4006
+ res.status(statusCode).json({ error: errorMessage });
3797
4007
  }
3798
4008
  }
3799
4009
  finally {