aicodeswitch 4.0.3 → 5.0.0
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 +7 -6
- package/UPGRADE.md +5 -6
- package/dist/server/coding-plan.js +94 -0
- package/dist/server/config-managed-fields.js +1 -0
- package/dist/server/conversions/compact.js +613 -0
- package/dist/server/conversions/detector.js +70 -0
- package/dist/server/conversions/index.js +285 -0
- package/dist/server/conversions/pairs/claude-completions/request.js +167 -0
- package/dist/server/conversions/pairs/claude-completions/response.js +56 -0
- package/dist/server/conversions/pairs/claude-completions/streaming.js +259 -0
- package/dist/server/conversions/pairs/claude-gemini/request.js +130 -0
- package/dist/server/conversions/pairs/claude-gemini/response.js +65 -0
- package/dist/server/conversions/pairs/claude-gemini/streaming.js +199 -0
- package/dist/server/conversions/pairs/claude-responses/request.js +190 -0
- package/dist/server/conversions/pairs/claude-responses/response.js +89 -0
- package/dist/server/conversions/pairs/claude-responses/streaming.js +266 -0
- package/dist/server/conversions/pairs/completions-claude/request.js +111 -0
- package/dist/server/conversions/pairs/completions-claude/response.js +67 -0
- package/dist/server/conversions/pairs/completions-claude/streaming.js +165 -0
- package/dist/server/conversions/pairs/completions-gemini/request.js +169 -0
- package/dist/server/conversions/pairs/completions-gemini/response.js +70 -0
- package/dist/server/conversions/pairs/completions-gemini/streaming.js +132 -0
- package/dist/server/conversions/pairs/completions-responses/request.js +149 -0
- package/dist/server/conversions/pairs/completions-responses/response.js +74 -0
- package/dist/server/conversions/pairs/completions-responses/streaming.js +189 -0
- package/dist/server/conversions/pairs/gemini-claude/request.js +118 -0
- package/dist/server/conversions/pairs/gemini-claude/response.js +45 -0
- package/dist/server/conversions/pairs/gemini-claude/streaming.js +146 -0
- package/dist/server/conversions/pairs/gemini-completions/request.js +151 -0
- package/dist/server/conversions/pairs/gemini-completions/response.js +54 -0
- package/dist/server/conversions/pairs/gemini-completions/streaming.js +108 -0
- package/dist/server/conversions/pairs/gemini-responses/request.js +18 -0
- package/dist/server/conversions/pairs/gemini-responses/response.js +18 -0
- package/dist/server/conversions/pairs/gemini-responses/streaming.js +43 -0
- package/dist/server/conversions/pairs/responses-claude/request.js +155 -0
- package/dist/server/conversions/pairs/responses-claude/response.js +70 -0
- package/dist/server/conversions/pairs/responses-claude/streaming.js +345 -0
- package/dist/server/conversions/pairs/responses-completions/request.js +207 -0
- package/dist/server/conversions/pairs/responses-completions/response.js +96 -0
- package/dist/server/conversions/pairs/responses-completions/streaming.js +344 -0
- package/dist/server/conversions/pairs/responses-gemini/request.js +18 -0
- package/dist/server/conversions/pairs/responses-gemini/response.js +18 -0
- package/dist/server/conversions/pairs/responses-gemini/streaming.js +43 -0
- package/dist/server/conversions/pairs/responses-responses/request.js +115 -0
- package/dist/server/conversions/pipeline.js +296 -0
- package/dist/server/conversions/stream-converter-adapter.js +49 -0
- package/dist/server/conversions/thinking/effort.js +61 -0
- package/dist/server/conversions/thinking/mapper.js +59 -0
- package/dist/server/conversions/thinking/providers.js +76 -0
- package/dist/server/conversions/types.js +5 -0
- package/dist/server/conversions/url-normalizer.js +58 -0
- package/dist/server/conversions/utils/format-mappers.js +57 -0
- package/dist/server/conversions/utils/id.js +33 -0
- package/dist/server/conversions/utils/stop-reasons.js +95 -0
- package/dist/server/conversions/utils/streaming-helpers.js +59 -0
- package/dist/server/conversions/utils/tool-schema.js +169 -0
- package/dist/server/conversions/utils/usage.js +82 -0
- package/dist/server/fs-database.js +465 -135
- package/dist/server/main.js +93 -33
- package/dist/server/original-config-reader.js +1 -1
- package/dist/server/proxy-server.js +1102 -804
- package/dist/server/transformers/chunk-collector.js +5 -1
- package/dist/server/transformers/streaming.js +6 -3235
- package/dist/server/type-migration.js +2 -3
- package/dist/server/utils.js +5 -0
- package/dist/ui/assets/{index-C7G0whng.css → index-BHR12ImE.css} +1 -1
- package/dist/ui/assets/index-DjdBW1yu.js +517 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/server/transformers/transformers.js +0 -1767
- package/dist/ui/assets/index-Nl6yJxrc.js +0 -514
- package/schema/claude.schema.md +0 -946
- package/schema/deepseek-chat.schema.md +0 -799
- package/schema/gemini.schema.md +0 -1408
- package/schema/openai-chat-completions.schema.md +0 -1088
- package/schema/openai-responses.schema.md +0 -226196
- package/schema/stream.md +0 -2592
|
@@ -52,12 +52,123 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
52
52
|
const streaming_1 = require("./transformers/streaming");
|
|
53
53
|
const chunk_collector_1 = require("./transformers/chunk-collector");
|
|
54
54
|
const rules_status_service_1 = require("./rules-status-service");
|
|
55
|
-
const
|
|
55
|
+
const index_1 = require("./conversions/index");
|
|
56
|
+
const stream_converter_adapter_1 = require("./conversions/stream-converter-adapter");
|
|
56
57
|
const types_1 = require("../types");
|
|
57
58
|
const mcp_image_handler_1 = require("./mcp-image-handler");
|
|
58
59
|
const type_migration_1 = require("./type-migration");
|
|
59
60
|
const original_config_reader_1 = require("./original-config-reader");
|
|
61
|
+
const compact_1 = require("./conversions/compact");
|
|
62
|
+
const coding_plan_1 = require("./coding-plan");
|
|
60
63
|
const SUPPORTED_TARGETS = ['claude-code', 'codex'];
|
|
64
|
+
/** 默认模型列表 */
|
|
65
|
+
const DEFAULT_MODELS = [
|
|
66
|
+
{ id: 'claude-sonnet-4-20250514', owned_by: 'anthropic' },
|
|
67
|
+
{ id: 'claude-opus-4-20250514', owned_by: 'anthropic' },
|
|
68
|
+
{ id: 'claude-haiku-4-20250514', owned_by: 'anthropic' },
|
|
69
|
+
{ id: 'gpt-5.3-codex', owned_by: 'openai' },
|
|
70
|
+
{ id: 'gpt-5.4', owned_by: 'openai' },
|
|
71
|
+
{ id: 'gpt-5.5', owned_by: 'openai' },
|
|
72
|
+
{ id: 'gpt-5.4-mini', owned_by: 'openai' },
|
|
73
|
+
{ id: 'o3-pro', owned_by: 'openai' },
|
|
74
|
+
{ id: 'gemini-3-pro-preview', owned_by: 'google' },
|
|
75
|
+
{ id: 'gemini-3-flash-preview', owned_by: 'google' },
|
|
76
|
+
{ id: 'deepseek-r1', owned_by: 'deepseek' },
|
|
77
|
+
{ id: 'deepseek-chat', owned_by: 'deepseek' },
|
|
78
|
+
];
|
|
79
|
+
/** 根据 config 生成模型列表响应 */
|
|
80
|
+
function buildModelsResponse(customModelsStr) {
|
|
81
|
+
const models = (customModelsStr === null || customModelsStr === void 0 ? void 0 : customModelsStr.trim())
|
|
82
|
+
? customModelsStr.split(',').map(s => s.trim()).filter(Boolean)
|
|
83
|
+
: DEFAULT_MODELS.map(m => m.id);
|
|
84
|
+
return {
|
|
85
|
+
object: 'list',
|
|
86
|
+
data: models.map(id => ({
|
|
87
|
+
id,
|
|
88
|
+
object: 'model',
|
|
89
|
+
created: 1747267200,
|
|
90
|
+
owned_by: 'custom',
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** 匹配标准 API 路径 */
|
|
95
|
+
function matchApiPath(reqPath) {
|
|
96
|
+
const p = reqPath.split('?')[0];
|
|
97
|
+
if (p === '/v1/models')
|
|
98
|
+
return '/v1/models';
|
|
99
|
+
if (p === '/v1/messages' || p.startsWith('/v1/messages/'))
|
|
100
|
+
return '/v1/messages';
|
|
101
|
+
if (p === '/v1/responses')
|
|
102
|
+
return '/v1/responses';
|
|
103
|
+
if (p === '/v1/chat/completions')
|
|
104
|
+
return '/v1/chat/completions';
|
|
105
|
+
if (/^\/v1beta\/models\//.test(p))
|
|
106
|
+
return '/v1beta/models';
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** 从 API 路径推断客户端格式 */
|
|
110
|
+
function apiPathToClientFormat(apiPath) {
|
|
111
|
+
switch (apiPath) {
|
|
112
|
+
case '/v1/messages': return 'claude';
|
|
113
|
+
case '/v1/responses': return 'responses';
|
|
114
|
+
case '/v1/chat/completions': return 'completions';
|
|
115
|
+
case '/v1beta/models': return 'gemini';
|
|
116
|
+
case '/v1/models': return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
class ClaudeCompactResponseSanitizer extends stream_1.Transform {
|
|
120
|
+
constructor() {
|
|
121
|
+
super({ objectMode: true });
|
|
122
|
+
Object.defineProperty(this, "skippedBlockIndexes", {
|
|
123
|
+
enumerable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
writable: true,
|
|
126
|
+
value: new Set()
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(this, "filteredToolUse", {
|
|
129
|
+
enumerable: true,
|
|
130
|
+
configurable: true,
|
|
131
|
+
writable: true,
|
|
132
|
+
value: false
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
_transform(event, _encoding, callback) {
|
|
136
|
+
var _a, _b;
|
|
137
|
+
const data = event === null || event === void 0 ? void 0 : event.data;
|
|
138
|
+
if (!data || typeof data !== 'object') {
|
|
139
|
+
this.push(event);
|
|
140
|
+
callback();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (data.type === 'content_block_start') {
|
|
144
|
+
const blockType = (_a = data.content_block) === null || _a === void 0 ? void 0 : _a.type;
|
|
145
|
+
if (blockType === 'thinking' || blockType === 'tool_use') {
|
|
146
|
+
if (typeof data.index === 'number') {
|
|
147
|
+
this.skippedBlockIndexes.add(data.index);
|
|
148
|
+
}
|
|
149
|
+
if (blockType === 'tool_use') {
|
|
150
|
+
this.filteredToolUse = true;
|
|
151
|
+
}
|
|
152
|
+
callback();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if ((data.type === 'content_block_delta' || data.type === 'content_block_stop') && this.skippedBlockIndexes.has(data.index)) {
|
|
157
|
+
if (data.type === 'content_block_stop') {
|
|
158
|
+
this.skippedBlockIndexes.delete(data.index);
|
|
159
|
+
}
|
|
160
|
+
callback();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (data.type === 'message_delta' && this.filteredToolUse && ((_b = data.delta) === null || _b === void 0 ? void 0 : _b.stop_reason) === 'tool_use') {
|
|
164
|
+
this.push(Object.assign(Object.assign({}, event), { data: Object.assign(Object.assign({}, (data || {})), { delta: Object.assign(Object.assign({}, ((data === null || data === void 0 ? void 0 : data.delta) || {})), { stop_reason: 'end_turn' }) }) }));
|
|
165
|
+
callback();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.push(event);
|
|
169
|
+
callback();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
61
172
|
class ProxyServer {
|
|
62
173
|
constructor(dbManager, app) {
|
|
63
174
|
Object.defineProperty(this, "app", {
|
|
@@ -133,6 +244,14 @@ class ProxyServer {
|
|
|
133
244
|
}
|
|
134
245
|
return undefined;
|
|
135
246
|
}
|
|
247
|
+
inferToolFromRequest(req) {
|
|
248
|
+
const path = req.path || '';
|
|
249
|
+
if (path.startsWith('/claude-code'))
|
|
250
|
+
return 'claude-code';
|
|
251
|
+
if (path.startsWith('/codex'))
|
|
252
|
+
return 'codex';
|
|
253
|
+
return 'claude-code';
|
|
254
|
+
}
|
|
136
255
|
buildRelayTags(relayed, useOriginalConfig = false) {
|
|
137
256
|
const tags = [relayed ? '通过中转' : '未通过中转'];
|
|
138
257
|
if (useOriginalConfig) {
|
|
@@ -147,7 +266,7 @@ class ProxyServer {
|
|
|
147
266
|
const resolvedTargetType = options.targetType ||
|
|
148
267
|
this.inferTargetTypeFromPath(req.path) ||
|
|
149
268
|
this.inferTargetTypeFromPath(req.originalUrl || '');
|
|
150
|
-
if (!enableLogging
|
|
269
|
+
if (!enableLogging) {
|
|
151
270
|
return;
|
|
152
271
|
}
|
|
153
272
|
yield this.dbManager.addLog({
|
|
@@ -167,9 +286,48 @@ class ProxyServer {
|
|
|
167
286
|
});
|
|
168
287
|
}
|
|
169
288
|
initialize() {
|
|
170
|
-
//
|
|
289
|
+
// === 标准 API 路径前置中间件 ===
|
|
290
|
+
// 处理 /v1/models 和 4 个可绑定的标准 API 路径
|
|
171
291
|
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
172
|
-
|
|
292
|
+
const apiPath = matchApiPath(req.path);
|
|
293
|
+
if (!apiPath) {
|
|
294
|
+
return next();
|
|
295
|
+
}
|
|
296
|
+
// /v1/models: 直接返回静态模型列表
|
|
297
|
+
if (apiPath === '/v1/models') {
|
|
298
|
+
// 鉴权
|
|
299
|
+
if (this.config.apiKey) {
|
|
300
|
+
const authHeader = req.headers.authorization;
|
|
301
|
+
const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
|
|
302
|
+
if (!providedKey || providedKey !== this.config.apiKey) {
|
|
303
|
+
res.status(401).json({ error: { message: 'Invalid API key' } });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
res.json(buildModelsResponse(this.dbManager.getApiPathModels()));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// 其余 4 个路径:查找绑定
|
|
311
|
+
const bindings = this.dbManager.getApiPathBindings();
|
|
312
|
+
const binding = bindings.find((b) => b.apiPath === apiPath);
|
|
313
|
+
if (!binding || !binding.routeId) {
|
|
314
|
+
res.status(404).json({ error: { message: `API path ${apiPath} is not bound to any route. Please configure it in Route Mapping settings.` } });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// 加载绑定的路由
|
|
318
|
+
const allRoutes = this.dbManager.getRoutes();
|
|
319
|
+
const route = allRoutes.find((r) => r.id === binding.routeId);
|
|
320
|
+
if (!route) {
|
|
321
|
+
return res.status(404).json({ error: { message: `Bound route '${binding.routeId}' not found or inactive. Please check Route Mapping settings.` } });
|
|
322
|
+
}
|
|
323
|
+
// 推断客户端格式
|
|
324
|
+
const clientFormat = apiPathToClientFormat(apiPath);
|
|
325
|
+
// 复用完整的代理请求处理
|
|
326
|
+
yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath);
|
|
327
|
+
}));
|
|
328
|
+
// Dynamic proxy middleware (原有的 /claude-code, /codex 逻辑)
|
|
329
|
+
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
330
|
+
var _a, _b, _c, _d, _e;
|
|
173
331
|
// 仅处理支持的目标路径
|
|
174
332
|
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
175
333
|
return next();
|
|
@@ -195,9 +353,8 @@ class ProxyServer {
|
|
|
195
353
|
// 如果原始配置也不可用,返回错误
|
|
196
354
|
return res.status(404).json({ error: 'No matching route found and no original config available' });
|
|
197
355
|
}
|
|
198
|
-
//
|
|
199
|
-
const forcedContentType = yield this.prepareHighIqRouting(req, route,
|
|
200
|
-
// 检查是否启用故障切换
|
|
356
|
+
// 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
|
|
357
|
+
const forcedContentType = yield this.prepareHighIqRouting(req, route, this.inferTargetTypeFromPath(req.path) || 'claude-code');
|
|
201
358
|
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
202
359
|
if (!enableFailover) {
|
|
203
360
|
// 故障切换已禁用,使用传统的单一规则匹配
|
|
@@ -211,7 +368,7 @@ class ProxyServer {
|
|
|
211
368
|
yield this.logToolRequest(req, {
|
|
212
369
|
statusCode: 404,
|
|
213
370
|
responseTime: Date.now() - requestStartAt,
|
|
214
|
-
targetType:
|
|
371
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
215
372
|
error: 'No matching rule found',
|
|
216
373
|
tags: this.buildRelayTags(false),
|
|
217
374
|
});
|
|
@@ -222,7 +379,7 @@ class ProxyServer {
|
|
|
222
379
|
yield this.logToolRequest(req, {
|
|
223
380
|
statusCode: 500,
|
|
224
381
|
responseTime: Date.now() - requestStartAt,
|
|
225
|
-
targetType:
|
|
382
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
226
383
|
error: 'Target service not configured',
|
|
227
384
|
tags: this.buildRelayTags(false),
|
|
228
385
|
});
|
|
@@ -243,7 +400,7 @@ class ProxyServer {
|
|
|
243
400
|
yield this.logToolRequest(req, {
|
|
244
401
|
statusCode: 404,
|
|
245
402
|
responseTime: Date.now() - requestStartAt,
|
|
246
|
-
targetType:
|
|
403
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
247
404
|
error: 'No matching rule found',
|
|
248
405
|
tags: this.buildRelayTags(false),
|
|
249
406
|
});
|
|
@@ -294,7 +451,7 @@ class ProxyServer {
|
|
|
294
451
|
}
|
|
295
452
|
else {
|
|
296
453
|
// HTTP错误,检查状态码
|
|
297
|
-
const statusCode = (
|
|
454
|
+
const statusCode = this.getErrorStatusCode(error, 500);
|
|
298
455
|
if (statusCode >= 400) {
|
|
299
456
|
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
300
457
|
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
@@ -334,7 +491,7 @@ class ProxyServer {
|
|
|
334
491
|
yield this.logToolRequest(req, {
|
|
335
492
|
statusCode: 503,
|
|
336
493
|
responseTime: Date.now() - requestStartAt,
|
|
337
|
-
targetType:
|
|
494
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
338
495
|
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
339
496
|
tags: this.buildRelayTags(hasRelayAttempt),
|
|
340
497
|
});
|
|
@@ -353,13 +510,13 @@ class ProxyServer {
|
|
|
353
510
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
354
511
|
// 添加请求详情
|
|
355
512
|
targetType,
|
|
356
|
-
requestModel: (
|
|
513
|
+
requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
|
|
357
514
|
responseTime: Date.now() - requestStartAt,
|
|
358
515
|
// 添加最后失败的服务信息
|
|
359
516
|
ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
|
|
360
517
|
targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
|
|
361
518
|
targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
|
|
362
|
-
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((
|
|
519
|
+
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
363
520
|
vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
|
|
364
521
|
vendorName: _lastFailedVendor === null || _lastFailedVendor === void 0 ? void 0 : _lastFailedVendor.name,
|
|
365
522
|
});
|
|
@@ -407,7 +564,7 @@ class ProxyServer {
|
|
|
407
564
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
408
565
|
// 添加请求详情
|
|
409
566
|
targetType,
|
|
410
|
-
requestModel: (
|
|
567
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
411
568
|
responseTime: Date.now() - requestStartAt,
|
|
412
569
|
});
|
|
413
570
|
// 根据路径判断目标类型并返回适当的错误格式
|
|
@@ -440,7 +597,7 @@ class ProxyServer {
|
|
|
440
597
|
}
|
|
441
598
|
createFixedRouteHandler(targetType) {
|
|
442
599
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
443
|
-
var _a, _b, _c, _d, _e
|
|
600
|
+
var _a, _b, _c, _d, _e;
|
|
444
601
|
const requestStartAt = Date.now();
|
|
445
602
|
let hasRelayAttempt = false;
|
|
446
603
|
try {
|
|
@@ -470,7 +627,7 @@ class ProxyServer {
|
|
|
470
627
|
});
|
|
471
628
|
return res.status(404).json({ error: `No active route found for target type: ${targetType}` });
|
|
472
629
|
}
|
|
473
|
-
//
|
|
630
|
+
// 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
|
|
474
631
|
const forcedContentType = yield this.prepareHighIqRouting(req, route, targetType);
|
|
475
632
|
// 检查是否启用故障切换
|
|
476
633
|
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
@@ -569,7 +726,7 @@ class ProxyServer {
|
|
|
569
726
|
}
|
|
570
727
|
else {
|
|
571
728
|
// HTTP错误,检查状态码
|
|
572
|
-
const statusCode = (
|
|
729
|
+
const statusCode = this.getErrorStatusCode(error, 500);
|
|
573
730
|
if (statusCode >= 400) {
|
|
574
731
|
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
575
732
|
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
@@ -626,13 +783,13 @@ class ProxyServer {
|
|
|
626
783
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
627
784
|
// 添加请求详情
|
|
628
785
|
targetType,
|
|
629
|
-
requestModel: (
|
|
786
|
+
requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
|
|
630
787
|
responseTime: Date.now() - requestStartAt,
|
|
631
788
|
// 添加最后失败的服务信息
|
|
632
789
|
ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
|
|
633
790
|
targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
|
|
634
791
|
targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
|
|
635
|
-
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((
|
|
792
|
+
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
636
793
|
vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
|
|
637
794
|
vendorName: _lastFailedVendor2 === null || _lastFailedVendor2 === void 0 ? void 0 : _lastFailedVendor2.name,
|
|
638
795
|
});
|
|
@@ -679,7 +836,7 @@ class ProxyServer {
|
|
|
679
836
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
680
837
|
// 添加请求详情
|
|
681
838
|
targetType,
|
|
682
|
-
requestModel: (
|
|
839
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
683
840
|
responseTime: Date.now() - requestStartAt,
|
|
684
841
|
});
|
|
685
842
|
if (this.isResponseCommitted(res)) {
|
|
@@ -693,9 +850,6 @@ class ProxyServer {
|
|
|
693
850
|
* 从数据库实时获取所有活跃路由
|
|
694
851
|
* @returns 活跃路由列表
|
|
695
852
|
*/
|
|
696
|
-
getActiveRoutes() {
|
|
697
|
-
return this.dbManager.getRoutes().filter(route => route.isActive);
|
|
698
|
-
}
|
|
699
853
|
/**
|
|
700
854
|
* 从数据库实时获取指定路由的规则
|
|
701
855
|
* @param routeId 路由ID
|
|
@@ -709,20 +863,19 @@ class ProxyServer {
|
|
|
709
863
|
return this.dbManager.getRule(ruleId);
|
|
710
864
|
}
|
|
711
865
|
findMatchingRoute(req) {
|
|
712
|
-
|
|
713
|
-
let targetType;
|
|
866
|
+
let tool;
|
|
714
867
|
if (req.path.startsWith('/claude-code/')) {
|
|
715
|
-
|
|
868
|
+
tool = 'claude-code';
|
|
716
869
|
}
|
|
717
870
|
else if (req.path.startsWith('/codex/')) {
|
|
718
|
-
|
|
871
|
+
tool = 'codex';
|
|
719
872
|
}
|
|
720
|
-
if (!
|
|
873
|
+
if (!tool)
|
|
721
874
|
return undefined;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return
|
|
875
|
+
const routeId = this.dbManager.getActiveRouteIdForTool(tool);
|
|
876
|
+
if (!routeId)
|
|
877
|
+
return undefined;
|
|
878
|
+
return this.dbManager.getRoute(routeId);
|
|
726
879
|
}
|
|
727
880
|
/**
|
|
728
881
|
* 当没有激活的路由时,fallback 到原始配置
|
|
@@ -773,8 +926,6 @@ class ProxyServer {
|
|
|
773
926
|
const tempRoute = {
|
|
774
927
|
id: 'fallback-route',
|
|
775
928
|
name: 'Fallback to Original Config',
|
|
776
|
-
targetType: targetType,
|
|
777
|
-
isActive: true,
|
|
778
929
|
createdAt: Date.now(),
|
|
779
930
|
updatedAt: Date.now(),
|
|
780
931
|
};
|
|
@@ -873,9 +1024,11 @@ class ProxyServer {
|
|
|
873
1024
|
}
|
|
874
1025
|
return undefined;
|
|
875
1026
|
}
|
|
876
|
-
findRouteByTargetType(
|
|
877
|
-
const
|
|
878
|
-
|
|
1027
|
+
findRouteByTargetType(tool) {
|
|
1028
|
+
const routeId = this.dbManager.getActiveRouteIdForTool(tool);
|
|
1029
|
+
if (!routeId)
|
|
1030
|
+
return undefined;
|
|
1031
|
+
return this.dbManager.getRoute(routeId);
|
|
879
1032
|
}
|
|
880
1033
|
findNextAvailableServiceName(allRules, startIndex, routeId) {
|
|
881
1034
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -915,12 +1068,45 @@ class ProxyServer {
|
|
|
915
1068
|
createFailoverError(message, statusCode, originalError) {
|
|
916
1069
|
const failoverError = new Error(message);
|
|
917
1070
|
failoverError.isFailoverCandidate = true;
|
|
1071
|
+
failoverError.statusCode = statusCode;
|
|
918
1072
|
failoverError.response = { status: statusCode };
|
|
919
1073
|
if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
|
|
920
1074
|
failoverError.stack = originalError.stack;
|
|
921
1075
|
}
|
|
922
1076
|
return failoverError;
|
|
923
1077
|
}
|
|
1078
|
+
getErrorStatusCode(error, fallbackStatusCode = 500) {
|
|
1079
|
+
var _a, _b, _c;
|
|
1080
|
+
const statusCode = (_c = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.statusCode) !== null && _c !== void 0 ? _c : error === null || error === void 0 ? void 0 : error.status;
|
|
1081
|
+
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
1082
|
+
return statusCode;
|
|
1083
|
+
}
|
|
1084
|
+
return fallbackStatusCode;
|
|
1085
|
+
}
|
|
1086
|
+
detectStreamFailure(events) {
|
|
1087
|
+
var _a, _b;
|
|
1088
|
+
for (const event of events) {
|
|
1089
|
+
const eventType = (_a = event.event) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1090
|
+
if (!eventType)
|
|
1091
|
+
continue;
|
|
1092
|
+
if (eventType !== 'response.failed' && eventType !== 'error') {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const parsed = event.data ? this.safeJsonParse(event.data) : null;
|
|
1096
|
+
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;
|
|
1097
|
+
const errorCode = errorObj === null || errorObj === void 0 ? void 0 : errorObj.code;
|
|
1098
|
+
const errorMessage = (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message)
|
|
1099
|
+
|| (parsed === null || parsed === void 0 ? void 0 : parsed.message)
|
|
1100
|
+
|| `Upstream stream returned ${eventType}`;
|
|
1101
|
+
const normalizedMessage = `Upstream stream returned ${eventType}: ${errorMessage}`;
|
|
1102
|
+
const statusCode = errorCode === 'server_is_overloaded' ? 503 : 502;
|
|
1103
|
+
return {
|
|
1104
|
+
statusCode,
|
|
1105
|
+
errorMessage: normalizedMessage,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
924
1110
|
isDownstreamClosed(res) {
|
|
925
1111
|
return res.destroyed || res.writableEnded || !res.writable;
|
|
926
1112
|
}
|
|
@@ -1143,8 +1329,7 @@ class ProxyServer {
|
|
|
1143
1329
|
return undefined;
|
|
1144
1330
|
const body = req.body;
|
|
1145
1331
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
1146
|
-
const
|
|
1147
|
-
const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
|
|
1332
|
+
const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
|
|
1148
1333
|
// 高智商规则优先于 model-mapping,确保 !!/推断命中时不会被模型映射覆盖
|
|
1149
1334
|
if (contentType === 'high-iq') {
|
|
1150
1335
|
const highIqRules = enabledRules.filter(rule => rule.contentType === 'high-iq');
|
|
@@ -1260,8 +1445,7 @@ class ProxyServer {
|
|
|
1260
1445
|
const body = req.body;
|
|
1261
1446
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
1262
1447
|
const candidates = [];
|
|
1263
|
-
const
|
|
1264
|
-
const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
|
|
1448
|
+
const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
|
|
1265
1449
|
const prioritizeContentType = contentType === 'high-iq';
|
|
1266
1450
|
const modelMappingRules = requestModel
|
|
1267
1451
|
? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
@@ -1414,13 +1598,23 @@ class ProxyServer {
|
|
|
1414
1598
|
}
|
|
1415
1599
|
getContentTypeDetectors() {
|
|
1416
1600
|
return [
|
|
1601
|
+
{
|
|
1602
|
+
type: 'compact',
|
|
1603
|
+
match: (req, body) => {
|
|
1604
|
+
if ((0, compact_1.isCodexCompactRequest)(req.path) || (0, compact_1.isCodexCompactRequest)(req.originalUrl)) {
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
const messages = this.extractConversationMessages(body);
|
|
1608
|
+
return (0, compact_1.isLastClaudeMessageCompact)(messages);
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1417
1611
|
{
|
|
1418
1612
|
type: 'image-understanding',
|
|
1419
1613
|
match: (_req, body) => this.containsImageContentInLatestMessage(body.messages) || this.containsImageContent(body.input),
|
|
1420
1614
|
},
|
|
1421
1615
|
{
|
|
1422
1616
|
type: 'high-iq',
|
|
1423
|
-
match: (_req, body) => this.hasHighIqSignal(body),
|
|
1617
|
+
match: (_req, body, _sessionId, routeId) => this.hasHighIqSignal(body, routeId),
|
|
1424
1618
|
},
|
|
1425
1619
|
{
|
|
1426
1620
|
type: 'long-context',
|
|
@@ -1501,6 +1695,10 @@ class ProxyServer {
|
|
|
1501
1695
|
image_understanding: 'image-understanding',
|
|
1502
1696
|
'image-understanding': 'image-understanding',
|
|
1503
1697
|
vision: 'image-understanding',
|
|
1698
|
+
compact: 'compact',
|
|
1699
|
+
compaction: 'compact',
|
|
1700
|
+
summarize: 'compact',
|
|
1701
|
+
summary: 'compact',
|
|
1504
1702
|
};
|
|
1505
1703
|
return mapping[normalized] || null;
|
|
1506
1704
|
}
|
|
@@ -1564,11 +1762,19 @@ class ProxyServer {
|
|
|
1564
1762
|
((_a = body === null || body === void 0 ? void 0 : body.reasoning) === null || _a === void 0 ? void 0 : _a.effort) ||
|
|
1565
1763
|
((_b = body === null || body === void 0 ? void 0 : body.reasoning) === null || _b === void 0 ? void 0 : _b.enabled));
|
|
1566
1764
|
}
|
|
1567
|
-
|
|
1765
|
+
hasHighIqRuleForRoute(routeId) {
|
|
1766
|
+
var _a;
|
|
1767
|
+
const rules = this.getRulesByRouteId(routeId);
|
|
1768
|
+
return (_a = rules === null || rules === void 0 ? void 0 : rules.some(rule => rule.contentType === 'high-iq' && !rule.isDisabled)) !== null && _a !== void 0 ? _a : false;
|
|
1769
|
+
}
|
|
1770
|
+
hasHighIqSignal(body, routeId) {
|
|
1771
|
+
if (routeId && !this.hasHighIqRuleForRoute(routeId))
|
|
1772
|
+
return false;
|
|
1568
1773
|
return this.inferHighIqRouting(body, false).shouldUseHighIq;
|
|
1569
1774
|
}
|
|
1570
1775
|
inferHighIqRouting(body, previousMode) {
|
|
1571
1776
|
const messages = this.extractConversationMessages(body);
|
|
1777
|
+
// 从消息列表末尾往前查找 [!] 或 [x] 标记,普通消息跳过继续搜索
|
|
1572
1778
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1573
1779
|
const message = messages[i];
|
|
1574
1780
|
if ((message === null || message === void 0 ? void 0 : message.role) !== 'user') {
|
|
@@ -1578,17 +1784,22 @@ class ProxyServer {
|
|
|
1578
1784
|
if (!signal.hasHumanText) {
|
|
1579
1785
|
continue;
|
|
1580
1786
|
}
|
|
1787
|
+
// [x] 优先:同一消息中 [x] 覆盖 [!]
|
|
1788
|
+
if (signal.hasCancelPrefix) {
|
|
1789
|
+
return {
|
|
1790
|
+
shouldUseHighIq: false,
|
|
1791
|
+
decisionSource: 'human',
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1581
1794
|
if (signal.hasHighIqPrefix) {
|
|
1582
1795
|
return {
|
|
1583
1796
|
shouldUseHighIq: true,
|
|
1584
1797
|
decisionSource: 'human',
|
|
1585
1798
|
};
|
|
1586
1799
|
}
|
|
1587
|
-
|
|
1588
|
-
shouldUseHighIq: false,
|
|
1589
|
-
decisionSource: 'human',
|
|
1590
|
-
};
|
|
1800
|
+
// 普通消息(无 [!] 或 [x] 前缀),继续向前搜索
|
|
1591
1801
|
}
|
|
1802
|
+
// 未找到 [!] 或 [x] 标记,回退到 session 持久化状态
|
|
1592
1803
|
if (previousMode) {
|
|
1593
1804
|
return {
|
|
1594
1805
|
shouldUseHighIq: true,
|
|
@@ -1602,6 +1813,10 @@ class ProxyServer {
|
|
|
1602
1813
|
}
|
|
1603
1814
|
prepareHighIqRouting(req, route, targetType) {
|
|
1604
1815
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1816
|
+
// 无高智商规则时直接跳过,避免每次都检查消息前缀
|
|
1817
|
+
if (!this.hasHighIqRuleForRoute(route.id)) {
|
|
1818
|
+
return undefined;
|
|
1819
|
+
}
|
|
1605
1820
|
const sessionId = this.defaultExtractSessionId(req, targetType);
|
|
1606
1821
|
const session = sessionId ? this.dbManager.getSession(sessionId) : null;
|
|
1607
1822
|
const previousMode = (session === null || session === void 0 ? void 0 : session.highIqMode) === true;
|
|
@@ -1613,7 +1828,7 @@ class ProxyServer {
|
|
|
1613
1828
|
highIqRuleId: undefined,
|
|
1614
1829
|
lastRequestAt: Date.now(),
|
|
1615
1830
|
});
|
|
1616
|
-
console.log(`[HIGH-IQ] Session ${sessionId}
|
|
1831
|
+
console.log(`[HIGH-IQ] Session ${sessionId} cancelled by [x] prefix`);
|
|
1617
1832
|
}
|
|
1618
1833
|
return undefined;
|
|
1619
1834
|
}
|
|
@@ -1671,6 +1886,7 @@ class ProxyServer {
|
|
|
1671
1886
|
analyzeUserMessageForHighIq(message) {
|
|
1672
1887
|
let hasHumanText = false;
|
|
1673
1888
|
let hasHighIqPrefix = false;
|
|
1889
|
+
let hasCancelPrefix = false;
|
|
1674
1890
|
const scanText = (text, treatAsHuman) => {
|
|
1675
1891
|
const trimmed = text.trim();
|
|
1676
1892
|
if (!trimmed) {
|
|
@@ -1682,11 +1898,14 @@ class ProxyServer {
|
|
|
1682
1898
|
if (treatAsHuman && trimmed.startsWith('[!]')) {
|
|
1683
1899
|
hasHighIqPrefix = true;
|
|
1684
1900
|
}
|
|
1901
|
+
if (treatAsHuman && /^\[x]/i.test(trimmed)) {
|
|
1902
|
+
hasCancelPrefix = true;
|
|
1903
|
+
}
|
|
1685
1904
|
};
|
|
1686
1905
|
const content = message === null || message === void 0 ? void 0 : message.content;
|
|
1687
1906
|
if (typeof content === 'string') {
|
|
1688
1907
|
scanText(content, true);
|
|
1689
|
-
return { hasHumanText, hasHighIqPrefix };
|
|
1908
|
+
return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
|
|
1690
1909
|
}
|
|
1691
1910
|
const blocks = Array.isArray(content) ? content : [content];
|
|
1692
1911
|
for (const block of blocks) {
|
|
@@ -1722,7 +1941,7 @@ class ProxyServer {
|
|
|
1722
1941
|
}
|
|
1723
1942
|
}
|
|
1724
1943
|
}
|
|
1725
|
-
return { hasHumanText, hasHighIqPrefix };
|
|
1944
|
+
return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
|
|
1726
1945
|
}
|
|
1727
1946
|
/**
|
|
1728
1947
|
* 查找可用的高智商规则
|
|
@@ -2048,21 +2267,13 @@ class ProxyServer {
|
|
|
2048
2267
|
// 向下兼容:支持旧类型 'claude-code'
|
|
2049
2268
|
return sourceType === 'claude' || sourceType === 'claude-code';
|
|
2050
2269
|
}
|
|
2051
|
-
/** 判断是否为 Claude Chat 类型 */
|
|
2052
|
-
isClaudeChatSource(sourceType) {
|
|
2053
|
-
return sourceType === 'claude-chat';
|
|
2054
|
-
}
|
|
2055
2270
|
isOpenAISource(sourceType) {
|
|
2056
2271
|
// 向下兼容:支持旧类型 'openai-responses'
|
|
2057
2272
|
return sourceType === 'openai' || sourceType === 'openai-responses';
|
|
2058
2273
|
}
|
|
2059
2274
|
/** 判断是否为 OpenAI Chat 类型 */
|
|
2060
2275
|
isOpenAIChatSource(sourceType) {
|
|
2061
|
-
return sourceType === 'openai-chat'
|
|
2062
|
-
}
|
|
2063
|
-
/** 判断是否为 OpenAI 类型(包括 OpenAI Chat 和 OpenAI Responses) */
|
|
2064
|
-
isOpenAIType(sourceType) {
|
|
2065
|
-
return sourceType === 'openai' || sourceType === 'openai-chat' || sourceType === 'openai-responses' || sourceType === 'deepseek-reasoning-chat';
|
|
2276
|
+
return sourceType === 'openai-chat';
|
|
2066
2277
|
}
|
|
2067
2278
|
/** 判断是否为 Gemini 类型 */
|
|
2068
2279
|
isGeminiSource(sourceType) {
|
|
@@ -2164,7 +2375,7 @@ class ProxyServer {
|
|
|
2164
2375
|
const headers = {};
|
|
2165
2376
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
2166
2377
|
// 排除原始认证头,防止与代理设置的认证头冲突
|
|
2167
|
-
if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key'].includes(key.toLowerCase())) {
|
|
2378
|
+
if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key', 'accept-encoding'].includes(key.toLowerCase())) {
|
|
2168
2379
|
continue;
|
|
2169
2380
|
}
|
|
2170
2381
|
if (typeof value === 'string') {
|
|
@@ -2192,13 +2403,17 @@ class ProxyServer {
|
|
|
2192
2403
|
headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
|
|
2193
2404
|
}
|
|
2194
2405
|
}
|
|
2195
|
-
// 使用 Authorization 认证(适用于 openai-chat, openai-responses
|
|
2406
|
+
// 使用 Authorization 认证(适用于 openai-chat, openai-responses 等)
|
|
2196
2407
|
else {
|
|
2197
2408
|
headers.authorization = `Bearer ${effectiveApiKey}`;
|
|
2198
2409
|
}
|
|
2199
2410
|
if (streamRequested && !headers.accept) {
|
|
2200
2411
|
headers.accept = 'text/event-stream';
|
|
2201
2412
|
}
|
|
2413
|
+
// 流式场景显式禁用压缩,避免上游返回压缩字节流导致下游出现乱码
|
|
2414
|
+
if (streamRequested) {
|
|
2415
|
+
headers['accept-encoding'] = 'identity';
|
|
2416
|
+
}
|
|
2202
2417
|
if (!headers.connection) {
|
|
2203
2418
|
if (streamRequested) {
|
|
2204
2419
|
headers.connection = 'keep-alive';
|
|
@@ -2225,6 +2440,17 @@ class ProxyServer {
|
|
|
2225
2440
|
}
|
|
2226
2441
|
return vendor.apiKey || '';
|
|
2227
2442
|
}
|
|
2443
|
+
resolveEffectiveApiUrl(service) {
|
|
2444
|
+
if (service.inheritVendorApiBaseUrl !== true) {
|
|
2445
|
+
return service.apiUrl;
|
|
2446
|
+
}
|
|
2447
|
+
const vendor = this.dbManager.getVendorByServiceId(service.id);
|
|
2448
|
+
if (!vendor || !vendor.apiBaseUrl) {
|
|
2449
|
+
console.warn(`[Proxy] Service ${service.id} is set to inherit vendor API base URL, but vendor/url is missing`);
|
|
2450
|
+
return service.apiUrl;
|
|
2451
|
+
}
|
|
2452
|
+
return vendor.apiBaseUrl;
|
|
2453
|
+
}
|
|
2228
2454
|
copyResponseHeaders(responseHeaders, res) {
|
|
2229
2455
|
Object.keys(responseHeaders).forEach((key) => {
|
|
2230
2456
|
if (!['content-encoding', 'transfer-encoding', 'connection', 'content-length'].includes(key.toLowerCase())) {
|
|
@@ -2310,6 +2536,17 @@ class ProxyServer {
|
|
|
2310
2536
|
return null;
|
|
2311
2537
|
}
|
|
2312
2538
|
}
|
|
2539
|
+
cloneRequestBody(data) {
|
|
2540
|
+
if (data === null || data === undefined) {
|
|
2541
|
+
return data;
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
return JSON.parse(JSON.stringify(data));
|
|
2545
|
+
}
|
|
2546
|
+
catch (_a) {
|
|
2547
|
+
return data;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2313
2550
|
isEmptyResponse(data) {
|
|
2314
2551
|
if (data === null || data === undefined)
|
|
2315
2552
|
return true;
|
|
@@ -2332,8 +2569,8 @@ class ProxyServer {
|
|
|
2332
2569
|
return ProxyServer.extractSessionIdFromUserId(rawUserId);
|
|
2333
2570
|
}
|
|
2334
2571
|
else if (type === 'codex') {
|
|
2335
|
-
// Codex 使用 headers
|
|
2336
|
-
const sessionId = request.headers['session_id'];
|
|
2572
|
+
// Codex 使用 headers 中的 session-id 或 session_id(兼容新旧版本)
|
|
2573
|
+
const sessionId = request.headers['session-id'] || request.headers['session_id'];
|
|
2337
2574
|
if (typeof sessionId === 'string') {
|
|
2338
2575
|
return sessionId;
|
|
2339
2576
|
}
|
|
@@ -2524,48 +2761,19 @@ class ProxyServer {
|
|
|
2524
2761
|
* @param targetModel 目标模型名称(可选)
|
|
2525
2762
|
* @returns 转换后往服务商API接口的数据
|
|
2526
2763
|
*/
|
|
2527
|
-
transformRequestToUpstream(tool, source, payloadData, targetModel) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
if (
|
|
2536
|
-
|
|
2537
|
-
}
|
|
2538
|
-
// claudecode发送给openai chat completion接口
|
|
2539
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2540
|
-
return (0, transformers_1.transformRequestFromClaudeToChatCompletions)(payloadData, targetModel);
|
|
2541
|
-
}
|
|
2542
|
-
// claudecode发送给openai responses接口
|
|
2543
|
-
if (this.isOpenAISource(source)) {
|
|
2544
|
-
return (0, transformers_1.transformRequestFromClaudeToResponses)(payloadData, targetModel);
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
// Codex 发起的请求(仅支持 Responses API 格式)
|
|
2548
|
-
if (tool === 'codex') {
|
|
2549
|
-
// Codex 发送给 OpenAI Responses
|
|
2550
|
-
if (this.isOpenAISource(source)) {
|
|
2551
|
-
return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
|
|
2552
|
-
}
|
|
2553
|
-
// Codex 发送给 OpenAI Chat
|
|
2554
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2555
|
-
// 将 responses 格式转换为 chat completions 格式
|
|
2556
|
-
return (0, transformers_1.transformRequestFromResponsesToChatCompletions)(payloadData, targetModel);
|
|
2557
|
-
}
|
|
2558
|
-
// Codex 发送给 Gemini
|
|
2559
|
-
if (this.isGeminiChatSource(source) || this.isGeminiSource(source)) {
|
|
2560
|
-
return (0, transformers_1.transformRequestFromResponsesToGemini)(payloadData, targetModel);
|
|
2561
|
-
}
|
|
2562
|
-
// Codex 发送给 Claude
|
|
2563
|
-
if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
|
|
2564
|
-
return (0, transformers_1.transformRequestFromResponsesToClaude)(payloadData, targetModel);
|
|
2764
|
+
transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig) {
|
|
2765
|
+
const clientFormat = tool === 'codex' ? 'responses' : 'claude';
|
|
2766
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
2767
|
+
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
|
|
2768
|
+
const body = result.body;
|
|
2769
|
+
// 模型覆盖:OpenAI 模型族保持原样,其余覆盖为 targetModel
|
|
2770
|
+
if (targetModel) {
|
|
2771
|
+
const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
|
|
2772
|
+
if (!isOpenAIModel) {
|
|
2773
|
+
body.model = targetModel;
|
|
2565
2774
|
}
|
|
2566
2775
|
}
|
|
2567
|
-
|
|
2568
|
-
return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
|
|
2776
|
+
return body;
|
|
2569
2777
|
}
|
|
2570
2778
|
/**
|
|
2571
2779
|
* 将来自API接口的响应数据,转换为工具需要的数据结构
|
|
@@ -2574,170 +2782,114 @@ class ProxyServer {
|
|
|
2574
2782
|
* @param responseData
|
|
2575
2783
|
*/
|
|
2576
2784
|
transformResponseToTool(tool, source, responseData) {
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2582
|
-
return (0, transformers_1.transformResponseFromChatCompletionsToClaude)(responseData);
|
|
2583
|
-
}
|
|
2584
|
-
if (this.isOpenAISource(source)) {
|
|
2585
|
-
return (0, transformers_1.transformResponseFromResponsesToClaude)(responseData);
|
|
2586
|
-
}
|
|
2587
|
-
if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
|
|
2588
|
-
return (0, transformers_1.transformResponseFromGeminiToClaude)(responseData);
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
if (tool === 'codex') {
|
|
2592
|
-
// Codex 仅支持 Responses API 格式
|
|
2593
|
-
// Codex 接收来自 OpenAI Responses 的响应
|
|
2594
|
-
if (this.isOpenAISource(source)) {
|
|
2595
|
-
return responseData;
|
|
2596
|
-
}
|
|
2597
|
-
// Codex 接收来自 OpenAI Chat 的响应(转换为 Responses 格式)
|
|
2598
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2599
|
-
return (0, transformers_1.transformResponseFromChatCompletionsToResponses)(responseData);
|
|
2600
|
-
}
|
|
2601
|
-
// Codex 接收来自 Claude 的响应(转换为 Responses 格式)
|
|
2602
|
-
if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
|
|
2603
|
-
return (0, transformers_1.transformResponseFromClaudeToResponses)(responseData);
|
|
2604
|
-
}
|
|
2605
|
-
// Codex 接收来自 Gemini 的响应(转换为 Responses 格式)
|
|
2606
|
-
if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
|
|
2607
|
-
return (0, transformers_1.transformResponseFromGeminiToResponses)(responseData);
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2785
|
+
const clientFormat = tool === 'codex' ? 'responses' : 'claude';
|
|
2786
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
2787
|
+
return (0, index_1.transformResponse)({ fromFormat: upstreamFormat, toFormat: clientFormat, response: responseData });
|
|
2610
2788
|
}
|
|
2611
2789
|
/**
|
|
2612
2790
|
* 获取流式响应转换器
|
|
2613
2791
|
* @param targetType 目标工具类型
|
|
2614
2792
|
* @param sourceType 数据源类型
|
|
2615
|
-
* @param model 模型名称
|
|
2616
2793
|
* @returns 转换器实例和相关信息
|
|
2617
2794
|
*/
|
|
2618
|
-
transformSSEToTool(targetType, sourceType
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
extractUsage: (usage) => ({
|
|
2639
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2640
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2641
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2642
|
-
}),
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
// Claude Code 接收来自 Gemini 的响应
|
|
2646
|
-
if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2647
|
-
return {
|
|
2648
|
-
converter: new streaming_1.GeminiToClaudeEventTransform({ model }),
|
|
2649
|
-
extractUsage: (usage) => ({
|
|
2650
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2651
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2652
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2653
|
-
}),
|
|
2654
|
-
};
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
// Codex 接收流式响应
|
|
2658
|
-
if (targetType === 'codex') {
|
|
2659
|
-
// Codex 接收来自 Claude 的响应
|
|
2660
|
-
if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
|
|
2661
|
-
console.log('[Proxy] Using Claude -> Responses API stream converter');
|
|
2662
|
-
return {
|
|
2663
|
-
converter: new streaming_1.ClaudeToResponsesEventTransform({ model }),
|
|
2664
|
-
extractUsage: (usage) => ({
|
|
2665
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2666
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2667
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2668
|
-
}),
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
// Codex 接收来自 OpenAI Chat 的响应
|
|
2672
|
-
if (this.isOpenAIChatSource(sourceType)) {
|
|
2673
|
-
console.log('[Proxy] Using OpenAI Chat -> Responses API stream converter');
|
|
2674
|
-
return {
|
|
2675
|
-
converter: new streaming_1.ChatCompletionsToResponsesEventTransform({ model }),
|
|
2676
|
-
extractUsage: (usage) => ({
|
|
2677
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2678
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2679
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2680
|
-
}),
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
// Codex 接收来自 Gemini 的响应
|
|
2684
|
-
if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2685
|
-
console.log('[Proxy] Using Gemini -> Responses API stream converter');
|
|
2686
|
-
return {
|
|
2687
|
-
converter: new streaming_1.GeminiToResponsesEventTransform({ model }),
|
|
2688
|
-
extractUsage: (usage) => ({
|
|
2689
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2690
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2691
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2692
|
-
}),
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
// 默认:返回空转换器(不需要转换)
|
|
2697
|
-
return { converter: null };
|
|
2795
|
+
transformSSEToTool(targetType, sourceType) {
|
|
2796
|
+
const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
|
|
2797
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(sourceType);
|
|
2798
|
+
if (upstreamFormat === clientFormat) {
|
|
2799
|
+
return { converter: null };
|
|
2800
|
+
}
|
|
2801
|
+
const streamConverter = (0, index_1.createStreamConverter)({ fromFormat: upstreamFormat, toFormat: clientFormat });
|
|
2802
|
+
const adapter = new stream_converter_adapter_1.StreamConverterAdapter(streamConverter);
|
|
2803
|
+
const extractUsage = clientFormat === 'claude'
|
|
2804
|
+
? (usage) => ({
|
|
2805
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2806
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2807
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2808
|
+
})
|
|
2809
|
+
: (usage) => ({
|
|
2810
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2811
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2812
|
+
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2813
|
+
});
|
|
2814
|
+
return { converter: adapter, extractUsage };
|
|
2698
2815
|
}
|
|
2699
2816
|
extractTokenUsageFromResponse(responseData, sourceType) {
|
|
2817
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2700
2818
|
if (!responseData)
|
|
2701
2819
|
return undefined;
|
|
2702
|
-
|
|
2703
|
-
if (
|
|
2704
|
-
|
|
2820
|
+
const format = (0, index_1.sourceTypeToFormat)(sourceType);
|
|
2821
|
+
if (format === 'gemini') {
|
|
2822
|
+
const usage = responseData === null || responseData === void 0 ? void 0 : responseData.usageMetadata;
|
|
2823
|
+
if (!usage)
|
|
2824
|
+
return undefined;
|
|
2825
|
+
return {
|
|
2826
|
+
inputTokens: usage.promptTokenCount,
|
|
2827
|
+
outputTokens: usage.candidatesTokenCount,
|
|
2828
|
+
totalTokens: usage.totalTokenCount,
|
|
2829
|
+
cacheReadInputTokens: usage.cachedContentTokenCount,
|
|
2830
|
+
};
|
|
2705
2831
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2832
|
+
if (format === 'completions' || format === 'responses') {
|
|
2833
|
+
const usage = responseData === null || responseData === void 0 ? void 0 : responseData.usage;
|
|
2834
|
+
if (!usage)
|
|
2835
|
+
return undefined;
|
|
2836
|
+
return {
|
|
2837
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) || 0,
|
|
2838
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) || 0,
|
|
2839
|
+
totalTokens: (usage === null || usage === void 0 ? void 0 : usage.total_tokens) || 0,
|
|
2840
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cached_tokens) || (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2841
|
+
};
|
|
2709
2842
|
}
|
|
2710
|
-
|
|
2711
|
-
if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
|
|
2712
|
-
// 如果 responseData 直接包含 input_tokens/output_tokens,说明它本身就是 usage 对象
|
|
2843
|
+
if (format === 'claude') {
|
|
2713
2844
|
if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) === 'number' || typeof (responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) === 'number') {
|
|
2714
|
-
return
|
|
2845
|
+
return {
|
|
2846
|
+
inputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) || 0,
|
|
2847
|
+
outputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) || 0,
|
|
2848
|
+
totalTokens: ((_a = responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) !== null && _a !== void 0 ? _a : 0) + ((_b = responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) !== null && _b !== void 0 ? _b : 0) || undefined,
|
|
2849
|
+
cacheReadInputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.cache_read_input_tokens) || 0,
|
|
2850
|
+
};
|
|
2715
2851
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2852
|
+
const usage = responseData === null || responseData === void 0 ? void 0 : responseData.usage;
|
|
2853
|
+
if (!usage)
|
|
2854
|
+
return undefined;
|
|
2855
|
+
return {
|
|
2856
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2857
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2858
|
+
totalTokens: ((_c = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _c !== void 0 ? _c : 0) + ((_d = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _d !== void 0 ? _d : 0) || undefined,
|
|
2859
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2860
|
+
};
|
|
2718
2861
|
}
|
|
2862
|
+
// 通用 fallback
|
|
2719
2863
|
const usage = responseData.usage;
|
|
2720
2864
|
if (!usage)
|
|
2721
2865
|
return undefined;
|
|
2722
|
-
// OpenAI 使用 prompt_tokens 和 completion_tokens
|
|
2723
2866
|
if (typeof (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) === 'number' || typeof (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) === 'number') {
|
|
2724
|
-
return
|
|
2867
|
+
return {
|
|
2868
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) || 0,
|
|
2869
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) || 0,
|
|
2870
|
+
totalTokens: (usage === null || usage === void 0 ? void 0 : usage.total_tokens) || 0,
|
|
2871
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2872
|
+
};
|
|
2725
2873
|
}
|
|
2726
|
-
// Claude 使用 input_tokens 和 output_tokens
|
|
2727
2874
|
if (typeof (usage === null || usage === void 0 ? void 0 : usage.input_tokens) === 'number' || typeof (usage === null || usage === void 0 ? void 0 : usage.output_tokens) === 'number') {
|
|
2728
|
-
return
|
|
2875
|
+
return {
|
|
2876
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2877
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2878
|
+
totalTokens: ((_e = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _e !== void 0 ? _e : 0) + ((_f = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _f !== void 0 ? _f : 0) || undefined,
|
|
2879
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2880
|
+
};
|
|
2729
2881
|
}
|
|
2730
2882
|
return undefined;
|
|
2731
2883
|
}
|
|
2732
2884
|
proxyRequest(req, res, route, rule, service, options) {
|
|
2733
2885
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2734
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
2886
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2735
2887
|
res.locals.skipLog = true;
|
|
2736
2888
|
const startTime = Date.now();
|
|
2737
2889
|
const rawSourceType = service.sourceType || 'openai-chat';
|
|
2738
2890
|
// 标准化 sourceType,将旧类型转换为新类型(向下兼容)
|
|
2739
2891
|
const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
|
|
2740
|
-
const targetType =
|
|
2892
|
+
const targetType = this.inferToolFromRequest(req);
|
|
2741
2893
|
const sessionId = this.defaultExtractSessionId(req, targetType) || '-';
|
|
2742
2894
|
const vendor = this.dbManager.getVendorByServiceId(service.id);
|
|
2743
2895
|
console.log(`\x1b[32m[Request Start]\x1b[0m client=${targetType}, session=${sessionId}, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}, model=${rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model) || '-'}`);
|
|
@@ -2745,10 +2897,35 @@ class ProxyServer {
|
|
|
2745
2897
|
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
2746
2898
|
const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
|
|
2747
2899
|
let relayedForLog = !useOriginalConfig;
|
|
2748
|
-
let
|
|
2900
|
+
let originalToolRequestBody = this.cloneRequestBody(req.body || {});
|
|
2901
|
+
let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
|
|
2749
2902
|
let usageForLog;
|
|
2750
2903
|
let logged = false;
|
|
2751
2904
|
const extraTagsForLog = [];
|
|
2905
|
+
// 编程套餐限制检查
|
|
2906
|
+
const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
|
|
2907
|
+
if (!this.checkCodingPlan(req, res, service, clientFormat))
|
|
2908
|
+
return;
|
|
2909
|
+
// Compact 请求消息清理:确保 tool_use/tool_result 配对完整
|
|
2910
|
+
if (rule.contentType === 'compact' && targetType === 'claude-code') {
|
|
2911
|
+
if (Array.isArray(originalToolRequestBody === null || originalToolRequestBody === void 0 ? void 0 : originalToolRequestBody.messages)) {
|
|
2912
|
+
originalToolRequestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(originalToolRequestBody.messages);
|
|
2913
|
+
if (this.isClaudeSource(sourceType)) {
|
|
2914
|
+
originalToolRequestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(originalToolRequestBody.messages);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
originalToolRequestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(originalToolRequestBody);
|
|
2918
|
+
if (Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
|
|
2919
|
+
requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
|
|
2920
|
+
if (this.isClaudeSource(sourceType)) {
|
|
2921
|
+
requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
|
|
2925
|
+
if (Array.isArray(originalToolRequestBody === null || originalToolRequestBody === void 0 ? void 0 : originalToolRequestBody.messages)) {
|
|
2926
|
+
console.log('[Compact-Sanitize] initial unpaired tool_use count:', (0, compact_1.countUnpairedClaudeToolUses)(originalToolRequestBody.messages));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2752
2929
|
// MCP 图像理解处理
|
|
2753
2930
|
let tempImageFiles = [];
|
|
2754
2931
|
let useMCPProcessing = false;
|
|
@@ -3104,8 +3281,23 @@ class ProxyServer {
|
|
|
3104
3281
|
res.once('close', onResponseClosed);
|
|
3105
3282
|
try {
|
|
3106
3283
|
// 使用统一的请求转换方法
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3284
|
+
const payloadForTransform = this.cloneRequestBody(originalToolRequestBody);
|
|
3285
|
+
// 获取 provider config 用于驱动 request body 后处理(thinking 参数注入、reasoning 历史修复等)
|
|
3286
|
+
const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
|
|
3287
|
+
const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3288
|
+
const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
|
|
3289
|
+
const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig);
|
|
3290
|
+
requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
|
|
3291
|
+
// 对最终即将发送到上游的 Claude compact 请求再做一次兜底清理,
|
|
3292
|
+
// 避免中间转换/覆盖步骤重新引入未配对的 tool_use。
|
|
3293
|
+
if (rule.contentType === 'compact' && targetType === 'claude-code' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
|
|
3294
|
+
requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
|
|
3295
|
+
if (this.isClaudeSource(sourceType)) {
|
|
3296
|
+
requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
|
|
3297
|
+
}
|
|
3298
|
+
requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
|
|
3299
|
+
console.log('[Compact-Sanitize] final unpaired tool_use count:', (0, compact_1.countUnpairedClaudeToolUses)(requestBody.messages));
|
|
3300
|
+
}
|
|
3109
3301
|
// 应用 max_output_tokens 限制
|
|
3110
3302
|
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
3111
3303
|
if (this.shouldDefaultStreamingForClaudeBridge(req, targetType, sourceType, requestBody)
|
|
@@ -3116,16 +3308,16 @@ class ProxyServer {
|
|
|
3116
3308
|
const streamRequested = this.isStreamRequested(req, requestBody, targetType, sourceType);
|
|
3117
3309
|
// Build the full URL by appending the request path to the service API URL
|
|
3118
3310
|
let pathToRequest = req.path;
|
|
3119
|
-
if (
|
|
3311
|
+
if (targetType === 'claude-code' && req.path.startsWith('/claude-code')) {
|
|
3120
3312
|
pathToRequest = req.path.slice('/claude-code'.length);
|
|
3121
3313
|
}
|
|
3122
|
-
else if (
|
|
3314
|
+
else if (targetType === 'codex' && req.path.startsWith('/codex')) {
|
|
3123
3315
|
pathToRequest = req.path.slice('/codex'.length);
|
|
3124
3316
|
}
|
|
3125
3317
|
// 使用 mapRequestPathToUpstreamUrl 统一构建上游 URL
|
|
3126
3318
|
const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3127
|
-
const apiUrl = service
|
|
3128
|
-
const upstreamUrl = this.mapRequestPathToUpstreamUrl(
|
|
3319
|
+
const apiUrl = this.resolveEffectiveApiUrl(service);
|
|
3320
|
+
const upstreamUrl = this.mapRequestPathToUpstreamUrl(targetType, sourceType, pathToRequest, apiUrl, model, streamRequested);
|
|
3129
3321
|
const config = {
|
|
3130
3322
|
method: req.method,
|
|
3131
3323
|
url: upstreamUrl,
|
|
@@ -3209,416 +3401,17 @@ class ProxyServer {
|
|
|
3209
3401
|
}
|
|
3210
3402
|
if (isEventStream && response.data) {
|
|
3211
3403
|
res.status(response.status);
|
|
3212
|
-
// 统一走默认流式链路(transformSSEToTool + 统一断连保护),避免历史分支行为不一致
|
|
3213
|
-
const useLegacySpecialSSEBranches = false;
|
|
3214
|
-
if (useLegacySpecialSSEBranches && targetType === 'claude-code' && this.isOpenAIType(sourceType)) {
|
|
3215
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3216
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3217
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3218
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3219
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3220
|
-
const converter = new streaming_1.OpenAIToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3221
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3222
|
-
// 收集响应头
|
|
3223
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3224
|
-
// 监听事件收集器的完成事件,确保所有chunks都被收集
|
|
3225
|
-
const finalizeChunks = () => {
|
|
3226
|
-
const usage = converter.getUsage();
|
|
3227
|
-
console.log('[Proxy] Claude Code stream: converter usage:', usage);
|
|
3228
|
-
if (usage) {
|
|
3229
|
-
usageForLog = (0, transformers_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
3230
|
-
console.log('[Proxy] Claude Code stream: usageForLog from converter:', usageForLog);
|
|
3231
|
-
}
|
|
3232
|
-
// 尝试从event collector中提取usage(作为补充)
|
|
3233
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3234
|
-
console.log('[Proxy] Claude Code stream: extracted usage from eventCollector:', extractedUsage);
|
|
3235
|
-
if (!usageForLog && extractedUsage) {
|
|
3236
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3237
|
-
console.log('[Proxy] Claude Code stream: usageForLog from eventCollector:', usageForLog);
|
|
3238
|
-
}
|
|
3239
|
-
// 收集stream chunks(每个chunk是一个完整的SSE事件)
|
|
3240
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3241
|
-
// 将所有 chunks 合并成完整的响应体用于日志记录
|
|
3242
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3243
|
-
console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3244
|
-
console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
3245
|
-
console.log('[Proxy] Claude Code stream: final usageForLog before finalizeLog:', usageForLog);
|
|
3246
|
-
void finalizeLog(res.statusCode);
|
|
3247
|
-
};
|
|
3248
|
-
// 在pipeline完成且eventCollector flush后执行
|
|
3249
|
-
eventCollector.on('finish', () => {
|
|
3250
|
-
console.log('[Proxy] EventCollector finished, collecting chunks...');
|
|
3251
|
-
finalizeChunks();
|
|
3252
|
-
});
|
|
3253
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3254
|
-
res.on('finish', () => {
|
|
3255
|
-
console.log('[Proxy] Response finished');
|
|
3256
|
-
if (!streamChunksForLog) {
|
|
3257
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3258
|
-
finalizeChunks();
|
|
3259
|
-
}
|
|
3260
|
-
});
|
|
3261
|
-
// 监听 res 的错误事件
|
|
3262
|
-
res.on('error', (err) => {
|
|
3263
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3264
|
-
});
|
|
3265
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3266
|
-
var _a, _b;
|
|
3267
|
-
if (error) {
|
|
3268
|
-
console.error('[Proxy] Pipeline error for claude-code:', error);
|
|
3269
|
-
// 记录到错误日志 - 包含请求详情和实际转发信息
|
|
3270
|
-
try {
|
|
3271
|
-
// 获取供应商信息
|
|
3272
|
-
const vendors = this.dbManager.getVendors();
|
|
3273
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3274
|
-
yield this.dbManager.addErrorLog({
|
|
3275
|
-
timestamp: Date.now(),
|
|
3276
|
-
method: req.method,
|
|
3277
|
-
path: req.path,
|
|
3278
|
-
statusCode: 500,
|
|
3279
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3280
|
-
errorStack: error.stack,
|
|
3281
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3282
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3283
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3284
|
-
responseHeaders: responseHeadersForLog,
|
|
3285
|
-
// 添加请求详情
|
|
3286
|
-
ruleId: rule.id,
|
|
3287
|
-
targetType,
|
|
3288
|
-
targetServiceId: service.id,
|
|
3289
|
-
targetServiceName: service.name,
|
|
3290
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3291
|
-
vendorId: service.vendorId,
|
|
3292
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3293
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3294
|
-
responseTime: Date.now() - startTime,
|
|
3295
|
-
});
|
|
3296
|
-
}
|
|
3297
|
-
catch (logError) {
|
|
3298
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3299
|
-
}
|
|
3300
|
-
// 尝试向客户端发送错误事件
|
|
3301
|
-
try {
|
|
3302
|
-
if (!res.writableEnded) {
|
|
3303
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({
|
|
3304
|
-
type: 'error',
|
|
3305
|
-
error: {
|
|
3306
|
-
type: 'api_error',
|
|
3307
|
-
message: 'Stream processing error occurred'
|
|
3308
|
-
}
|
|
3309
|
-
})}\n\n`;
|
|
3310
|
-
res.write(errorEvent);
|
|
3311
|
-
res.end();
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
catch (writeError) {
|
|
3315
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3316
|
-
}
|
|
3317
|
-
yield finalizeLog(500, error.message);
|
|
3318
|
-
}
|
|
3319
|
-
}));
|
|
3320
|
-
return;
|
|
3321
|
-
}
|
|
3322
|
-
if (useLegacySpecialSSEBranches && targetType === 'codex' && this.isClaudeSource(sourceType)) {
|
|
3323
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3324
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3325
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3326
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3327
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3328
|
-
const converter = new streaming_1.ClaudeToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3329
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3330
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3331
|
-
// 监听事件收集器的完成事件,确保所有chunks都被收集
|
|
3332
|
-
const finalizeChunks = () => {
|
|
3333
|
-
const usage = converter.getUsage();
|
|
3334
|
-
console.log('[Proxy] Codex stream: converter usage:', usage);
|
|
3335
|
-
if (usage) {
|
|
3336
|
-
usageForLog = (0, transformers_1.extractTokenUsageFromOpenAIUsage)(usage);
|
|
3337
|
-
console.log('[Proxy] Codex stream: usageForLog from converter:', usageForLog);
|
|
3338
|
-
}
|
|
3339
|
-
// 尝试从event collector中提取usage(作为补充)
|
|
3340
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3341
|
-
console.log('[Proxy] Codex stream: extracted usage from eventCollector:', extractedUsage);
|
|
3342
|
-
if (!usageForLog && extractedUsage) {
|
|
3343
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3344
|
-
console.log('[Proxy] Codex stream: usageForLog from eventCollector:', usageForLog);
|
|
3345
|
-
}
|
|
3346
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3347
|
-
// 将所有 chunks 合并成完整的响应体用于日志记录
|
|
3348
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3349
|
-
console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3350
|
-
console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
3351
|
-
console.log('[Proxy] Codex stream: final usageForLog before finalizeLog:', usageForLog);
|
|
3352
|
-
void finalizeLog(res.statusCode);
|
|
3353
|
-
};
|
|
3354
|
-
// 在pipeline完成且eventCollector flush后执行
|
|
3355
|
-
eventCollector.on('finish', () => {
|
|
3356
|
-
console.log('[Proxy] EventCollector finished (codex), collecting chunks...');
|
|
3357
|
-
finalizeChunks();
|
|
3358
|
-
});
|
|
3359
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3360
|
-
res.on('finish', () => {
|
|
3361
|
-
console.log('[Proxy] Response finished (codex)');
|
|
3362
|
-
if (!streamChunksForLog) {
|
|
3363
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3364
|
-
finalizeChunks();
|
|
3365
|
-
}
|
|
3366
|
-
});
|
|
3367
|
-
// 监听 res 的错误事件
|
|
3368
|
-
res.on('error', (err) => {
|
|
3369
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3370
|
-
});
|
|
3371
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3372
|
-
var _a, _b;
|
|
3373
|
-
if (error) {
|
|
3374
|
-
console.error('[Proxy] Pipeline error for codex:', error);
|
|
3375
|
-
// 记录到错误日志 - 包含请求详情和实际转发信息
|
|
3376
|
-
try {
|
|
3377
|
-
// 获取供应商信息
|
|
3378
|
-
const vendors = this.dbManager.getVendors();
|
|
3379
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3380
|
-
yield this.dbManager.addErrorLog({
|
|
3381
|
-
timestamp: Date.now(),
|
|
3382
|
-
method: req.method,
|
|
3383
|
-
path: req.path,
|
|
3384
|
-
statusCode: 500,
|
|
3385
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3386
|
-
errorStack: error.stack,
|
|
3387
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3388
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3389
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3390
|
-
responseHeaders: responseHeadersForLog,
|
|
3391
|
-
// 添加请求详情
|
|
3392
|
-
ruleId: rule.id,
|
|
3393
|
-
targetType,
|
|
3394
|
-
targetServiceId: service.id,
|
|
3395
|
-
targetServiceName: service.name,
|
|
3396
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3397
|
-
vendorId: service.vendorId,
|
|
3398
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3399
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3400
|
-
responseTime: Date.now() - startTime,
|
|
3401
|
-
});
|
|
3402
|
-
}
|
|
3403
|
-
catch (logError) {
|
|
3404
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3405
|
-
}
|
|
3406
|
-
// 尝试向客户端发送错误事件
|
|
3407
|
-
try {
|
|
3408
|
-
if (!res.writableEnded) {
|
|
3409
|
-
const errorEvent = `data: ${JSON.stringify({
|
|
3410
|
-
error: 'Stream processing error occurred'
|
|
3411
|
-
})}\n\n`;
|
|
3412
|
-
res.write(errorEvent);
|
|
3413
|
-
res.end();
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
catch (writeError) {
|
|
3417
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3418
|
-
}
|
|
3419
|
-
yield finalizeLog(500, error.message);
|
|
3420
|
-
}
|
|
3421
|
-
}));
|
|
3422
|
-
return;
|
|
3423
|
-
}
|
|
3424
|
-
// Gemini / Gemini Chat -> Claude Code 流式转换
|
|
3425
|
-
if (useLegacySpecialSSEBranches && targetType === 'claude-code' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
3426
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3427
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3428
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3429
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3430
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3431
|
-
const converter = new streaming_1.GeminiToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3432
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3433
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3434
|
-
const finalizeChunks = () => {
|
|
3435
|
-
const usage = converter.getUsage();
|
|
3436
|
-
if (usage) {
|
|
3437
|
-
usageForLog = {
|
|
3438
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
3439
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
3440
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
3441
|
-
};
|
|
3442
|
-
}
|
|
3443
|
-
else {
|
|
3444
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3445
|
-
if (extractedUsage) {
|
|
3446
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3450
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3451
|
-
console.log('[Proxy] Gemini stream request finished (claude-code), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3452
|
-
void finalizeLog(res.statusCode);
|
|
3453
|
-
};
|
|
3454
|
-
eventCollector.on('finish', () => {
|
|
3455
|
-
console.log('[Proxy] EventCollector finished (gemini->claude-code), collecting chunks...');
|
|
3456
|
-
finalizeChunks();
|
|
3457
|
-
});
|
|
3458
|
-
res.on('finish', () => {
|
|
3459
|
-
console.log('[Proxy] Response finished (gemini->claude-code)');
|
|
3460
|
-
if (!streamChunksForLog) {
|
|
3461
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3462
|
-
finalizeChunks();
|
|
3463
|
-
}
|
|
3464
|
-
});
|
|
3465
|
-
res.on('error', (err) => {
|
|
3466
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3467
|
-
});
|
|
3468
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3469
|
-
var _a, _b;
|
|
3470
|
-
if (error) {
|
|
3471
|
-
console.error('[Proxy] Pipeline error for gemini->claude-code:', error);
|
|
3472
|
-
try {
|
|
3473
|
-
const vendors = this.dbManager.getVendors();
|
|
3474
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3475
|
-
yield this.dbManager.addErrorLog({
|
|
3476
|
-
timestamp: Date.now(),
|
|
3477
|
-
method: req.method,
|
|
3478
|
-
path: req.path,
|
|
3479
|
-
statusCode: 500,
|
|
3480
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3481
|
-
errorStack: error.stack,
|
|
3482
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3483
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3484
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3485
|
-
responseHeaders: responseHeadersForLog,
|
|
3486
|
-
ruleId: rule.id,
|
|
3487
|
-
targetType,
|
|
3488
|
-
targetServiceId: service.id,
|
|
3489
|
-
targetServiceName: service.name,
|
|
3490
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3491
|
-
vendorId: service.vendorId,
|
|
3492
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3493
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3494
|
-
responseTime: Date.now() - startTime,
|
|
3495
|
-
});
|
|
3496
|
-
}
|
|
3497
|
-
catch (logError) {
|
|
3498
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3499
|
-
}
|
|
3500
|
-
try {
|
|
3501
|
-
if (!res.writableEnded) {
|
|
3502
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({
|
|
3503
|
-
type: 'error',
|
|
3504
|
-
error: {
|
|
3505
|
-
type: 'api_error',
|
|
3506
|
-
message: 'Stream processing error occurred'
|
|
3507
|
-
}
|
|
3508
|
-
})}\n\n`;
|
|
3509
|
-
res.write(errorEvent);
|
|
3510
|
-
res.end();
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
catch (writeError) {
|
|
3514
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3515
|
-
}
|
|
3516
|
-
yield finalizeLog(500, error.message);
|
|
3517
|
-
}
|
|
3518
|
-
}));
|
|
3519
|
-
return;
|
|
3520
|
-
}
|
|
3521
|
-
// Gemini / Gemini Chat -> Codex 流式转换
|
|
3522
|
-
if (useLegacySpecialSSEBranches && targetType === 'codex' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
3523
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3524
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3525
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3526
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3527
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3528
|
-
const converter = new streaming_1.GeminiToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3529
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3530
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3531
|
-
const finalizeChunks = () => {
|
|
3532
|
-
const usage = converter.getUsage();
|
|
3533
|
-
if (usage) {
|
|
3534
|
-
usageForLog = {
|
|
3535
|
-
inputTokens: usage.prompt_tokens,
|
|
3536
|
-
outputTokens: usage.completion_tokens,
|
|
3537
|
-
totalTokens: usage.total_tokens,
|
|
3538
|
-
};
|
|
3539
|
-
}
|
|
3540
|
-
else {
|
|
3541
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3542
|
-
if (extractedUsage) {
|
|
3543
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3547
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3548
|
-
console.log('[Proxy] Gemini stream request finished (codex), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3549
|
-
void finalizeLog(res.statusCode);
|
|
3550
|
-
};
|
|
3551
|
-
eventCollector.on('finish', () => {
|
|
3552
|
-
console.log('[Proxy] EventCollector finished (gemini->codex), collecting chunks...');
|
|
3553
|
-
finalizeChunks();
|
|
3554
|
-
});
|
|
3555
|
-
res.on('finish', () => {
|
|
3556
|
-
console.log('[Proxy] Response finished (gemini->codex)');
|
|
3557
|
-
if (!streamChunksForLog) {
|
|
3558
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3559
|
-
finalizeChunks();
|
|
3560
|
-
}
|
|
3561
|
-
});
|
|
3562
|
-
res.on('error', (err) => {
|
|
3563
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3564
|
-
});
|
|
3565
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3566
|
-
var _a, _b;
|
|
3567
|
-
if (error) {
|
|
3568
|
-
console.error('[Proxy] Pipeline error for gemini->codex:', error);
|
|
3569
|
-
try {
|
|
3570
|
-
const vendors = this.dbManager.getVendors();
|
|
3571
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3572
|
-
yield this.dbManager.addErrorLog({
|
|
3573
|
-
timestamp: Date.now(),
|
|
3574
|
-
method: req.method,
|
|
3575
|
-
path: req.path,
|
|
3576
|
-
statusCode: 500,
|
|
3577
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3578
|
-
errorStack: error.stack,
|
|
3579
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3580
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3581
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3582
|
-
responseHeaders: responseHeadersForLog,
|
|
3583
|
-
ruleId: rule.id,
|
|
3584
|
-
targetType,
|
|
3585
|
-
targetServiceId: service.id,
|
|
3586
|
-
targetServiceName: service.name,
|
|
3587
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3588
|
-
vendorId: service.vendorId,
|
|
3589
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3590
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3591
|
-
responseTime: Date.now() - startTime,
|
|
3592
|
-
});
|
|
3593
|
-
}
|
|
3594
|
-
catch (logError) {
|
|
3595
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3596
|
-
}
|
|
3597
|
-
try {
|
|
3598
|
-
if (!res.writableEnded) {
|
|
3599
|
-
const errorEvent = `data: ${JSON.stringify({
|
|
3600
|
-
error: 'Stream processing error occurred'
|
|
3601
|
-
})}\n\n`;
|
|
3602
|
-
res.write(errorEvent);
|
|
3603
|
-
res.end();
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
catch (writeError) {
|
|
3607
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3608
|
-
}
|
|
3609
|
-
yield finalizeLog(500, error.message);
|
|
3610
|
-
}
|
|
3611
|
-
}));
|
|
3612
|
-
return;
|
|
3613
|
-
}
|
|
3614
3404
|
// 默认stream处理(无转换)
|
|
3615
3405
|
const parser = new streaming_1.SSEParserTransform();
|
|
3616
3406
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3617
3407
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
3618
3408
|
const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
|
|
3409
|
+
const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
|
|
3410
|
+
? new ClaudeCompactResponseSanitizer()
|
|
3411
|
+
: null;
|
|
3619
3412
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3620
3413
|
// 使用 transformSSEToTool 方法选择转换器
|
|
3621
|
-
const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType
|
|
3414
|
+
const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
|
|
3622
3415
|
this.copyResponseHeaders(responseHeaders, res);
|
|
3623
3416
|
// 收集日志:responseBody/streamChunks 记录上游原始响应;downstreamResponseBody 记录实际下发内容
|
|
3624
3417
|
const finalizeChunks = () => {
|
|
@@ -3642,180 +3435,166 @@ class ProxyServer {
|
|
|
3642
3435
|
else if (extractedUsage) {
|
|
3643
3436
|
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3644
3437
|
}
|
|
3645
|
-
void finalizeLog(res.statusCode);
|
|
3646
3438
|
};
|
|
3647
|
-
// 在下游 chunk 收集器完成后执行,确保拿到真正下发给客户端的完整文本
|
|
3648
|
-
downstreamChunkCollector.on('finish', () => {
|
|
3649
|
-
finalizeChunks();
|
|
3650
|
-
});
|
|
3651
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3652
|
-
res.on('finish', () => {
|
|
3653
|
-
if (!streamChunksForLog) {
|
|
3654
|
-
finalizeChunks();
|
|
3655
|
-
}
|
|
3656
|
-
});
|
|
3657
3439
|
// 监听 res 的错误事件
|
|
3658
3440
|
res.on('error', (err) => {
|
|
3659
3441
|
console.error('[Proxy] Response stream error:', err);
|
|
3660
3442
|
});
|
|
3661
|
-
|
|
3662
|
-
if (converter) {
|
|
3443
|
+
const runStreamPipeline = () => __awaiter(this, void 0, void 0, function* () {
|
|
3663
3444
|
ensureResponseWritable();
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
if (
|
|
3668
|
-
|
|
3669
|
-
yield finalizeLog(499, 'Client disconnected');
|
|
3670
|
-
return;
|
|
3445
|
+
return yield new Promise((resolve, reject) => {
|
|
3446
|
+
if (converter) {
|
|
3447
|
+
const streamStages = [response.data, parser, eventCollector, converter];
|
|
3448
|
+
if (compactResponseSanitizer) {
|
|
3449
|
+
streamStages.push(compactResponseSanitizer);
|
|
3671
3450
|
}
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
errorStack: error.stack,
|
|
3682
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3683
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3684
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3685
|
-
responseHeaders: responseHeadersForLog,
|
|
3686
|
-
ruleId: rule.id,
|
|
3687
|
-
targetType,
|
|
3688
|
-
targetServiceId: service.id,
|
|
3689
|
-
targetServiceName: service.name,
|
|
3690
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3691
|
-
vendorId: service.vendorId,
|
|
3692
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3693
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3694
|
-
responseTime: Date.now() - startTime,
|
|
3695
|
-
});
|
|
3696
|
-
}
|
|
3697
|
-
catch (logError) {
|
|
3698
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3699
|
-
}
|
|
3700
|
-
yield finalizeLog(500, error.message);
|
|
3451
|
+
streamStages.push(serializer, downstreamChunkCollector, res);
|
|
3452
|
+
(0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], streamStages[3], ...streamStages.slice(4), (error) => {
|
|
3453
|
+
if (error) {
|
|
3454
|
+
reject(error);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
resolve();
|
|
3458
|
+
});
|
|
3459
|
+
return;
|
|
3701
3460
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
console.warn('[Proxy] Default stream pipeline closed because client disconnected');
|
|
3711
|
-
yield finalizeLog(499, 'Client disconnected');
|
|
3461
|
+
const streamStages = [response.data, parser, eventCollector];
|
|
3462
|
+
if (compactResponseSanitizer) {
|
|
3463
|
+
streamStages.push(compactResponseSanitizer);
|
|
3464
|
+
}
|
|
3465
|
+
streamStages.push(serializer, downstreamChunkCollector, res);
|
|
3466
|
+
(0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], ...streamStages.slice(3), (error) => {
|
|
3467
|
+
if (error) {
|
|
3468
|
+
reject(error);
|
|
3712
3469
|
return;
|
|
3713
3470
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
path: req.path,
|
|
3721
|
-
statusCode: 500,
|
|
3722
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3723
|
-
errorStack: error.stack,
|
|
3724
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3725
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3726
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3727
|
-
responseHeaders: responseHeadersForLog,
|
|
3728
|
-
ruleId: rule.id,
|
|
3729
|
-
targetType,
|
|
3730
|
-
targetServiceId: service.id,
|
|
3731
|
-
targetServiceName: service.name,
|
|
3732
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3733
|
-
vendorId: service.vendorId,
|
|
3734
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3735
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3736
|
-
responseTime: Date.now() - startTime,
|
|
3737
|
-
});
|
|
3738
|
-
}
|
|
3739
|
-
catch (logError) {
|
|
3740
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3741
|
-
}
|
|
3742
|
-
yield finalizeLog(500, error.message);
|
|
3743
|
-
}
|
|
3744
|
-
}));
|
|
3471
|
+
resolve();
|
|
3472
|
+
});
|
|
3473
|
+
});
|
|
3474
|
+
});
|
|
3475
|
+
try {
|
|
3476
|
+
yield runStreamPipeline();
|
|
3745
3477
|
}
|
|
3478
|
+
catch (error) {
|
|
3479
|
+
if (this.isClientDisconnectError(error, res)) {
|
|
3480
|
+
console.warn('[Proxy] Default stream pipeline closed because client disconnected');
|
|
3481
|
+
yield finalizeLog(499, 'Client disconnected');
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
console.error('[Proxy] Pipeline error (default stream):', error);
|
|
3485
|
+
// 记录到错误日志
|
|
3486
|
+
try {
|
|
3487
|
+
yield this.dbManager.addErrorLog({
|
|
3488
|
+
timestamp: Date.now(),
|
|
3489
|
+
method: req.method,
|
|
3490
|
+
path: req.path,
|
|
3491
|
+
statusCode: 500,
|
|
3492
|
+
errorMessage: error.message || 'Stream processing error',
|
|
3493
|
+
errorStack: error.stack,
|
|
3494
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3495
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3496
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3497
|
+
responseHeaders: responseHeadersForLog,
|
|
3498
|
+
ruleId: rule.id,
|
|
3499
|
+
targetType,
|
|
3500
|
+
targetServiceId: service.id,
|
|
3501
|
+
targetServiceName: service.name,
|
|
3502
|
+
targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
3503
|
+
vendorId: service.vendorId,
|
|
3504
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3505
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
3506
|
+
responseTime: Date.now() - startTime,
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
catch (logError) {
|
|
3510
|
+
console.error('[Proxy] Failed to log error:', logError);
|
|
3511
|
+
}
|
|
3512
|
+
yield finalizeLog(500, error.message);
|
|
3513
|
+
if (failoverEnabled && !this.isResponseCommitted(res)) {
|
|
3514
|
+
throw this.createFailoverError(error.message || 'Stream processing error', 500, error);
|
|
3515
|
+
}
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
finalizeChunks();
|
|
3519
|
+
// 检测空流:上游返回 SSE Content-Type 但没有发送任何事件数据
|
|
3520
|
+
const collectedEvents = eventCollector.getEvents();
|
|
3521
|
+
if (collectedEvents.length === 0) {
|
|
3522
|
+
const emptyStreamMsg = 'Upstream API returned an empty stream (HTTP 200, no SSE events)';
|
|
3523
|
+
console.warn(`[Proxy] ${emptyStreamMsg}`);
|
|
3524
|
+
yield finalizeLog(200, emptyStreamMsg);
|
|
3525
|
+
if (!res.writableEnded) {
|
|
3526
|
+
res.end();
|
|
3527
|
+
}
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
// 关键修复:识别 stream 内部的 response.failed / error 事件,归类为错误并触发 failover 交接
|
|
3531
|
+
const streamFailure = this.detectStreamFailure(collectedEvents);
|
|
3532
|
+
if (streamFailure) {
|
|
3533
|
+
try {
|
|
3534
|
+
yield this.dbManager.addErrorLog({
|
|
3535
|
+
timestamp: Date.now(),
|
|
3536
|
+
method: req.method,
|
|
3537
|
+
path: req.path,
|
|
3538
|
+
statusCode: streamFailure.statusCode,
|
|
3539
|
+
errorMessage: streamFailure.errorMessage,
|
|
3540
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3541
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3542
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3543
|
+
responseHeaders: responseHeadersForLog,
|
|
3544
|
+
responseBody: responseBodyForLog,
|
|
3545
|
+
ruleId: rule.id,
|
|
3546
|
+
targetType,
|
|
3547
|
+
targetServiceId: service.id,
|
|
3548
|
+
targetServiceName: service.name,
|
|
3549
|
+
targetModel: rule.targetModel || ((_f = req.body) === null || _f === void 0 ? void 0 : _f.model),
|
|
3550
|
+
vendorId: service.vendorId,
|
|
3551
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3552
|
+
requestModel: (_g = req.body) === null || _g === void 0 ? void 0 : _g.model,
|
|
3553
|
+
responseTime: Date.now() - startTime,
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
catch (logError) {
|
|
3557
|
+
console.error('[Proxy] Failed to log stream failure:', logError);
|
|
3558
|
+
}
|
|
3559
|
+
yield finalizeLog(streamFailure.statusCode, streamFailure.errorMessage);
|
|
3560
|
+
if (failoverEnabled && !this.isResponseCommitted(res)) {
|
|
3561
|
+
throw this.createFailoverError(streamFailure.errorMessage, streamFailure.statusCode);
|
|
3562
|
+
}
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
yield finalizeLog(res.statusCode);
|
|
3746
3566
|
return;
|
|
3747
3567
|
}
|
|
3748
3568
|
let responseData = response.data;
|
|
3749
3569
|
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
3750
3570
|
const raw = yield this.readStreamBody(response.data);
|
|
3751
|
-
responseData = (
|
|
3571
|
+
responseData = (_h = this.safeJsonParse(raw)) !== null && _h !== void 0 ? _h : raw;
|
|
3752
3572
|
}
|
|
3753
3573
|
// 收集响应头
|
|
3754
3574
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3755
|
-
// 检测上游空响应(HTTP 200 但 body
|
|
3575
|
+
// 检测上游空响应(HTTP 200 但 body 为空)— 透传 200
|
|
3756
3576
|
if (this.isEmptyResponse(responseData)) {
|
|
3757
|
-
const
|
|
3758
|
-
console.warn(`[Proxy] ${
|
|
3759
|
-
if (failoverEnabled) {
|
|
3760
|
-
throw this.createFailoverError(emptyErrorMsg, 502);
|
|
3761
|
-
}
|
|
3577
|
+
const emptyInfoMsg = 'Upstream API returned an empty response (HTTP 200), passing through';
|
|
3578
|
+
console.warn(`[Proxy] ${emptyInfoMsg}`);
|
|
3762
3579
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
yield this.dbManager.addErrorLog({
|
|
3766
|
-
timestamp: Date.now(),
|
|
3767
|
-
method: req.method,
|
|
3768
|
-
path: req.path,
|
|
3769
|
-
statusCode: 502,
|
|
3770
|
-
errorMessage: emptyErrorMsg,
|
|
3771
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3772
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3773
|
-
ruleId: rule.id,
|
|
3774
|
-
targetType,
|
|
3775
|
-
targetServiceId: service.id,
|
|
3776
|
-
targetServiceName: service.name,
|
|
3777
|
-
targetModel: rule.targetModel || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
|
|
3778
|
-
vendorId: service.vendorId,
|
|
3779
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3780
|
-
requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
|
|
3781
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3782
|
-
responseHeaders: responseHeadersForLog,
|
|
3783
|
-
responseTime: Date.now() - startTime,
|
|
3784
|
-
});
|
|
3785
|
-
yield finalizeLog(502, emptyErrorMsg);
|
|
3786
|
-
if (route.targetType === 'claude-code') {
|
|
3787
|
-
const claudeError = {
|
|
3788
|
-
type: 'error',
|
|
3789
|
-
error: { type: 'api_error', message: emptyErrorMsg }
|
|
3790
|
-
};
|
|
3791
|
-
if (streamRequested) {
|
|
3792
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3793
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3794
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3795
|
-
res.status(200);
|
|
3796
|
-
res.write(`event: error\ndata: ${JSON.stringify(claudeError)}\n\n`);
|
|
3797
|
-
res.end();
|
|
3798
|
-
}
|
|
3799
|
-
else {
|
|
3800
|
-
res.status(502).json(claudeError);
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
else {
|
|
3804
|
-
res.status(502).json({ error: emptyErrorMsg });
|
|
3805
|
-
}
|
|
3580
|
+
yield finalizeLog(200, emptyInfoMsg);
|
|
3581
|
+
res.status(200).end();
|
|
3806
3582
|
return;
|
|
3807
3583
|
}
|
|
3808
3584
|
// 使用统一的响应转换方法
|
|
3809
3585
|
const converted = this.transformResponseToTool(targetType, sourceType, responseData);
|
|
3586
|
+
const normalizedConverted = rule.contentType === 'compact' && targetType === 'claude-code'
|
|
3587
|
+
? (0, compact_1.stripClaudeCompactResponseContent)(converted)
|
|
3588
|
+
: converted;
|
|
3810
3589
|
// 提取 token usage(从原始响应数据中提取)
|
|
3811
3590
|
usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
|
|
3812
3591
|
console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
|
|
3813
3592
|
this.copyResponseHeaders(responseHeaders, res);
|
|
3814
|
-
if (
|
|
3593
|
+
if (normalizedConverted && normalizedConverted !== responseData) {
|
|
3815
3594
|
// 非流式:responseBody 记录上游原始响应,downstreamResponseBody 记录转换后下发内容
|
|
3816
3595
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
3817
|
-
downstreamResponseBodyForLog = JSON.stringify(
|
|
3818
|
-
res.status(response.status).json(
|
|
3596
|
+
downstreamResponseBodyForLog = JSON.stringify(normalizedConverted);
|
|
3597
|
+
res.status(response.status).json(normalizedConverted);
|
|
3819
3598
|
}
|
|
3820
3599
|
else {
|
|
3821
3600
|
// 没有转换,使用原始数据
|
|
@@ -3872,10 +3651,10 @@ class ProxyServer {
|
|
|
3872
3651
|
targetType,
|
|
3873
3652
|
targetServiceId: service.id,
|
|
3874
3653
|
targetServiceName: service.name,
|
|
3875
|
-
targetModel: rule.targetModel || ((
|
|
3654
|
+
targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
|
|
3876
3655
|
vendorId: service.vendorId,
|
|
3877
3656
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3878
|
-
requestModel: (
|
|
3657
|
+
requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
|
|
3879
3658
|
upstreamRequest: upstreamRequestForLog,
|
|
3880
3659
|
responseTime: Date.now() - startTime,
|
|
3881
3660
|
});
|
|
@@ -3890,8 +3669,9 @@ class ProxyServer {
|
|
|
3890
3669
|
console.error('Proxy error:', error);
|
|
3891
3670
|
// 检测是否是 timeout 错误
|
|
3892
3671
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
3893
|
-
((
|
|
3672
|
+
((_l = error.message) === null || _l === void 0 ? void 0 : _l.toLowerCase().includes('timeout')) ||
|
|
3894
3673
|
(error.errno && error.errno === 'ETIMEDOUT');
|
|
3674
|
+
const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
|
|
3895
3675
|
const baseErrorMessage = isTimeout
|
|
3896
3676
|
? 'Request timeout - the upstream API took too long to respond'
|
|
3897
3677
|
: (error.message || 'Internal server error');
|
|
@@ -3905,7 +3685,7 @@ class ProxyServer {
|
|
|
3905
3685
|
timestamp: Date.now(),
|
|
3906
3686
|
method: req.method,
|
|
3907
3687
|
path: req.path,
|
|
3908
|
-
statusCode
|
|
3688
|
+
statusCode,
|
|
3909
3689
|
errorMessage: errorMessage,
|
|
3910
3690
|
errorStack: error.stack,
|
|
3911
3691
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
@@ -3915,24 +3695,24 @@ class ProxyServer {
|
|
|
3915
3695
|
targetType,
|
|
3916
3696
|
targetServiceId: service.id,
|
|
3917
3697
|
targetServiceName: service.name,
|
|
3918
|
-
targetModel: rule.targetModel || ((
|
|
3698
|
+
targetModel: rule.targetModel || ((_m = req.body) === null || _m === void 0 ? void 0 : _m.model),
|
|
3919
3699
|
vendorId: service.vendorId,
|
|
3920
3700
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3921
|
-
requestModel: (
|
|
3701
|
+
requestModel: (_o = req.body) === null || _o === void 0 ? void 0 : _o.model,
|
|
3922
3702
|
upstreamRequest: upstreamRequestForLog,
|
|
3923
3703
|
responseHeaders: responseHeadersForLog,
|
|
3924
3704
|
responseTime: Date.now() - startTime,
|
|
3925
3705
|
});
|
|
3926
|
-
yield finalizeLog(
|
|
3706
|
+
yield finalizeLog(statusCode, errorMessage);
|
|
3927
3707
|
if (failoverEnabled) {
|
|
3928
|
-
throw this.createFailoverError(errorMessage,
|
|
3708
|
+
throw this.createFailoverError(errorMessage, statusCode, error);
|
|
3929
3709
|
}
|
|
3930
3710
|
if (this.isResponseCommitted(res)) {
|
|
3931
3711
|
return;
|
|
3932
3712
|
}
|
|
3933
3713
|
// 根据请求类型返回适当格式的错误响应
|
|
3934
3714
|
const streamRequested = this.isStreamRequested(req, req.body || {}, targetType, sourceType);
|
|
3935
|
-
if (
|
|
3715
|
+
if (targetType === 'claude-code') {
|
|
3936
3716
|
// 对于 Claude Code,返回符合 Claude API 标准的错误响应
|
|
3937
3717
|
const claudeError = {
|
|
3938
3718
|
type: 'error',
|
|
@@ -3954,12 +3734,12 @@ class ProxyServer {
|
|
|
3954
3734
|
}
|
|
3955
3735
|
else {
|
|
3956
3736
|
// 非流式请求:返回 JSON 格式
|
|
3957
|
-
res.status(
|
|
3737
|
+
res.status(statusCode).json(claudeError);
|
|
3958
3738
|
}
|
|
3959
3739
|
}
|
|
3960
3740
|
else {
|
|
3961
3741
|
// 对于 Codex,返回 JSON 格式的错误响应
|
|
3962
|
-
res.status(
|
|
3742
|
+
res.status(statusCode).json({ error: errorMessage });
|
|
3963
3743
|
}
|
|
3964
3744
|
}
|
|
3965
3745
|
finally {
|
|
@@ -3974,13 +3754,13 @@ class ProxyServer {
|
|
|
3974
3754
|
// 这个方法主要用于初始化和日志记录
|
|
3975
3755
|
// 修改数据库后无需调用此方法,配置会自动生效
|
|
3976
3756
|
const allRoutes = this.dbManager.getRoutes();
|
|
3977
|
-
const
|
|
3757
|
+
const allRoutesList = allRoutes;
|
|
3978
3758
|
const allServices = this.dbManager.getAPIServices();
|
|
3979
3759
|
// 保留缓存以备将来可能的性能优化需求
|
|
3980
|
-
this.routes =
|
|
3760
|
+
this.routes = allRoutesList;
|
|
3981
3761
|
if (this.rules) {
|
|
3982
3762
|
this.rules.clear();
|
|
3983
|
-
for (const route of
|
|
3763
|
+
for (const route of allRoutesList) {
|
|
3984
3764
|
const routeRules = this.dbManager.getRules(route.id);
|
|
3985
3765
|
const sortedRules = [...routeRules].sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
|
|
3986
3766
|
this.rules.set(route.id, sortedRules);
|
|
@@ -3993,7 +3773,7 @@ class ProxyServer {
|
|
|
3993
3773
|
services.set(service.id, service);
|
|
3994
3774
|
});
|
|
3995
3775
|
}
|
|
3996
|
-
console.log(`Initialized with ${
|
|
3776
|
+
console.log(`Initialized with ${allRoutesList.length} routes and ${allServices.length} services (all config read from database in real-time)`);
|
|
3997
3777
|
});
|
|
3998
3778
|
}
|
|
3999
3779
|
updateConfig(config) {
|
|
@@ -4007,5 +3787,523 @@ class ProxyServer {
|
|
|
4007
3787
|
yield this.reloadRoutes();
|
|
4008
3788
|
});
|
|
4009
3789
|
}
|
|
3790
|
+
// ============================================================
|
|
3791
|
+
// 标准 API 路径代理请求处理
|
|
3792
|
+
// ============================================================
|
|
3793
|
+
/**
|
|
3794
|
+
* 处理通过标准 API 路径(/v1/messages, /v1/responses 等)进入的代理请求。
|
|
3795
|
+
* 与原有 proxyRequest 逻辑独立,复用规则匹配、故障切换等机制。
|
|
3796
|
+
*/
|
|
3797
|
+
handleApiPathProxyRequest(req, res, route, clientFormat, apiPath) {
|
|
3798
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
3799
|
+
var _a, _b;
|
|
3800
|
+
const requestStartAt = Date.now();
|
|
3801
|
+
// 鉴权
|
|
3802
|
+
if (this.config.apiKey) {
|
|
3803
|
+
const authHeader = req.headers.authorization;
|
|
3804
|
+
const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
|
|
3805
|
+
if (!providedKey || providedKey !== this.config.apiKey) {
|
|
3806
|
+
yield this.logToolRequest(req, {
|
|
3807
|
+
statusCode: 401,
|
|
3808
|
+
responseTime: Date.now() - requestStartAt,
|
|
3809
|
+
error: 'Invalid API key',
|
|
3810
|
+
tags: this.buildRelayTags(false),
|
|
3811
|
+
});
|
|
3812
|
+
// 根据客户端格式返回错误
|
|
3813
|
+
if (clientFormat === 'claude') {
|
|
3814
|
+
res.status(401).json({ type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } });
|
|
3815
|
+
}
|
|
3816
|
+
else {
|
|
3817
|
+
res.status(401).json({ error: { message: 'Invalid API key' } });
|
|
3818
|
+
}
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false;
|
|
3823
|
+
if (!enableFailover) {
|
|
3824
|
+
const rule = yield this.findMatchingRule(route.id, req);
|
|
3825
|
+
if (!rule) {
|
|
3826
|
+
return res.status(404).json({ error: { message: 'No matching rule found' } });
|
|
3827
|
+
}
|
|
3828
|
+
const service = this.getServiceById(rule.targetServiceId);
|
|
3829
|
+
if (!service) {
|
|
3830
|
+
return res.status(500).json({ error: { message: 'Target service not configured' } });
|
|
3831
|
+
}
|
|
3832
|
+
try {
|
|
3833
|
+
yield this.proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath);
|
|
3834
|
+
}
|
|
3835
|
+
catch (error) {
|
|
3836
|
+
console.error('[ApiPathProxy] Error:', error.message);
|
|
3837
|
+
this.sendFormatError(res, clientFormat, 500, error.message);
|
|
3838
|
+
}
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
// 故障切换模式
|
|
3842
|
+
const allRules = this.getAllMatchingRules(route.id, req);
|
|
3843
|
+
if (allRules.length === 0) {
|
|
3844
|
+
return res.status(404).json({ error: { message: 'No matching rule found' } });
|
|
3845
|
+
}
|
|
3846
|
+
let lastError = null;
|
|
3847
|
+
let lastFailedRule = null;
|
|
3848
|
+
let lastFailedService = null;
|
|
3849
|
+
for (let i = 0; i < allRules.length; i++) {
|
|
3850
|
+
const rule = allRules[i];
|
|
3851
|
+
const service = this.getServiceById(rule.targetServiceId);
|
|
3852
|
+
if (!service)
|
|
3853
|
+
continue;
|
|
3854
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, route.id, rule.contentType);
|
|
3855
|
+
if (isBlacklisted) {
|
|
3856
|
+
console.log(`[ApiPathProxy] Service ${service.name} is blacklisted, skipping...`);
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
try {
|
|
3860
|
+
const nextServiceName = yield this.findNextAvailableServiceName(allRules, i + 1, route.id);
|
|
3861
|
+
yield this.proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, {
|
|
3862
|
+
failoverEnabled: true,
|
|
3863
|
+
forwardedToServiceName: nextServiceName,
|
|
3864
|
+
});
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
catch (error) {
|
|
3868
|
+
console.error(`[ApiPathProxy] Service ${service.name} failed:`, error.message);
|
|
3869
|
+
lastError = error;
|
|
3870
|
+
lastFailedRule = rule;
|
|
3871
|
+
lastFailedService = service;
|
|
3872
|
+
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
3873
|
+
((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
|
|
3874
|
+
error.errno === 'ETIMEDOUT';
|
|
3875
|
+
if (isTimeout) {
|
|
3876
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout', undefined, 'timeout');
|
|
3877
|
+
rules_status_service_1.rulesStatusBroadcaster.markRuleSuspended(route.id, rule.id, service.id, rule.contentType, '请求超时', 'timeout');
|
|
3878
|
+
}
|
|
3879
|
+
else {
|
|
3880
|
+
const statusCode = this.getErrorStatusCode(error, 500);
|
|
3881
|
+
if (statusCode >= 400) {
|
|
3882
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
3883
|
+
rules_status_service_1.rulesStatusBroadcaster.markRuleSuspended(route.id, rule.id, service.id, rule.contentType, `HTTP ${statusCode} 错误`, 'http');
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
// Fallback: try the last failed service
|
|
3890
|
+
if (lastFailedRule && lastFailedService) {
|
|
3891
|
+
try {
|
|
3892
|
+
yield this.proxyRequestForApiPath(req, res, route, lastFailedRule, lastFailedService, clientFormat, apiPath, {
|
|
3893
|
+
failoverEnabled: false,
|
|
3894
|
+
});
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
catch (fallbackError) {
|
|
3898
|
+
lastError = fallbackError;
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
yield this.logToolRequest(req, {
|
|
3902
|
+
statusCode: 503,
|
|
3903
|
+
responseTime: Date.now() - requestStartAt,
|
|
3904
|
+
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
3905
|
+
tags: this.buildRelayTags(true),
|
|
3906
|
+
});
|
|
3907
|
+
this.sendFormatError(res, clientFormat, 503, (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed');
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* 对单个规则执行代理请求(标准 API 路径入口)。
|
|
3912
|
+
* 与原有 proxyRequest 类似,但使用 clientFormat 而非 targetType 推断格式。
|
|
3913
|
+
*/
|
|
3914
|
+
proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, options) {
|
|
3915
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
3916
|
+
var _a, _b, _c, _d;
|
|
3917
|
+
const startTime = Date.now();
|
|
3918
|
+
const rawSourceType = service.sourceType || 'openai-chat';
|
|
3919
|
+
const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
|
|
3920
|
+
const vendor = this.dbManager.getVendorByServiceId(service.id);
|
|
3921
|
+
console.log(`\x1b[32m[ApiPathProxy]\x1b[0m path=${apiPath}, clientFormat=${clientFormat}, session=-, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}`);
|
|
3922
|
+
const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
|
|
3923
|
+
let requestBody = this.cloneRequestBody(req.body || {});
|
|
3924
|
+
let usageForLog;
|
|
3925
|
+
let responseBodyForLog;
|
|
3926
|
+
let downstreamResponseBodyForLog;
|
|
3927
|
+
let streamChunksForLog;
|
|
3928
|
+
let responseHeadersForLog;
|
|
3929
|
+
let upstreamRequestForLog;
|
|
3930
|
+
let relayedForLog = true;
|
|
3931
|
+
void downstreamResponseBodyForLog;
|
|
3932
|
+
void streamChunksForLog;
|
|
3933
|
+
void responseHeadersForLog;
|
|
3934
|
+
void upstreamRequestForLog;
|
|
3935
|
+
// 编程套餐限制检查
|
|
3936
|
+
if (!this.checkCodingPlan(req, res, service, clientFormat))
|
|
3937
|
+
return;
|
|
3938
|
+
// Compact 处理(针对 claude 格式的 compact)
|
|
3939
|
+
if (rule.contentType === 'compact' && clientFormat === 'claude') {
|
|
3940
|
+
if (Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
|
|
3941
|
+
requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
|
|
3942
|
+
if (this.isClaudeSource(sourceType)) {
|
|
3943
|
+
requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
|
|
3947
|
+
}
|
|
3948
|
+
const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
|
|
3949
|
+
if (logged)
|
|
3950
|
+
return;
|
|
3951
|
+
logged = true;
|
|
3952
|
+
yield this.logToolRequest(req, {
|
|
3953
|
+
statusCode,
|
|
3954
|
+
responseTime: Date.now() - startTime,
|
|
3955
|
+
usage: usageForLog,
|
|
3956
|
+
error,
|
|
3957
|
+
tags: this.buildRelayTags(relayedForLog),
|
|
3958
|
+
});
|
|
3959
|
+
});
|
|
3960
|
+
let logged = false;
|
|
3961
|
+
// count_tokens 本地处理
|
|
3962
|
+
if (this.isCountTokensPath(req.path)) {
|
|
3963
|
+
const inputTokens = this.estimateClaudeCountTokens(requestBody);
|
|
3964
|
+
const localTokenResponse = { input_tokens: inputTokens };
|
|
3965
|
+
usageForLog = { inputTokens, outputTokens: 0, totalTokens: inputTokens };
|
|
3966
|
+
res.status(200).json(localTokenResponse);
|
|
3967
|
+
yield finalizeLog(200);
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
// 请求转换:使用 clientFormat 而非硬编码的 tool→format 映射
|
|
3971
|
+
const payloadForTransform = this.cloneRequestBody(requestBody);
|
|
3972
|
+
const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
|
|
3973
|
+
const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3974
|
+
const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
|
|
3975
|
+
const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig);
|
|
3976
|
+
requestBody = (_a = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(requestBody)) !== null && _a !== void 0 ? _a : {};
|
|
3977
|
+
// Compact final sanitize
|
|
3978
|
+
if (rule.contentType === 'compact' && clientFormat === 'claude' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
|
|
3979
|
+
requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
|
|
3980
|
+
if (this.isClaudeSource(sourceType)) {
|
|
3981
|
+
requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
|
|
3982
|
+
}
|
|
3983
|
+
requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
|
|
3984
|
+
}
|
|
3985
|
+
// 应用 max_output_tokens 限制
|
|
3986
|
+
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
3987
|
+
// Stream 判断
|
|
3988
|
+
const streamRequested = this.isStreamRequested(req, requestBody);
|
|
3989
|
+
// 构建上游 URL
|
|
3990
|
+
const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3991
|
+
const apiUrl = this.resolveEffectiveApiUrl(service);
|
|
3992
|
+
const upstreamUrl = this.mapApiPathToUpstreamUrl(apiPath, sourceType, apiUrl, model, streamRequested);
|
|
3993
|
+
upstreamRequestForLog = { url: upstreamUrl, body: requestBody || undefined };
|
|
3994
|
+
const upstreamHeaders = this.buildUpstreamHeaders(req, service, sourceType, streamRequested, requestBody);
|
|
3995
|
+
const upstreamAbortController = new AbortController();
|
|
3996
|
+
const abortUpstreamRequest = (reason) => {
|
|
3997
|
+
if (!upstreamAbortController.signal.aborted) {
|
|
3998
|
+
upstreamAbortController.abort(new Error(`Client disconnected: ${reason}`));
|
|
3999
|
+
}
|
|
4000
|
+
};
|
|
4001
|
+
req.once('aborted', () => abortUpstreamRequest('request aborted'));
|
|
4002
|
+
res.once('close', () => {
|
|
4003
|
+
if (!res.writableEnded)
|
|
4004
|
+
abortUpstreamRequest('response stream closed');
|
|
4005
|
+
});
|
|
4006
|
+
try {
|
|
4007
|
+
const axiosConfig = {
|
|
4008
|
+
method: req.method,
|
|
4009
|
+
url: upstreamUrl,
|
|
4010
|
+
headers: upstreamHeaders,
|
|
4011
|
+
timeout: this.resolveEffectiveTimeout(rule),
|
|
4012
|
+
validateStatus: () => true,
|
|
4013
|
+
responseType: streamRequested ? 'stream' : 'json',
|
|
4014
|
+
signal: upstreamAbortController.signal,
|
|
4015
|
+
};
|
|
4016
|
+
if (Object.keys(req.query).length > 0) {
|
|
4017
|
+
axiosConfig.params = req.query;
|
|
4018
|
+
}
|
|
4019
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
|
|
4020
|
+
axiosConfig.data = requestBody;
|
|
4021
|
+
}
|
|
4022
|
+
// 代理配置
|
|
4023
|
+
if (service.enableProxy) {
|
|
4024
|
+
const appConfig = this.dbManager.getConfig();
|
|
4025
|
+
if (appConfig.proxyEnabled && appConfig.proxyUrl) {
|
|
4026
|
+
try {
|
|
4027
|
+
const { HttpsProxyAgent } = yield Promise.resolve().then(() => __importStar(require('https-proxy-agent')));
|
|
4028
|
+
let proxyUrl = appConfig.proxyUrl;
|
|
4029
|
+
const proxyAuth = appConfig.proxyUsername && appConfig.proxyPassword
|
|
4030
|
+
? `${appConfig.proxyUsername}:${appConfig.proxyPassword}@` : '';
|
|
4031
|
+
if (!proxyUrl.startsWith('http://') && !proxyUrl.startsWith('https://')) {
|
|
4032
|
+
proxyUrl = `http://${proxyAuth}${proxyUrl}`;
|
|
4033
|
+
}
|
|
4034
|
+
else if (proxyAuth) {
|
|
4035
|
+
const urlObj = new URL(proxyUrl);
|
|
4036
|
+
urlObj.username = appConfig.proxyUsername;
|
|
4037
|
+
urlObj.password = appConfig.proxyPassword;
|
|
4038
|
+
proxyUrl = urlObj.toString();
|
|
4039
|
+
}
|
|
4040
|
+
axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl);
|
|
4041
|
+
axiosConfig.httpAgent = new HttpsProxyAgent(proxyUrl);
|
|
4042
|
+
}
|
|
4043
|
+
catch (error) {
|
|
4044
|
+
console.error('[ApiPathProxy] Failed to create proxy agent:', error);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
const response = yield (0, axios_1.default)(axiosConfig);
|
|
4049
|
+
const responseHeaders = response.headers || {};
|
|
4050
|
+
const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
|
|
4051
|
+
const isEventStream = streamRequested && contentType.includes('text/event-stream');
|
|
4052
|
+
// Handle upstream errors
|
|
4053
|
+
if (response.status >= 400) {
|
|
4054
|
+
let errorResponseData = response.data;
|
|
4055
|
+
if (streamRequested && response.data && typeof response.data.on === 'function') {
|
|
4056
|
+
const raw = yield this.readStreamBody(response.data);
|
|
4057
|
+
errorResponseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
|
|
4058
|
+
}
|
|
4059
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4060
|
+
const errorMessage = typeof errorResponseData === 'string'
|
|
4061
|
+
? errorResponseData
|
|
4062
|
+
: ((_c = errorResponseData === null || errorResponseData === void 0 ? void 0 : errorResponseData.error) === null || _c === void 0 ? void 0 : _c.message) || (errorResponseData === null || errorResponseData === void 0 ? void 0 : errorResponseData.error) || JSON.stringify(errorResponseData);
|
|
4063
|
+
if (failoverEnabled) {
|
|
4064
|
+
yield finalizeLog(response.status, errorMessage);
|
|
4065
|
+
throw this.createFailoverError(errorMessage, response.status);
|
|
4066
|
+
}
|
|
4067
|
+
this.copyResponseHeaders(responseHeaders, res);
|
|
4068
|
+
if (contentType.includes('application/json')) {
|
|
4069
|
+
res.status(response.status).json(errorResponseData);
|
|
4070
|
+
}
|
|
4071
|
+
else {
|
|
4072
|
+
res.status(response.status).send(errorResponseData);
|
|
4073
|
+
}
|
|
4074
|
+
yield finalizeLog(response.status);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
if (isEventStream && response.data) {
|
|
4078
|
+
// Stream pipeline
|
|
4079
|
+
const parser = new streaming_1.SSEParserTransform();
|
|
4080
|
+
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
4081
|
+
const serializer = new streaming_1.SSESerializerTransform();
|
|
4082
|
+
const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
|
|
4083
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4084
|
+
const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
|
|
4085
|
+
this.copyResponseHeaders(responseHeaders, res);
|
|
4086
|
+
res.status(response.status);
|
|
4087
|
+
const finalizeStreamChunks = () => {
|
|
4088
|
+
streamChunksForLog = eventCollector.getChunks();
|
|
4089
|
+
responseBodyForLog = streamChunksForLog.join('\n');
|
|
4090
|
+
downstreamResponseBodyForLog = downstreamChunkCollector.getChunks().join('');
|
|
4091
|
+
let extractedUsage = eventCollector.extractUsage();
|
|
4092
|
+
if (converter && typeof converter.getUsage === 'function') {
|
|
4093
|
+
const converterUsage = converter.getUsage();
|
|
4094
|
+
if (converterUsage)
|
|
4095
|
+
extractedUsage = converterUsage;
|
|
4096
|
+
}
|
|
4097
|
+
if (extractUsage && extractedUsage) {
|
|
4098
|
+
usageForLog = extractUsage(extractedUsage);
|
|
4099
|
+
}
|
|
4100
|
+
else if (extractedUsage) {
|
|
4101
|
+
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
try {
|
|
4105
|
+
yield new Promise((resolve, reject) => {
|
|
4106
|
+
if (converter) {
|
|
4107
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
|
|
4108
|
+
if (error) {
|
|
4109
|
+
reject(error);
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
resolve();
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
else {
|
|
4116
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
|
|
4117
|
+
if (error) {
|
|
4118
|
+
reject(error);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
resolve();
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
catch (error) {
|
|
4127
|
+
if (this.isClientDisconnectError(error, res)) {
|
|
4128
|
+
yield finalizeLog(499, 'Client disconnected');
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
console.error('[ApiPathProxy] Stream pipeline error:', error);
|
|
4132
|
+
yield finalizeLog(500, error.message);
|
|
4133
|
+
if (failoverEnabled && !this.isResponseCommitted(res)) {
|
|
4134
|
+
throw this.createFailoverError(error.message, 500, error);
|
|
4135
|
+
}
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4138
|
+
finalizeStreamChunks();
|
|
4139
|
+
yield finalizeLog(res.statusCode);
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
// Non-stream response
|
|
4143
|
+
let responseData = response.data;
|
|
4144
|
+
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
4145
|
+
const raw = yield this.readStreamBody(response.data);
|
|
4146
|
+
responseData = (_d = this.safeJsonParse(raw)) !== null && _d !== void 0 ? _d : raw;
|
|
4147
|
+
}
|
|
4148
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4149
|
+
if (this.isEmptyResponse(responseData)) {
|
|
4150
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
4151
|
+
yield finalizeLog(200);
|
|
4152
|
+
res.status(200).end();
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
// 使用 clientFormat 做响应转换
|
|
4156
|
+
const converted = this.transformResponseByFormat(sourceType, clientFormat, responseData);
|
|
4157
|
+
const normalizedConverted = rule.contentType === 'compact' && clientFormat === 'claude'
|
|
4158
|
+
? (0, compact_1.stripClaudeCompactResponseContent)(converted)
|
|
4159
|
+
: converted;
|
|
4160
|
+
usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
|
|
4161
|
+
this.copyResponseHeaders(responseHeaders, res);
|
|
4162
|
+
if (normalizedConverted && normalizedConverted !== responseData) {
|
|
4163
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
4164
|
+
downstreamResponseBodyForLog = JSON.stringify(normalizedConverted);
|
|
4165
|
+
res.status(response.status).json(normalizedConverted);
|
|
4166
|
+
}
|
|
4167
|
+
else {
|
|
4168
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
4169
|
+
downstreamResponseBodyForLog = responseBodyForLog;
|
|
4170
|
+
if (contentType.includes('application/json')) {
|
|
4171
|
+
res.status(response.status).json(responseData);
|
|
4172
|
+
}
|
|
4173
|
+
else {
|
|
4174
|
+
res.status(response.status).send(responseData);
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
yield finalizeLog(res.statusCode);
|
|
4178
|
+
}
|
|
4179
|
+
finally {
|
|
4180
|
+
rules_status_service_1.rulesStatusBroadcaster.markRuleIdle(route.id, rule.id);
|
|
4181
|
+
}
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4185
|
+
* 使用显式 clientFormat 进行请求转换(取代 tool → format 的硬编码映射)
|
|
4186
|
+
*/
|
|
4187
|
+
transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig) {
|
|
4188
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
4189
|
+
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
|
|
4190
|
+
const body = result.body;
|
|
4191
|
+
if (targetModel) {
|
|
4192
|
+
const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
|
|
4193
|
+
if (!isOpenAIModel) {
|
|
4194
|
+
body.model = targetModel;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return body;
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* 使用显式格式进行响应转换
|
|
4201
|
+
*/
|
|
4202
|
+
transformResponseByFormat(upstreamFormat, clientFormat, responseData) {
|
|
4203
|
+
const upstream = (0, index_1.sourceTypeToFormat)(upstreamFormat);
|
|
4204
|
+
return (0, index_1.transformResponse)({ fromFormat: upstream, toFormat: clientFormat, response: responseData });
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* 使用显式格式进行流式转换
|
|
4208
|
+
*/
|
|
4209
|
+
transformSSEByFormat(clientFormat, sourceType) {
|
|
4210
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(sourceType);
|
|
4211
|
+
if (upstreamFormat === clientFormat) {
|
|
4212
|
+
return { converter: null };
|
|
4213
|
+
}
|
|
4214
|
+
const streamConverter = (0, index_1.createStreamConverter)({ fromFormat: upstreamFormat, toFormat: clientFormat });
|
|
4215
|
+
const adapter = new stream_converter_adapter_1.StreamConverterAdapter(streamConverter);
|
|
4216
|
+
const extractUsage = clientFormat === 'claude'
|
|
4217
|
+
? (usage) => ({
|
|
4218
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
4219
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
4220
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
4221
|
+
})
|
|
4222
|
+
: (usage) => ({
|
|
4223
|
+
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
4224
|
+
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
4225
|
+
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
4226
|
+
});
|
|
4227
|
+
return { converter: adapter, extractUsage };
|
|
4228
|
+
}
|
|
4229
|
+
/**
|
|
4230
|
+
* 为标准 API 路径构建上游 URL
|
|
4231
|
+
*/
|
|
4232
|
+
mapApiPathToUpstreamUrl(_apiPath, source, apiUrl, modelName, isStream) {
|
|
4233
|
+
const geminiEndpoint = isStream ? 'streamGenerateContent' : 'generateContent';
|
|
4234
|
+
const buildGeminiUrl = (url) => {
|
|
4235
|
+
if (url.includes('streamGenerateContent')) {
|
|
4236
|
+
const [pathname, search] = url.split('?');
|
|
4237
|
+
if (search === null || search === void 0 ? void 0 : search.includes('alt=sse'))
|
|
4238
|
+
return url;
|
|
4239
|
+
if (search)
|
|
4240
|
+
return `${pathname}?${search}&alt=sse`;
|
|
4241
|
+
return `${pathname}?alt=sse`;
|
|
4242
|
+
}
|
|
4243
|
+
return url;
|
|
4244
|
+
};
|
|
4245
|
+
// Gemini chat 类型:URL 中包含 {modelName} 和 {endPoint} 占位符
|
|
4246
|
+
if (this.isGeminiChatSource(source)) {
|
|
4247
|
+
const url = apiUrl.replace('{modelName}', modelName).replace('{endPoint}', geminiEndpoint);
|
|
4248
|
+
return buildGeminiUrl(url);
|
|
4249
|
+
}
|
|
4250
|
+
// Chat 类型(openai-chat, claude-chat):直接使用 apiUrl
|
|
4251
|
+
if (this.isChatType(source)) {
|
|
4252
|
+
return apiUrl;
|
|
4253
|
+
}
|
|
4254
|
+
// Gemini base 类型
|
|
4255
|
+
if (this.isGeminiSource(source)) {
|
|
4256
|
+
const url = `${apiUrl}/v1beta/models/${modelName}:${geminiEndpoint}`;
|
|
4257
|
+
return buildGeminiUrl(url);
|
|
4258
|
+
}
|
|
4259
|
+
// 对于标准 API 路径,直接根据上游格式拼接
|
|
4260
|
+
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
4261
|
+
switch (upstreamFormat) {
|
|
4262
|
+
case 'claude':
|
|
4263
|
+
return `${apiUrl}/v1/messages`;
|
|
4264
|
+
case 'responses':
|
|
4265
|
+
return `${apiUrl}/v1/responses`;
|
|
4266
|
+
case 'completions':
|
|
4267
|
+
return `${apiUrl}/v1/chat/completions`;
|
|
4268
|
+
case 'gemini':
|
|
4269
|
+
return `${apiUrl}/v1beta/models/${modelName}:${geminiEndpoint}`;
|
|
4270
|
+
default:
|
|
4271
|
+
return apiUrl;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* 编程套餐限制检查
|
|
4276
|
+
* 当服务启用了 enableCodingPlan 时,仅允许编程工具发起的请求通过。
|
|
4277
|
+
* @returns true 表示通过检查(可以继续),false 表示已被拒绝(已写入响应)
|
|
4278
|
+
*/
|
|
4279
|
+
checkCodingPlan(req, res, service, clientFormat) {
|
|
4280
|
+
if (!service.enableCodingPlan)
|
|
4281
|
+
return true; // 未启用,直接通过
|
|
4282
|
+
const headers = req.headers;
|
|
4283
|
+
const codingCheck = (0, coding_plan_1.isCodingToolRequest)(req.body, clientFormat, headers);
|
|
4284
|
+
if (codingCheck.isCoding)
|
|
4285
|
+
return true; // 是编程工具请求,通过
|
|
4286
|
+
// 非编程工具请求,拒绝
|
|
4287
|
+
console.warn(`\x1b[33m[CodingPlan]\x1b[0m Rejected non-coding request: service=${service.name}, reason=${codingCheck.reason}`);
|
|
4288
|
+
this.sendFormatError(res, clientFormat, 403, '此 API 服务仅允许编程工具调用(如 Claude Code、Codex、Cursor 等)');
|
|
4289
|
+
return false;
|
|
4290
|
+
}
|
|
4291
|
+
/**
|
|
4292
|
+
* 根据客户端格式发送错误响应
|
|
4293
|
+
*/
|
|
4294
|
+
sendFormatError(res, clientFormat, statusCode, message) {
|
|
4295
|
+
if (this.isResponseCommitted(res))
|
|
4296
|
+
return;
|
|
4297
|
+
switch (clientFormat) {
|
|
4298
|
+
case 'claude':
|
|
4299
|
+
res.status(statusCode).json({ type: 'error', error: { type: 'api_error', message } });
|
|
4300
|
+
break;
|
|
4301
|
+
case 'gemini':
|
|
4302
|
+
res.status(statusCode).json({ error: { code: statusCode, message } });
|
|
4303
|
+
break;
|
|
4304
|
+
default:
|
|
4305
|
+
res.status(statusCode).json({ error: { message } });
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4010
4308
|
}
|
|
4011
4309
|
exports.ProxyServer = ProxyServer;
|