aicodeswitch 3.0.18 → 3.0.20

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/README.md CHANGED
@@ -30,6 +30,10 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
30
30
  * 自定义API Key,支持B/S架构,让aicodeswitch成为在线服务,提供给团队使用
31
31
  * 数据完全本地,自主可控
32
32
 
33
+ ## 桌面客户端
34
+
35
+ [进入下载](https://github.com/tangshuang/aicodeswitch/releases)
36
+
33
37
  ## 命令行工具
34
38
 
35
39
  ### 安装
@@ -111,6 +111,14 @@ class ProxyServer {
111
111
  writable: true,
112
112
  value: 60000
113
113
  }); // 去重缓存1分钟过期
114
+ // 频率限制跟踪:用于跟踪每个规则在当前时间窗口内的请求数
115
+ // key: ruleId, value: { count: number, windowStart: number }
116
+ Object.defineProperty(this, "frequencyLimitTracker", {
117
+ enumerable: true,
118
+ configurable: true,
119
+ writable: true,
120
+ value: new Map()
121
+ });
114
122
  this.dbManager = dbManager;
115
123
  this.config = dbManager.getConfig();
116
124
  this.app = app;
@@ -150,7 +158,8 @@ class ProxyServer {
150
158
  }
151
159
  // 尝试每个规则,直到成功或全部失败
152
160
  let lastError = null;
