aicodeswitch 5.2.1 → 5.2.3

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.
@@ -122,10 +122,14 @@ function extractMessageContent(content) {
122
122
  /**
123
123
  * 检测消息是否为 Claude Code 的 compact 命令请求。
124
124
  *
125
+ * 采用两级检测策略:
126
+ * - 严格匹配:四个关键词全部命中(适配已知格式)
127
+ * - 宽松匹配:"TEXT ONLY" + "<summary>" 组合(覆盖 prompt 格式变化)
128
+ *
125
129
  * Compact 命令触发时,Claude Code 会在 messages 末尾插入一条特殊指令:
126
130
  * - role 为 "user"
127
131
  * - content 为数组,包含一个 text 块
128
- * - text 内容以 "CRITICAL: Respond with TEXT ONLY" 开头
132
+ * - text 内容包含 "CRITICAL: Respond with TEXT ONLY" 等标识
129
133
  * - 包含对话摘要生成指令,要求输出 <analysis> 和 <summary> 结构
130
134
  */
131
135
  function isClaudeCompactRequest(message) {
@@ -139,10 +143,21 @@ function isClaudeCompactRequest(message) {
139
143
  for (const block of content) {
140
144
  if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && typeof block.text === 'string') {
141
145
  const text = block.text;
142
- if (text.includes('CRITICAL: Respond with TEXT ONLY') &&
143
- text.includes('create a detailed summary of the conversation') &&
144
- text.includes('<analysis>') &&
145
- text.includes('<summary>')) {
146
+ const hasTextOnly = text.includes('TEXT ONLY');
147
+ const hasSummary = text.includes('<summary>');
148
+ const hasCritical = text.includes('CRITICAL: Respond with TEXT ONLY');
149
+ const hasDetailedSummary = text.includes('create a detailed summary of the conversation');
150
+ const hasAnalysis = text.includes('<analysis>');
151
+ // 严格匹配:Claude Code 标准格式(四个关键词)
152
+ if (hasCritical && hasDetailedSummary && hasAnalysis && hasSummary) {
153
+ console.log('[COMPACT] Strict match: all 4 markers found');
154
+ return true;
155
+ }
156
+ // 宽松匹配:覆盖 Claude Code prompt 格式变化
157
+ // "TEXT ONLY" 是紧凑指令的核心标识,<summary> 是输出格式标签
158
+ // 两者组合在非 compact 请求中几乎不可能同时出现
159
+ if (hasTextOnly && hasSummary) {
160
+ console.log(`[COMPACT] Loose match: CRITICAL=${hasCritical}, detailed_summary=${hasDetailedSummary}, <analysis>=${hasAnalysis}`);
146
161
  return true;
147
162
  }
148
163
  }
@@ -150,13 +165,22 @@ function isClaudeCompactRequest(message) {
150
165
  return false;
151
166
  }
152
167
  /**
153
- * 检测消息列表中的最后一条消息是否为 Claude Code compact 请求。
168
+ * 检测消息列表中是否包含 Claude Code compact 请求。
169
+ *
170
+ * 从最后一条消息开始往前搜索,最多检查 3 条,
171
+ * 处理 compact 指令后面可能跟了 assistant 占位消息的边缘情况。
154
172
  */
155
173
  function isLastClaudeMessageCompact(messages) {
156
174
  if (!Array.isArray(messages) || messages.length === 0) {
157
175
  return false;
158
176
  }
159
- return isClaudeCompactRequest(messages[messages.length - 1]);
177
+ const checkCount = Math.min(messages.length, 3);
178
+ for (let i = messages.length - 1; i >= messages.length - checkCount; i--) {
179
+ if (isClaudeCompactRequest(messages[i])) {
180
+ return true;
181
+ }
182
+ }
183
+ return false;
160
184
  }
161
185
  /**
162
186
  * 检测请求是否为 Codex 的 compact(压缩)请求。
@@ -1045,6 +1045,12 @@ const listInstalledSkills = () => {
1045
1045
  const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
1046
1046
  updateProxyConfig(dbManager.getConfig());
1047
1047
  app.get('/health', (_req, res) => res.json({ status: 'ok' }));
1048
+ // 数据就绪验证端点(供 Tauri 启动阶段确认后端完全可用)
1049
+ app.get('/api/ready', (_req, res) => {
1050
+ const vendors = dbManager.getVendors();
1051
+ const routes = dbManager.getRoutes();
1052
+ res.json({ ready: true, vendorsCount: vendors.length, routesCount: routes.length });
1053
+ });
1048
1054
  // 局域网访问控制中间件:当 enableLanDiscovery 关闭时,仅允许本机访问 /api/* 路由
1049
1055
  app.use('/api', (req, res, next) => {
1050
1056
  const config = dbManager.getConfig();
@@ -1385,6 +1385,161 @@ class ProxyServer {
1385
1385
  isResponseCommitted(res) {
1386
1386
  return res.headersSent || this.isDownstreamClosed(res);
1387
1387
  }
1388
+ /**
1389
+ * SSE 流预检:在提交响应头之前读取上游流的第一个有意义的 SSE 事件,
1390
+ * 判断上游是否健康。若首事件为错误(response.failed / error),则不提交响应头,
1391
+ * 允许外层故障切换循环尝试下一个候选服务。
1392
+ *
1393
+ * @returns healthy=true 时携带 bufferedRaw(预检期间读取的原始字节),
1394
+ * healthy=false 时携带 failureInfo 用于构建错误信息。
1395
+ */
1396
+ preflightStream(upstreamStream, options = {}) {
1397
+ const { timeoutMs = 5000 } = options;
1398
+ return new Promise((resolve) => {
1399
+ const chunks = [];
1400
+ const tempParser = new streaming_1.SSEParserTransform();
1401
+ const events = [];
1402
+ let settled = false;
1403
+ let timer;
1404
+ const finish = (result) => {
1405
+ var _a;
1406
+ if (settled)
1407
+ return;
1408
+ settled = true;
1409
+ if (timer)
1410
+ clearTimeout(timer);
1411
+ // 停止从上游流读取,但不 destroy(后续可能还需要用)
1412
+ (_a = upstreamStream.removeAllListeners) === null || _a === void 0 ? void 0 : _a.call(upstreamStream);
1413
+ resolve(result);
1414
+ };
1415
+ // 超时保护
1416
+ timer = setTimeout(() => {
1417
+ finish({
1418
+ healthy: false,
1419
+ failureInfo: { statusCode: 504, errorMessage: 'Stream preflight timed out waiting for first event' },
1420
+ bufferedRaw: Buffer.concat(chunks),
1421
+ });
1422
+ }, timeoutMs);
1423
+ // 将原始数据喂给临时 parser 解析 SSE 事件
1424
+ const onData = (chunk) => {
1425
+ if (settled)
1426
+ return;
1427
+ chunks.push(chunk);
1428
+ // 手动喂给 parser
1429
+ tempParser.write(chunk);
1430
+ drainParserEvents();
1431
+ };
1432
+ const drainParserEvents = () => {
1433
+ var _a, _b;
1434
+ // 从 tempParser 的 readable 侧读出解析后的事件
1435
+ let event;
1436
+ while (null !== (event = tempParser.read())) {
1437
+ if (settled)
1438
+ return;
1439
+ events.push(event);
1440
+ // 跳过无意义的空事件和 done
1441
+ const eventType = (_a = event.event) === null || _a === void 0 ? void 0 : _a.trim();
1442
+ const eventData = event.data;
1443
+ if (!eventType && eventData && typeof eventData === 'object' && eventData.type === 'done')
1444
+ continue;
1445
+ if (!eventType && !eventData)
1446
+ continue;
1447
+ // 检查是否为错误事件
1448
+ if (eventType === 'response.failed' || eventType === 'error') {
1449
+ const parsed = event.data ? this.safeJsonParse(event.data) : null;
1450
+ 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;
1451
+ const errorCode = errorObj === null || errorObj === void 0 ? void 0 : errorObj.code;
1452
+ const errorMessage = (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message)
1453
+ || (parsed === null || parsed === void 0 ? void 0 : parsed.message)
1454
+ || `Upstream stream returned ${eventType}`;
1455
+ const statusCode = errorCode === 'server_is_overloaded' ? 503 : 502;
1456
+ finish({
1457
+ healthy: false,
1458
+ failureInfo: {
1459
+ statusCode,
1460
+ errorMessage: `Upstream stream returned ${eventType}: ${errorMessage}`,
1461
+ },
1462
+ bufferedRaw: Buffer.concat(chunks),
1463
+ errorData: event.data,
1464
+ });
1465
+ return;
1466
+ }
1467
+ // 首个有意义的正常事件 → 健康通过
1468
+ finish({
1469
+ healthy: true,
1470
+ bufferedRaw: Buffer.concat(chunks),
1471
+ });
1472
+ return;
1473
+ }
1474
+ };
1475
+ const onEnd = () => {
1476
+ if (settled)
1477
+ return;
1478
+ // 流结束了但没读到有意义的 event
1479
+ if (events.length === 0 && chunks.length === 0) {
1480
+ finish({
1481
+ healthy: false,
1482
+ failureInfo: { statusCode: 502, errorMessage: 'Upstream stream ended before sending any data' },
1483
+ bufferedRaw: Buffer.concat(chunks),
1484
+ });
1485
+ }
1486
+ else {
1487
+ // 读到了一些数据但没有明确的错误 → 视为健康
1488
+ finish({
1489
+ healthy: true,
1490
+ bufferedRaw: Buffer.concat(chunks),
1491
+ });
1492
+ }
1493
+ };
1494
+ const onError = (err) => {
1495
+ if (settled)
1496
+ return;
1497
+ finish({
1498
+ healthy: false,
1499
+ failureInfo: { statusCode: 502, errorMessage: `Upstream stream error during preflight: ${err.message}` },
1500
+ bufferedRaw: Buffer.concat(chunks),
1501
+ });
1502
+ };
1503
+ upstreamStream.on('data', onData);
1504
+ upstreamStream.once('end', onEnd);
1505
+ upstreamStream.once('error', onError);
1506
+ // 暂停自动读取 — 我们只需要第一个事件
1507
+ // 注意:不能 pause,因为 axios stream 需要 flow mode 才能获取数据
1508
+ });
1509
+ }
1510
+ /**
1511
+ * 创建一个组合流:先输出 bufferedRaw 中的原始字节,再透传上游流的剩余数据。
1512
+ * 用于预检通过后无缝衔接后续的 SSE 管道。
1513
+ */
1514
+ createPreflightCombinedStream(upstreamStream, bufferedRaw) {
1515
+ let pushed = false;
1516
+ const combined = new stream_1.Readable({
1517
+ read() {
1518
+ var _a, _b;
1519
+ if (!pushed) {
1520
+ pushed = true;
1521
+ if (bufferedRaw.length > 0) {
1522
+ this.push(bufferedRaw);
1523
+ }
1524
+ // 将上游流 pipe 到 combined
1525
+ upstreamStream.on('data', (chunk) => {
1526
+ if (!this.push(chunk)) {
1527
+ upstreamStream.pause();
1528
+ }
1529
+ });
1530
+ upstreamStream.once('end', () => {
1531
+ this.push(null);
1532
+ });
1533
+ upstreamStream.once('error', (err) => {
1534
+ this.destroy(err);
1535
+ });
1536
+ // 如果上游已经暂停了(预检时消费了一些数据),恢复它
1537
+ (_b = (_a = upstreamStream).resume) === null || _b === void 0 ? void 0 : _b.call(_a);
1538
+ }
1539
+ },
1540
+ });
1541
+ return combined;
1542
+ }
1388
1543
  isClientDisconnectError(error, res) {
1389
1544
  const code = error === null || error === void 0 ? void 0 : error.code;
1390
1545
  if (code === 'CLIENT_DISCONNECTED' || code === 'ERR_CANCELED') {
@@ -1624,6 +1779,28 @@ class ProxyServer {
1624
1779
  return rule;
1625
1780
  }
1626
1781
  }
1782
+ // compact 规则同样拥有最高优先级,确保压缩请求不被其他规则覆盖
1783
+ if (contentType === 'compact') {
1784
+ const compactRules = enabledRules.filter(rule => rule.contentType === 'compact');
1785
+ for (const rule of compactRules) {
1786
+ const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, rule.contentType);
1787
+ if (isBlacklisted) {
1788
+ continue;
1789
+ }
1790
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
1791
+ this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
1792
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
1793
+ continue;
1794
+ }
1795
+ if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
1796
+ continue;
1797
+ }
1798
+ if (this.isFrequencyLimitExceeded(rule)) {
1799
+ continue;
1800
+ }
1801
+ return rule;
1802
+ }
1803
+ }
1627
1804
  // 1. 查找其他内容类型的规则
1628
1805
  const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
1629
1806
  // 过滤黑名单和token限制
@@ -3193,7 +3370,7 @@ class ProxyServer {
3193
3370
  }
3194
3371
  proxyRequest(req, res, route, rule, service, options) {
3195
3372
  return __awaiter(this, void 0, void 0, function* () {
3196
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
3373
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
3197
3374
  res.locals.skipLog = true;
3198
3375
  const startTime = Date.now();
3199
3376
  const rawSourceType = service.sourceType || 'openai-chat';
@@ -3815,6 +3992,64 @@ class ProxyServer {
3815
3992
  return;
3816
3993
  }
3817
3994
  if (isEventStream && response.data) {
3995
+ // ── SSE 预检:在提交响应头之前,先读取第一个 SSE 事件以检测上游错误 ──
3996
+ // 这使得故障切换能在流式场景下生效(首事件为 error 时不提交响应头,允许切换到下一个服务)
3997
+ const preflightResult = yield this.preflightStream(response.data, { timeoutMs: 5000 });
3998
+ if (!preflightResult.healthy) {
3999
+ // 预检失败(首事件是错误 / 超时 / 流提前关闭)
4000
+ const failureInfo = preflightResult.failureInfo;
4001
+ console.warn(`[Proxy] Stream preflight failed: ${(failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'unknown'}`);
4002
+ // 尝试读取完整错误体用于日志
4003
+ let errorBody = preflightResult.errorData;
4004
+ if (!errorBody && preflightResult.bufferedRaw.length > 0) {
4005
+ errorBody = this.safeJsonParse(preflightResult.bufferedRaw.toString('utf8'));
4006
+ }
4007
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4008
+ const errorMsg = (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'Stream preflight detected upstream error';
4009
+ // 记录错误日志
4010
+ try {
4011
+ yield this.dbManager.addErrorLog({
4012
+ timestamp: Date.now(),
4013
+ method: req.method,
4014
+ path: req.path,
4015
+ statusCode: (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502,
4016
+ errorMessage: errorMsg,
4017
+ requestHeaders: this.normalizeHeaders(req.headers),
4018
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
4019
+ upstreamRequest: upstreamRequestForLog,
4020
+ responseHeaders: responseHeadersForLog,
4021
+ responseBody: preflightResult.bufferedRaw.toString('utf8'),
4022
+ ruleId: rule.id,
4023
+ targetType,
4024
+ targetServiceId: service.id,
4025
+ targetServiceName: service.name,
4026
+ targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
4027
+ vendorId: service.vendorId,
4028
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4029
+ requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
4030
+ responseTime: Date.now() - startTime,
4031
+ });
4032
+ }
4033
+ catch (logError) {
4034
+ console.error('[Proxy] Failed to log preflight error:', logError);
4035
+ }
4036
+ yield finalizeLog((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502, errorMsg);
4037
+ // 销毁上游流
4038
+ if (typeof response.data.destroy === 'function') {
4039
+ response.data.destroy();
4040
+ }
4041
+ // 响应头未提交 → 可以触发故障切换
4042
+ if (failoverEnabled) {
4043
+ throw this.createFailoverError(errorMsg, (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502);
4044
+ }
4045
+ // 非 failover 模式:直接返回错误给客户端
4046
+ res.status((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502).json({
4047
+ error: { message: errorMsg, type: 'upstream_error' },
4048
+ });
4049
+ return;
4050
+ }
4051
+ // ── 预检通过:提交响应头,使用组合流继续管道传输 ──
4052
+ const streamSource = this.createPreflightCombinedStream(response.data, preflightResult.bufferedRaw);
3818
4053
  res.status(response.status);
3819
4054
  // 默认stream处理(无转换)
3820
4055
  const parser = new streaming_1.SSEParserTransform();
@@ -3827,7 +4062,7 @@ class ProxyServer {
3827
4062
  ? new ClaudeCompactResponseSanitizer()
3828
4063
  : null;
3829
4064
  // 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
3830
- const originalModel = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
4065
+ const originalModel = (_f = req.body) === null || _f === void 0 ? void 0 : _f.model;
3831
4066
  const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
3832
4067
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3833
4068
  // 使用 transformSSEToTool 方法选择转换器
@@ -3864,7 +4099,7 @@ class ProxyServer {
3864
4099
  ensureResponseWritable();
3865
4100
  return yield new Promise((resolve, reject) => {
3866
4101
  if (converter) {
3867
- const streamStages = [response.data, parser, eventCollector, converter];
4102
+ const streamStages = [streamSource, parser, eventCollector, converter];
3868
4103
  if (compactResponseSanitizer) {
3869
4104
  streamStages.push(compactResponseSanitizer);
3870
4105
  }
@@ -3882,7 +4117,7 @@ class ProxyServer {
3882
4117
  });
3883
4118
  return;
3884
4119
  }
3885
- const streamStages = [response.data, parser, eventCollector];
4120
+ const streamStages = [streamSource, parser, eventCollector];
3886
4121
  if (compactResponseSanitizer) {
3887
4122
  streamStages.push(compactResponseSanitizer);
3888
4123
  }
@@ -3927,10 +4162,10 @@ class ProxyServer {
3927
4162
  targetType,
3928
4163
  targetServiceId: service.id,
3929
4164
  targetServiceName: service.name,
3930
- targetModel: rule.targetModel || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
4165
+ targetModel: rule.targetModel || ((_g = req.body) === null || _g === void 0 ? void 0 : _g.model),
3931
4166
  vendorId: service.vendorId,
3932
4167
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3933
- requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
4168
+ requestModel: (_h = req.body) === null || _h === void 0 ? void 0 : _h.model,
3934
4169
  responseTime: Date.now() - startTime,
3935
4170
  });
3936
4171
  }
@@ -3974,10 +4209,10 @@ class ProxyServer {
3974
4209
  targetType,
3975
4210
  targetServiceId: service.id,
3976
4211
  targetServiceName: service.name,
3977
- targetModel: rule.targetModel || ((_g = req.body) === null || _g === void 0 ? void 0 : _g.model),
4212
+ targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
3978
4213
  vendorId: service.vendorId,
3979
4214
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3980
- requestModel: (_h = req.body) === null || _h === void 0 ? void 0 : _h.model,
4215
+ requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
3981
4216
  responseTime: Date.now() - startTime,
3982
4217
  });
3983
4218
  }
@@ -3996,7 +4231,7 @@ class ProxyServer {
3996
4231
  let responseData = response.data;
3997
4232
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
3998
4233
  const raw = yield this.readStreamBody(response.data);
3999
- responseData = (_j = this.safeJsonParse(raw)) !== null && _j !== void 0 ? _j : raw;
4234
+ responseData = (_l = this.safeJsonParse(raw)) !== null && _l !== void 0 ? _l : raw;
4000
4235
  }
4001
4236
  // 收集响应头
4002
4237
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
@@ -4018,7 +4253,7 @@ class ProxyServer {
4018
4253
  usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
4019
4254
  console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
4020
4255
  // 回写 model 字段:将上游返回的 model 改写为客户端请求时的原始模型名
4021
- const originalModel = (_k = req.body) === null || _k === void 0 ? void 0 : _k.model;
4256
+ const originalModel = (_m = req.body) === null || _m === void 0 ? void 0 : _m.model;
4022
4257
  (0, model_rewrite_transform_1.rewriteResponseModel)(normalizedConverted, originalModel);
4023
4258
  (0, model_rewrite_transform_1.rewriteResponseModel)(responseData, originalModel);
4024
4259
  this.copyResponseHeaders(responseHeaders, res);
@@ -4083,10 +4318,10 @@ class ProxyServer {
4083
4318
  targetType,
4084
4319
  targetServiceId: service.id,
4085
4320
  targetServiceName: service.name,
4086
- targetModel: rule.targetModel || ((_l = req.body) === null || _l === void 0 ? void 0 : _l.model),
4321
+ targetModel: rule.targetModel || ((_o = req.body) === null || _o === void 0 ? void 0 : _o.model),
4087
4322
  vendorId: service.vendorId,
4088
4323
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4089
- requestModel: (_m = req.body) === null || _m === void 0 ? void 0 : _m.model,
4324
+ requestModel: (_p = req.body) === null || _p === void 0 ? void 0 : _p.model,
4090
4325
  upstreamRequest: upstreamRequestForLog,
4091
4326
  responseTime: Date.now() - startTime,
4092
4327
  });
@@ -4101,7 +4336,7 @@ class ProxyServer {
4101
4336
  console.error('Proxy error:', error);
4102
4337
  // 检测是否是 timeout 错误
4103
4338
  const isTimeout = error.code === 'ECONNABORTED' ||
4104
- ((_o = error.message) === null || _o === void 0 ? void 0 : _o.toLowerCase().includes('timeout')) ||
4339
+ ((_q = error.message) === null || _q === void 0 ? void 0 : _q.toLowerCase().includes('timeout')) ||
4105
4340
  (error.errno && error.errno === 'ETIMEDOUT');
4106
4341
  const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
4107
4342
  const baseErrorMessage = isTimeout
@@ -4127,10 +4362,10 @@ class ProxyServer {
4127
4362
  targetType,
4128
4363
  targetServiceId: service.id,
4129
4364
  targetServiceName: service.name,
4130
- targetModel: rule.targetModel || ((_p = req.body) === null || _p === void 0 ? void 0 : _p.model),
4365
+ targetModel: rule.targetModel || ((_r = req.body) === null || _r === void 0 ? void 0 : _r.model),
4131
4366
  vendorId: service.vendorId,
4132
4367
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4133
- requestModel: (_q = req.body) === null || _q === void 0 ? void 0 : _q.model,
4368
+ requestModel: (_s = req.body) === null || _s === void 0 ? void 0 : _s.model,
4134
4369
  upstreamRequest: upstreamRequestForLog,
4135
4370
  responseHeaders: responseHeadersForLog,
4136
4371
  responseTime: Date.now() - startTime,
@@ -4605,6 +4840,31 @@ class ProxyServer {
4605
4840
  return;
4606
4841
  }
4607
4842
  if (isEventStream && response.data) {
4843
+ // ── SSE 预检:在提交响应头之前,先读取第一个 SSE 事件以检测上游错误 ──
4844
+ const preflightResult = yield this.preflightStream(response.data, { timeoutMs: 5000 });
4845
+ if (!preflightResult.healthy) {
4846
+ const failureInfo = preflightResult.failureInfo;
4847
+ console.warn(`[ApiPathProxy] Stream preflight failed: ${(failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'unknown'}`);
4848
+ let errorBody = preflightResult.errorData;
4849
+ if (!errorBody && preflightResult.bufferedRaw.length > 0) {
4850
+ errorBody = this.safeJsonParse(preflightResult.bufferedRaw.toString('utf8'));
4851
+ }
4852
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4853
+ const errorMsg = (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'Stream preflight detected upstream error';
4854
+ yield finalizeLog((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502, errorMsg);
4855
+ if (typeof response.data.destroy === 'function') {
4856
+ response.data.destroy();
4857
+ }
4858
+ if (failoverEnabled) {
4859
+ throw this.createFailoverError(errorMsg, (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502);
4860
+ }
4861
+ res.status((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502).json({
4862
+ error: { message: errorMsg, type: 'upstream_error' },
4863
+ });
4864
+ return;
4865
+ }
4866
+ // ── 预检通过:使用组合流继续管道传输 ──
4867
+ const streamSource = this.createPreflightCombinedStream(response.data, preflightResult.bufferedRaw);
4608
4868
  // Stream pipeline
4609
4869
  const parser = new streaming_1.SSEParserTransform();
4610
4870
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
@@ -4646,7 +4906,7 @@ class ProxyServer {
4646
4906
  return stages;
4647
4907
  };
4648
4908
  if (converter) {
4649
- const stages = buildStages(response.data, parser, eventCollector, converter);
4909
+ const stages = buildStages(streamSource, parser, eventCollector, converter);
4650
4910
  stream_1.pipeline(...stages, (error) => {
4651
4911
  if (error) {
4652
4912
  reject(error);
@@ -4656,7 +4916,7 @@ class ProxyServer {
4656
4916
  });
4657
4917
  }
4658
4918
  else {
4659
- const stages = buildStages(response.data, parser, eventCollector);
4919
+ const stages = buildStages(streamSource, parser, eventCollector);
4660
4920
  stream_1.pipeline(...stages, (error) => {
4661
4921
  if (error) {
4662
4922
  reject(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "5.2.1",
3
+ "version": "5.2.3",
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",
@@ -46,6 +46,7 @@
46
46
  "prepublishOnly": "npm run build",
47
47
  "release": "standard-version --no-changelog",
48
48
  "tauri:dev": "tauri dev",
49
+ "tauri:start": "yarn build && node tauri/prepare-resources.js && cd tauri && cargo run",
49
50
  "tauri:build": "tauri build && node tauri/move-bundle.js",
50
51
  "tauri:icon": "tauri icon src/ui/assets/logo.png"
51
52
  },