aicodeswitch 3.0.19 → 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.
@@ -158,7 +158,8 @@ class ProxyServer {
158
158
  }
159
159
  // 尝试每个规则,直到成功或全部失败
160
160
  let lastError = null;
161
- for (const rule of allRules) {
161
+ for (let index = 0; index < allRules.length; index++) {
162
+ const rule = allRules[index];
162
163
  const service = this.getServiceById(rule.targetServiceId);
163
164
  if (!service)
164
165
  continue;
@@ -169,8 +170,12 @@ class ProxyServer {
169
170
  continue;
170
171
  }
171
172
  try {
173
+ const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
172
174
  // 尝试代理请求
173
- yield this.proxyRequest(req, res, route, rule, service);
175
+ yield this.proxyRequest(req, res, route, rule, service, {
176
+ failoverEnabled: true,
177
+ forwardedToServiceName: nextServiceName,
178
+ });
174
179
  return; // 成功,直接返回
175
180
  }
176
181
  catch (error) {
@@ -339,7 +344,8 @@ class ProxyServer {
339
344
  }
340
345
  // 尝试每个规则,直到成功或全部失败
341
346
  let lastError = null;
342
- for (const rule of allRules) {
347
+ for (let index = 0; index < allRules.length; index++) {
348
+ const rule = allRules[index];
343
349
  const service = this.getServiceById(rule.targetServiceId);
344
350
  if (!service)
345
351
  continue;
@@ -350,8 +356,12 @@ class ProxyServer {
350
356
  continue;
351
357
  }
352
358
  try {
359
+ const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
353
360
  // 尝试代理请求
354
- yield this.proxyRequest(req, res, route, rule, service);
361
+ yield this.proxyRequest(req, res, route, rule, service, {
362
+ failoverEnabled: true,
363
+ forwardedToServiceName: nextServiceName,
364
+ });
355
365
  return; // 成功,直接返回
356
366
  }
357
367
  catch (error) {
@@ -497,6 +507,36 @@ class ProxyServer {
497
507
  const activeRoutes = this.getActiveRoutes();
498
508
  return activeRoutes.find(route => route.targetType === targetType && route.isActive);
499
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
+ }
500
540
  /**
501
541
  * 计算请求内容的哈希值,用于去重
502
542
  * 基于请求的关键字段生成唯一标识
@@ -1559,13 +1599,15 @@ class ProxyServer {
1559
1599
  // 默认:直接返回原始路径
1560
1600
  return originalPath;
1561
1601
  }
1562
- proxyRequest(req, res, route, rule, service) {
1602
+ proxyRequest(req, res, route, rule, service, options) {
1563
1603
  return __awaiter(this, void 0, void 0, function* () {
1564
- var _a, _b, _c, _d, _e, _f;
1604
+ var _a, _b, _c, _d, _e;
1565
1605
  res.locals.skipLog = true;
1566
1606
  const startTime = Date.now();
1567
1607
  const sourceType = (service.sourceType || 'openai-chat');
1568
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;
1569
1611
  let requestBody = req.body || {};
1570
1612
  let usageForLog;
1571
1613
  let logged = false;
@@ -1801,6 +1843,62 @@ class ProxyServer {
1801
1843
  console.log(`[MCP] Cleaned up ${tempImageFiles.length} temporary image files`);
1802
1844
  }
1803
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
+ });
1804
1902
  try {
1805
1903
  if (targetType === 'claude-code') {
1806
1904
  if (this.isClaudeSource(sourceType)) {
@@ -1922,6 +2020,17 @@ class ProxyServer {
1922
2020
  const responseHeaders = response.headers || {};
1923
2021
  const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
1924
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
+ }
1925
2034
  if (isEventStream && response.data) {
1926
2035
  res.status(response.status);
1927
2036
  if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
@@ -2379,65 +2488,10 @@ class ProxyServer {
2379
2488
  let responseData = response.data;
2380
2489
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
2381
2490
  const raw = yield this.readStreamBody(response.data);
2382
- responseData = (_a = this.safeJsonParse(raw)) !== null && _a !== void 0 ? _a : raw;
2491
+ responseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
2383
2492
  }
2384
2493
  // 收集响应头
2385
2494
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
2386
- if (response.status >= 400) {
2387
- usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
2388
- // 记录错误响应体
2389
- responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
2390
- // 将 4xx/5xx 错误记录到错误日志
2391
- // 确保 errorDetail 总是字符串类型
2392
- let errorDetail;
2393
- if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
2394
- errorDetail = responseData.error;
2395
- }
2396
- else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
2397
- errorDetail = responseData.message;
2398
- }
2399
- else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
2400
- errorDetail = JSON.stringify(responseData.error);
2401
- }
2402
- else {
2403
- errorDetail = JSON.stringify(responseData);
2404
- }
2405
- // 获取供应商信息
2406
- const vendors = this.dbManager.getVendors();
2407
- const vendor = vendors.find(v => v.id === service.vendorId);
2408
- yield this.dbManager.addErrorLog({
2409
- timestamp: Date.now(),
2410
- method: req.method,
2411
- path: req.path,
2412
- statusCode: response.status,
2413
- errorMessage: `Upstream API returned ${response.status}: ${errorDetail}`,
2414
- errorStack: undefined,
2415
- requestHeaders: this.normalizeHeaders(req.headers),
2416
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
2417
- responseHeaders: responseHeadersForLog,
2418
- responseBody: responseBodyForLog,
2419
- // 添加请求详情和实际转发信息
2420
- ruleId: rule.id,
2421
- targetType,
2422
- targetServiceId: service.id,
2423
- targetServiceName: service.name,
2424
- targetModel: rule.targetModel || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.model),
2425
- vendorId: service.vendorId,
2426
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2427
- requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
2428
- upstreamRequest: upstreamRequestForLog,
2429
- responseTime: Date.now() - startTime,
2430
- });
2431
- this.copyResponseHeaders(responseHeaders, res);
2432
- if (contentType.includes('application/json')) {
2433
- res.status(response.status).json(responseData);
2434
- }
2435
- else {
2436
- res.status(response.status).send(responseData);
2437
- }
2438
- yield finalizeLog(res.statusCode);
2439
- return;
2440
- }
2441
2495
  if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
2442
2496
  const converted = (0, claude_openai_1.transformOpenAIChatResponseToClaude)(responseData);
2443
2497
  usageForLog = (0, claude_openai_1.extractTokenUsageFromOpenAIUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
@@ -2479,14 +2533,19 @@ class ProxyServer {
2479
2533
  yield finalizeLog(res.statusCode);
2480
2534
  }
2481
2535
  catch (error) {
2536
+ if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
2537
+ throw error;
2538
+ }
2482
2539
  console.error('Proxy error:', error);
2483
2540
  // 检测是否是 timeout 错误
2484
2541
  const isTimeout = error.code === 'ECONNABORTED' ||
2485
- ((_d = error.message) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes('timeout')) ||
2542
+ ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
2486
2543
  (error.errno && error.errno === 'ETIMEDOUT');
2487
- const errorMessage = isTimeout
2544
+ const baseErrorMessage = isTimeout
2488
2545
  ? 'Request timeout - the upstream API took too long to respond'
2489
2546
  : (error.message || 'Internal server error');
2547
+ const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
2548
+ const errorMessage = `${baseErrorMessage}${failoverHint}`;
2490
2549
  // 将错误记录到错误日志 - 包含请求详情和实际转发信息
2491
2550
  // 获取供应商信息
2492
2551
  const vendors = this.dbManager.getVendors();
@@ -2505,14 +2564,17 @@ class ProxyServer {
2505
2564
  targetType,
2506
2565
  targetServiceId: service.id,
2507
2566
  targetServiceName: service.name,
2508
- targetModel: rule.targetModel || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
2567
+ targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
2509
2568
  vendorId: service.vendorId,
2510
2569
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2511
- requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
2570
+ requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
2512
2571
  upstreamRequest: upstreamRequestForLog,
2513
2572
  responseTime: Date.now() - startTime,
2514
2573
  });
2515
2574
  yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
2575
+ if (failoverEnabled) {
2576
+ throw this.createFailoverError(errorMessage, isTimeout ? 504 : 500, error);
2577
+ }
2516
2578
  // 根据请求类型返回适当格式的错误响应
2517
2579
  const streamRequested = this.isStreamRequested(req, req.body || {});
2518
2580
  if (route.targetType === 'claude-code') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "3.0.19",
3
+ "version": "3.0.20",
4
4
  "description": "A tool to help you manage AI programming tools to access large language models locally. It allows your Claude Code, Codex and other tools to no longer be limited to official models.",
5
5
  "author": "tangshuang",
6
6
  "license": "GPL-3.0",