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.
- package/dist/server/conversions/compact.js +31 -7
- package/dist/server/main.js +6 -0
- package/dist/server/proxy-server.js +277 -17
- package/package.json +2 -1
|
@@ -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
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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(压缩)请求。
|
package/dist/server/main.js
CHANGED
|
@@ -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 = (
|
|
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 = [
|
|
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 = [
|
|
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 || ((
|
|
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: (
|
|
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 || ((
|
|
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: (
|
|
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 = (
|
|
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 = (
|
|
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 || ((
|
|
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: (
|
|
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
|
-
((
|
|
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 || ((
|
|
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: (
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
},
|