153
- for (const rule of allRules) {
161
+ for (let index = 0; index < allRules.length; index++) {
162
+ const rule = allRules[index];
154
163
  const service = this.getServiceById(rule.targetServiceId);
155
164
  if (!service)
156
165
  continue;
@@ -161,8 +170,12 @@ class ProxyServer {
161
170
  continue;
162
171
  }
163
172
  try {
173
+ const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
164
174
  // 尝试代理请求
165
- yield this.proxyRequest(req, res, route, rule, service);
175
+ yield this.proxyRequest(req, res, route, rule, service, {
176
+ failoverEnabled: true,
177
+ forwardedToServiceName: nextServiceName,
178
+ });
166
179
  return; // 成功,直接返回
167
180
  }
168
181
  catch (error) {
@@ -331,7 +344,8 @@ class ProxyServer {
331
344
  }
332
345
  // 尝试每个规则,直到成功或全部失败
333
346
  let lastError = null;
334
- for (const rule of allRules) {
347
+ for (let index = 0; index < allRules.length; index++) {
348
+ const rule = allRules[index];
335
349
  const service = this.getServiceById(rule.targetServiceId);
336
350
  if (!service)
337
351
  continue;
@@ -342,8 +356,12 @@ class ProxyServer {
342
356
  continue;
343
357
  }
344
358
  try {
359
+ const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
345
360
  // 尝试代理请求
346
- yield this.proxyRequest(req, res, route, rule, service);
361
+ yield this.proxyRequest(req, res, route, rule, service, {
362
+ failoverEnabled: true,
363
+ forwardedToServiceName: nextServiceName,
364
+ });
347
365
  return; // 成功,直接返回
348
366
  }
349
367
  catch (error) {
@@ -466,6 +484,9 @@ class ProxyServer {
466
484
  const routeRules = this.dbManager.getRules(routeId);
467
485
  return routeRules.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
468
486
  }
487
+ getRuleById(ruleId) {
488
+ return this.dbManager.getRule(ruleId);
489
+ }
469
490
  findMatchingRoute(req) {
470
491
  // 根据请求路径确定目标类型
471
492
  let targetType;
@@ -486,6 +507,36 @@ class ProxyServer {
486
507
  const activeRoutes = this.getActiveRoutes();
487
508
  return activeRoutes.find(route => route.targetType === targetType && route.isActive);
488
509
  }
510
+ findNextAvailableServiceName(allRules, startIndex, routeId) {
511
+ return __awaiter(this, void 0, void 0, function* () {
512
+ for (let index = startIndex; index < allRules.length; index++) {
513
+ const rule = allRules[index];
514
+ const service = this.getServiceById(rule.targetServiceId);
515
+ if (!service)
516
+ continue;
517
+ const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, routeId, rule.contentType);
518
+ if (isBlacklisted)
519
+ continue;
520
+ return service.name;
521
+ }
522
+ return undefined;
523
+ });
524
+ }
525
+ buildFailoverHint(forwardedToServiceName) {
526
+ if (!forwardedToServiceName) {
527
+ return '';
528
+ }
529
+ return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
530
+ }
531
+ createFailoverError(message, statusCode, originalError) {
532
+ const failoverError = new Error(message);
533
+ failoverError.isFailoverCandidate = true;
534
+ failoverError.response = { status: statusCode };
535
+ if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
536
+ failoverError.stack = originalError.stack;
537
+ }
538
+ return failoverError;
539
+ }
489
540
  /**
490
541
  * 计算请求内容的哈希值,用于去重
491
542
  * 基于请求的关键字段生成唯一标识
@@ -564,6 +615,32 @@ class ProxyServer {
564
615
  }
565
616
  }
566
617
  }
618
+ /**
619
+ * 清理过期的频率限制跟踪数据
620
+ */
621
+ cleanExpiredFrequencyTrackers() {
622
+ const now = Date.now();
623
+ const rules = this.dbManager.getRules();
624
+ const activeRuleIds = new Set(rules.map((r) => r.id));
625
+ for (const ruleId of this.frequencyLimitTracker.keys()) {
626
+ // 清理不再存在的规则的跟踪数据
627
+ if (!activeRuleIds.has(ruleId)) {
628
+ this.frequencyLimitTracker.delete(ruleId);
629
+ continue;
630
+ }
631
+ // 清理超时的跟踪数据
632
+ const tracker = this.frequencyLimitTracker.get(ruleId);
633
+ if (tracker) {
634
+ const rule = this.dbManager.getRule(ruleId);
635
+ if (rule && rule.frequencyWindow) {
636
+ const windowMs = rule.frequencyWindow * 1000;
637
+ if (now - tracker.windowStart > windowMs * 2) {
638
+ this.frequencyLimitTracker.delete(ruleId);
639
+ }
640
+ }
641
+ }
642
+ }
643
+ }
567
644
  /**
568
645
  * 根据GLM计费逻辑判断请求是否应该计费
569
646
  * 核心规则:
@@ -678,6 +755,10 @@ class ProxyServer {
678
755
  if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
679
756
  continue; // 跳过超限规则
680
757
  }
758
+ // 检查频率限制
759
+ if (this.isFrequencyLimitExceeded(rule)) {
760
+ continue; // 跳过达到频率限制的规则
761
+ }
681
762
  return rule;
682
763
  }
683
764
  }
@@ -701,6 +782,10 @@ class ProxyServer {
701
782
  if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
702
783
  continue; // 跳过超限规则
703
784
  }
785
+ // 检查频率限制
786
+ if (this.isFrequencyLimitExceeded(rule)) {
787
+ continue; // 跳过达到频率限制的规则
788
+ }
704
789
  return rule;
705
790
  }
706
791
  // 3. 最后返回 default 规则
@@ -722,6 +807,10 @@ class ProxyServer {
722
807
  if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
723
808
  continue; // 跳过超限规则
724
809
  }
810
+ // 检查频率限制
811
+ if (this.isFrequencyLimitExceeded(rule)) {
812
+ continue; // 跳过达到频率限制的规则
813
+ }
725
814
  return rule;
726
815
  }
727
816
  return undefined;
@@ -772,6 +861,10 @@ class ProxyServer {
772
861
  return false;
773
862
  }
774
863
  }
864
+ // 检查频率限制
865
+ if (this.isFrequencyLimitExceeded(rule)) {
866
+ return false;
867
+ }
775
868
  return true; // 没有设置限制的规则总是可用
776
869
  });
777
870
  // 如果过滤后还有规则,使用过滤后的结果
@@ -781,6 +874,88 @@ class ProxyServer {
781
874
  }
782
875
  return candidates;
783
876
  }
877
+ /**
878
+ * 检查规则是否达到频率限制
879
+ * 如果设置了频率限制(frequencyLimit)和时间窗口(frequencyWindow),
880
+ * 则跟踪当前时间窗口内的请求数,超过限制则返回true
881
+ *
882
+ * frequencyWindow = 0 表示"同一时刻",计数器不会按时间窗口重置,
883
+ * 持续累积直到达到 frequencyLimit
884
+ */
885
+ isFrequencyLimitExceeded(rule) {
886
+ if (!rule.frequencyLimit || rule.frequencyLimit <= 0) {
887
+ return false; // 没有设置频率限制,不超过限制
888
+ }
889
+ // frequencyWindow 为 0 表示"同一时刻"(不按时间窗口重置)
890
+ const isZeroWindow = rule.frequencyWindow === 0;
891
+ if (!rule.frequencyWindow && !isZeroWindow) {
892
+ return false; // 没有设置时间窗口且不是0,不启用频率限制
893
+ }
894
+ const now = Date.now();
895
+ const existing = this.frequencyLimitTracker.get(rule.id);
896
+ if (!existing) {
897
+ // 首次请求,创建新记录
898
+ this.frequencyLimitTracker.set(rule.id, { count: 1, windowStart: now });
899
+ return false;
900
+ }
901
+ // 如果是零窗口(同一时刻),不按时间重置,持续累积
902
+ if (!isZeroWindow && rule.frequencyWindow) {
903
+ const windowMs = rule.frequencyWindow * 1000;
904
+ // 检查是否在当前时间窗口内
905
+ if (now - existing.windowStart >= windowMs) {
906
+ // 时间窗口已过,重置计数器
907
+ this.frequencyLimitTracker.set(rule.id, { count: 1, windowStart: now });
908
+ return false;
909
+ }
910
+ }
911
+ // 检查是否超过限制
912
+ if (existing.count >= rule.frequencyLimit) {
913
+ return true; // 超过频率限制
914
+ }
915
+ // 增加计数
916
+ existing.count++;
917
+ this.frequencyLimitTracker.set(rule.id, existing);
918
+ return false;
919
+ }
920
+ /**
921
+ * 记录请求(增加频率计数)
922
+ * 在请求成功处理后调用
923
+ * frequencyWindow = 0 表示"同一时刻",计数器不会按时间窗口重置
924
+ */
925
+ recordRequest(ruleId) {
926
+ const rule = this.getRuleById(ruleId);
927
+ if (!rule || !rule.frequencyLimit || rule.frequencyLimit <= 0) {
928
+ return;
929
+ }
930
+ // frequencyWindow 为 0 表示"同一时刻"
931
+ const isZeroWindow = rule.frequencyWindow === 0;
932
+ // 如果 frequencyWindow 既不是 0 也不是正数,则不记录
933
+ if (!isZeroWindow && !rule.frequencyWindow) {
934
+ return;
935
+ }
936
+ const now = Date.now();
937
+ const existing = this.frequencyLimitTracker.get(ruleId);
938
+ if (!existing) {
939
+ this.frequencyLimitTracker.set(ruleId, { count: 1, windowStart: now });
940
+ }
941
+ else if (isZeroWindow) {
942
+ // 零窗口:持续累积,不按时间重置
943
+ existing.count++;
944
+ this.frequencyLimitTracker.set(ruleId, existing);
945
+ }
946
+ else if (rule.frequencyWindow) {
947
+ const windowMs = rule.frequencyWindow * 1000;
948
+ if (now - existing.windowStart < windowMs) {
949
+ // 在时间窗口内,增加计数
950
+ existing.count++;
951
+ this.frequencyLimitTracker.set(ruleId, existing);
952
+ }
953
+ else {
954
+ // 时间窗口已过,重置
955
+ this.frequencyLimitTracker.set(ruleId, { count: 1, windowStart: now });
956
+ }
957
+ }
958
+ }
784
959
  determineContentType(req) {
785
960
  const body = req.body;
786
961
  if (!body)
@@ -1424,13 +1599,15 @@ class ProxyServer {
1424
1599
  // 默认:直接返回原始路径
1425
1600
  return originalPath;
1426
1601
  }
1427
- proxyRequest(req, res, route, rule, service) {
1602
+ proxyRequest(req, res, route, rule, service, options) {
1428
1603
  return __awaiter(this, void 0, void 0, function* () {
1429
- var _a, _b, _c, _d;
1604
+ var _a, _b, _c, _d, _e;
1430
1605
  res.locals.skipLog = true;
1431
1606
  const startTime = Date.now();
1432
1607
  const sourceType = (service.sourceType || 'openai-chat');
1433
1608
  const targetType = route.targetType;
1609
+ const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
1610
+ const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
1434
1611
  let requestBody = req.body || {};
1435
1612
  let usageForLog;
1436
1613
  let logged = false;
@@ -1595,7 +1772,7 @@ class ProxyServer {
1595
1772
  targetType,
1596
1773
  targetServiceId: service.id,
1597
1774
  targetServiceName: service.name,
1598
- targetModel: rule.targetModel,
1775
+ targetModel: rule.targetModel || requestModel,
1599
1776
  vendorId: service.vendorId,
1600
1777
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
1601
1778
  requestModel,
@@ -1643,6 +1820,8 @@ class ProxyServer {
1643
1820
  // 检查是否是重复请求(如网络重试)
1644
1821
  if (!this.isRequestProcessed(requestHash)) {
1645
1822
  this.dbManager.incrementRuleRequestCount(rule.id, 1);
1823
+ // 更新频率限制跟踪
1824
+ this.recordRequest(rule.id);
1646
1825
  // 获取更新后的规则数据并广播
1647
1826
  const updatedRule = this.dbManager.getRule(rule.id);
1648
1827
  if (updatedRule) {
@@ -1654,12 +1833,72 @@ class ProxyServer {
1654
1833
  this.cleanExpiredDedupeCache();
1655
1834
  }
1656
1835
  }
1836
+ // 定期清理过期的频率限制跟踪数据
1837
+ if (Math.random() < 0.01) { // 1%概率清理
1838
+ this.cleanExpiredFrequencyTrackers();
1839
+ }
1657
1840
  // 清理 MCP 临时图片文件
1658
1841
  if (tempImageFiles.length > 0) {
1659
1842
  (0, mcp_image_handler_1.cleanupTempImages)(tempImageFiles);
1660
1843
  console.log(`[MCP] Cleaned up ${tempImageFiles.length} temporary image files`);
1661
1844
  }
1662
1845
  });
1846
+ const handleUpstreamHttpError = (statusCode, responseData, responseHeaders, contentType) => __awaiter(this, void 0, void 0, function* () {
1847
+ var _a, _b;
1848
+ usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
1849
+ responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
1850
+ let errorDetail;
1851
+ if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
1852
+ errorDetail = responseData.error;
1853
+ }
1854
+ else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
1855
+ errorDetail = responseData.message;
1856
+ }
1857
+ else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
1858
+ errorDetail = JSON.stringify(responseData.error);
1859
+ }
1860
+ else {
1861
+ errorDetail = JSON.stringify(responseData);
1862
+ }
1863
+ const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
1864
+ const upstreamErrorMessage = `Upstream API returned ${statusCode}: ${errorDetail}${failoverHint}`;
1865
+ const vendors = this.dbManager.getVendors();
1866
+ const vendor = vendors.find(v => v.id === service.vendorId);
1867
+ yield this.dbManager.addErrorLog({
1868
+ timestamp: Date.now(),
1869
+ method: req.method,
1870
+ path: req.path,
1871
+ statusCode,
1872
+ errorMessage: upstreamErrorMessage,
1873
+ errorStack: undefined,
1874
+ requestHeaders: this.normalizeHeaders(req.headers),
1875
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1876
+ responseHeaders: responseHeadersForLog,
1877
+ responseBody: responseBodyForLog,
1878
+ ruleId: rule.id,
1879
+ targetType,
1880
+ targetServiceId: service.id,
1881
+ targetServiceName: service.name,
1882
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
1883
+ vendorId: service.vendorId,
1884
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
1885
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
1886
+ upstreamRequest: upstreamRequestForLog,
1887
+ responseTime: Date.now() - startTime,
1888
+ });
1889
+ if (failoverEnabled) {
1890
+ yield finalizeLog(statusCode, upstreamErrorMessage);
1891
+ throw this.createFailoverError(upstreamErrorMessage, statusCode);
1892
+ }
1893
+ this.copyResponseHeaders(responseHeaders, res);
1894
+ if (contentType.includes('application/json')) {
1895
+ res.status(statusCode).json(responseData);
1896
+ }
1897
+ else {
1898
+ res.status(statusCode).send(responseData);
1899
+ }
1900
+ yield finalizeLog(res.statusCode);
1901
+ });
1663
1902
  try {
1664
1903
  if (targetType === 'claude-code') {
1665
1904
  if (this.isClaudeSource(sourceType)) {
@@ -1781,6 +2020,17 @@ class ProxyServer {
1781
2020
  const responseHeaders = response.headers || {};
1782
2021
  const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
1783
2022
  const isEventStream = streamRequested && contentType.includes('text/event-stream');
2023
+ // 先处理 4xx/5xx:在故障切换模式下抛错,由上层继续切换下一服务
2024
+ if (response.status >= 400) {
2025
+ let errorResponseData = response.data;
2026
+ if (streamRequested && response.data && typeof response.data.on === 'function') {
2027
+ const raw = yield this.readStreamBody(response.data);
2028
+ errorResponseData = (_a = this.safeJsonParse(raw)) !== null && _a !== void 0 ? _a : raw;
2029
+ }
2030
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
2031
+ yield handleUpstreamHttpError(response.status, errorResponseData, responseHeaders, contentType);
2032
+ return;
2033
+ }
1784
2034
  if (isEventStream && response.data) {
1785
2035
  res.status(response.status);
1786
2036
  if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
@@ -1832,7 +2082,7 @@ class ProxyServer {
1832
2082
  console.error('[Proxy] Response stream error:', err);
1833
2083
  });
1834
2084
  (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1835
- var _a;
2085
+ var _a, _b;
1836
2086
  if (error) {
1837
2087
  console.error('[Proxy] Pipeline error for claude-code:', error);
1838
2088
  // 记录到错误日志 - 包含请求详情和实际转发信息
@@ -1855,10 +2105,10 @@ class ProxyServer {
1855
2105
  targetType,
1856
2106
  targetServiceId: service.id,
1857
2107
  targetServiceName: service.name,
1858
- targetModel: rule.targetModel,
2108
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
1859
2109
  vendorId: service.vendorId,
1860
2110
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
1861
- requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
2111
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
1862
2112
  responseTime: Date.now() - startTime,
1863
2113
  });
1864
2114
  }
@@ -1934,7 +2184,7 @@ class ProxyServer {
1934
2184
  console.error('[Proxy] Response stream error:', err);
1935
2185
  });
1936
2186
  (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1937
- var _a;
2187
+ var _a, _b;
1938
2188
  if (error) {
1939
2189
  console.error('[Proxy] Pipeline error for codex:', error);
1940
2190
  // 记录到错误日志 - 包含请求详情和实际转发信息
@@ -1957,10 +2207,10 @@ class ProxyServer {
1957
2207
  targetType,
1958
2208
  targetServiceId: service.id,
1959
2209
  targetServiceName: service.name,
1960
- targetModel: rule.targetModel,
2210
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
1961
2211
  vendorId: service.vendorId,
1962
2212
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
1963
- requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
2213
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
1964
2214
  responseTime: Date.now() - startTime,
1965
2215
  });
1966
2216
  }
@@ -2030,7 +2280,7 @@ class ProxyServer {
2030
2280
  console.error('[Proxy] Response stream error:', err);
2031
2281
  });
2032
2282
  (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
2033
- var _a;
2283
+ var _a, _b;
2034
2284
  if (error) {
2035
2285
  console.error('[Proxy] Pipeline error for gemini->claude-code:', error);
2036
2286
  try {
@@ -2050,10 +2300,10 @@ class ProxyServer {
2050
2300
  targetType,
2051
2301
  targetServiceId: service.id,
2052
2302
  targetServiceName: service.name,
2053
- targetModel: rule.targetModel,
2303
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
2054
2304
  vendorId: service.vendorId,
2055
2305
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2056
- requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
2306
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
2057
2307
  responseTime: Date.now() - startTime,
2058
2308
  });
2059
2309
  }
@@ -2126,7 +2376,7 @@ class ProxyServer {
2126
2376
  console.error('[Proxy] Response stream error:', err);
2127
2377
  });
2128
2378
  (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
2129
- var _a;
2379
+ var _a, _b;
2130
2380
  if (error) {
2131
2381
  console.error('[Proxy] Pipeline error for gemini->codex:', error);
2132
2382
  try {
@@ -2146,10 +2396,10 @@ class ProxyServer {
2146
2396
  targetType,
2147
2397
  targetServiceId: service.id,
2148
2398
  targetServiceName: service.name,
2149
- targetModel: rule.targetModel,
2399
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
2150
2400
  vendorId: service.vendorId,
2151
2401
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2152
- requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
2402
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
2153
2403
  responseTime: Date.now() - startTime,
2154
2404
  });
2155
2405
  }
@@ -2238,65 +2488,10 @@ class ProxyServer {
2238
2488
  let responseData = response.data;
2239
2489
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
2240
2490
  const raw = yield this.readStreamBody(response.data);
2241
- responseData = (_a = this.safeJsonParse(raw)) !== null && _a !== void 0 ? _a : raw;
2491
+ responseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
2242
2492
  }
2243
2493
  // 收集响应头
2244
2494
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
2245
- if (response.status >= 400) {
2246
- usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
2247
- // 记录错误响应体
2248
- responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
2249
- // 将 4xx/5xx 错误记录到错误日志
2250
- // 确保 errorDetail 总是字符串类型
2251
- let errorDetail;
2252
- if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
2253
- errorDetail = responseData.error;
2254
- }
2255
- else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
2256
- errorDetail = responseData.message;
2257
- }
2258
- else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
2259
- errorDetail = JSON.stringify(responseData.error);
2260
- }
2261
- else {
2262
- errorDetail = JSON.stringify(responseData);
2263
- }
2264
- // 获取供应商信息
2265
- const vendors = this.dbManager.getVendors();
2266
- const vendor = vendors.find(v => v.id === service.vendorId);
2267
- yield this.dbManager.addErrorLog({
2268
- timestamp: Date.now(),
2269
- method: req.method,
2270
- path: req.path,
2271
- statusCode: response.status,
2272
- errorMessage: `Upstream API returned ${response.status}: ${errorDetail}`,
2273
- errorStack: undefined,
2274
- requestHeaders: this.normalizeHeaders(req.headers),
2275
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
2276
- responseHeaders: responseHeadersForLog,
2277
- responseBody: responseBodyForLog,
2278
- // 添加请求详情和实际转发信息
2279
- ruleId: rule.id,
2280
- targetType,
2281
- targetServiceId: service.id,
2282
- targetServiceName: service.name,
2283
- targetModel: rule.targetModel,
2284
- vendorId: service.vendorId,
2285
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2286
- requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
2287
- upstreamRequest: upstreamRequestForLog,
2288
- responseTime: Date.now() - startTime,
2289
- });
2290
- this.copyResponseHeaders(responseHeaders, res);
2291
- if (contentType.includes('application/json')) {
2292
- res.status(response.status).json(responseData);
2293
- }
2294
- else {
2295
- res.status(response.status).send(responseData);
2296
- }
2297
- yield finalizeLog(res.statusCode);
2298
- return;
2299
- }
2300
2495
  if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
2301
2496
  const converted = (0, claude_openai_1.transformOpenAIChatResponseToClaude)(responseData);
2302
2497
  usageForLog = (0, claude_openai_1.extractTokenUsageFromOpenAIUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
@@ -2338,14 +2533,19 @@ class ProxyServer {
2338
2533
  yield finalizeLog(res.statusCode);
2339
2534
  }
2340
2535
  catch (error) {
2536
+ if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
2537
+ throw error;
2538
+ }
2341
2539
  console.error('Proxy error:', error);
2342
2540
  // 检测是否是 timeout 错误
2343
2541
  const isTimeout = error.code === 'ECONNABORTED' ||
2344
2542
  ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
2345
2543
  (error.errno && error.errno === 'ETIMEDOUT');
2346
- const errorMessage = isTimeout
2544
+ const baseErrorMessage = isTimeout
2347
2545
  ? 'Request timeout - the upstream API took too long to respond'
2348
2546
  : (error.message || 'Internal server error');
2547
+ const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
2548
+ const errorMessage = `${baseErrorMessage}${failoverHint}`;
2349
2549
  // 将错误记录到错误日志 - 包含请求详情和实际转发信息
2350
2550
  // 获取供应商信息
2351
2551
  const vendors = this.dbManager.getVendors();
@@ -2364,14 +2564,17 @@ class ProxyServer {
2364
2564
  targetType,
2365
2565
  targetServiceId: service.id,
2366
2566
  targetServiceName: service.name,
2367
- targetModel: rule.targetModel,
2567
+ targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
2368
2568
  vendorId: service.vendorId,
2369
2569
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2370
- requestModel: (_d = req.body) === null || _d === void 0 ? void 0 : _d.model,
2570
+ requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
2371
2571
  upstreamRequest: upstreamRequestForLog,
2372
2572
  responseTime: Date.now() - startTime,
2373
2573
  });
2374
2574
  yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
2575
+ if (failoverEnabled) {
2576
+ throw this.createFailoverError(errorMessage, isTimeout ? 504 : 500, error);
2577
+ }
2375
2578
  // 根据请求类型返回适当格式的错误响应
2376
2579
  const streamRequested = this.isStreamRequested(req, req.body || {});
2377
2580
  if (route.targetType === 'claude-code') {