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 +4 -0
- package/dist/server/proxy-server.js +281 -78
- package/dist/ui/assets/index-BCdyCT1c.js +510 -0
- package/dist/ui/assets/index-C7G0whng.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-Bbt481dy.css +0 -1
- package/dist/ui/assets/index-CcN9H5O_.js +0 -506
package/README.md
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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 = (
|
|
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
|
|
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: (
|
|
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') {
|