aicodeswitch 4.0.4 → 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 +6 -5
- 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 +887 -633
- 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-Dl-B9pXM.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,7 +286,46 @@ class ProxyServer {
|
|
|
167
286
|
});
|
|
168
287
|
}
|
|
169
288
|
initialize() {
|
|
170
|
-
//
|
|
289
|
+
// === 标准 API 路径前置中间件 ===
|
|
290
|
+
// 处理 /v1/models 和 4 个可绑定的标准 API 路径
|
|
291
|
+
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
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 逻辑)
|
|
171
329
|
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
172
330
|
var _a, _b, _c, _d, _e;
|
|
173
331
|
// 仅处理支持的目标路径
|
|
@@ -196,7 +354,7 @@ class ProxyServer {
|
|
|
196
354
|
return res.status(404).json({ error: 'No matching route found and no original config available' });
|
|
197
355
|
}
|
|
198
356
|
// 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
|
|
199
|
-
const forcedContentType = yield this.prepareHighIqRouting(req, route,
|
|
357
|
+
const forcedContentType = yield this.prepareHighIqRouting(req, route, this.inferTargetTypeFromPath(req.path) || 'claude-code');
|
|
200
358
|
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
201
359
|
if (!enableFailover) {
|
|
202
360
|
// 故障切换已禁用,使用传统的单一规则匹配
|
|
@@ -210,7 +368,7 @@ class ProxyServer {
|
|
|
210
368
|
yield this.logToolRequest(req, {
|
|
211
369
|
statusCode: 404,
|
|
212
370
|
responseTime: Date.now() - requestStartAt,
|
|
213
|
-
targetType:
|
|
371
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
214
372
|
error: 'No matching rule found',
|
|
215
373
|
tags: this.buildRelayTags(false),
|
|
216
374
|
});
|
|
@@ -221,7 +379,7 @@ class ProxyServer {
|
|
|
221
379
|
yield this.logToolRequest(req, {
|
|
222
380
|
statusCode: 500,
|
|
223
381
|
responseTime: Date.now() - requestStartAt,
|
|
224
|
-
targetType:
|
|
382
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
225
383
|
error: 'Target service not configured',
|
|
226
384
|
tags: this.buildRelayTags(false),
|
|
227
385
|
});
|
|
@@ -242,7 +400,7 @@ class ProxyServer {
|
|
|
242
400
|
yield this.logToolRequest(req, {
|
|
243
401
|
statusCode: 404,
|
|
244
402
|
responseTime: Date.now() - requestStartAt,
|
|
245
|
-
targetType:
|
|
403
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
246
404
|
error: 'No matching rule found',
|
|
247
405
|
tags: this.buildRelayTags(false),
|
|
248
406
|
});
|
|
@@ -333,7 +491,7 @@ class ProxyServer {
|
|
|
333
491
|
yield this.logToolRequest(req, {
|
|
334
492
|
statusCode: 503,
|
|
335
493
|
responseTime: Date.now() - requestStartAt,
|
|
336
|
-
targetType:
|
|
494
|
+
targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
|
|
337
495
|
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
338
496
|
tags: this.buildRelayTags(hasRelayAttempt),
|
|
339
497
|
});
|
|
@@ -692,9 +850,6 @@ class ProxyServer {
|
|
|
692
850
|
* 从数据库实时获取所有活跃路由
|
|
693
851
|
* @returns 活跃路由列表
|
|
694
852
|
*/
|
|
695
|
-
getActiveRoutes() {
|
|
696
|
-
return this.dbManager.getRoutes().filter(route => route.isActive);
|
|
697
|
-
}
|
|
698
853
|
/**
|
|
699
854
|
* 从数据库实时获取指定路由的规则
|
|
700
855
|
* @param routeId 路由ID
|
|
@@ -708,20 +863,19 @@ class ProxyServer {
|
|
|
708
863
|
return this.dbManager.getRule(ruleId);
|
|
709
864
|
}
|
|
710
865
|
findMatchingRoute(req) {
|
|
711
|
-
|
|
712
|
-
let targetType;
|
|
866
|
+
let tool;
|
|
713
867
|
if (req.path.startsWith('/claude-code/')) {
|
|
714
|
-
|
|
868
|
+
tool = 'claude-code';
|
|
715
869
|
}
|
|
716
870
|
else if (req.path.startsWith('/codex/')) {
|
|
717
|
-
|
|
871
|
+
tool = 'codex';
|
|
718
872
|
}
|
|
719
|
-
if (!
|
|
873
|
+
if (!tool)
|
|
720
874
|
return undefined;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return
|
|
875
|
+
const routeId = this.dbManager.getActiveRouteIdForTool(tool);
|
|
876
|
+
if (!routeId)
|
|
877
|
+
return undefined;
|
|
878
|
+
return this.dbManager.getRoute(routeId);
|
|
725
879
|
}
|
|
726
880
|
/**
|
|
727
881
|
* 当没有激活的路由时,fallback 到原始配置
|
|
@@ -772,8 +926,6 @@ class ProxyServer {
|
|
|
772
926
|
const tempRoute = {
|
|
773
927
|
id: 'fallback-route',
|
|
774
928
|
name: 'Fallback to Original Config',
|
|
775
|
-
targetType: targetType,
|
|
776
|
-
isActive: true,
|
|
777
929
|
createdAt: Date.now(),
|
|
778
930
|
updatedAt: Date.now(),
|
|
779
931
|
};
|
|
@@ -872,9 +1024,11 @@ class ProxyServer {
|
|
|
872
1024
|
}
|
|
873
1025
|
return undefined;
|
|
874
1026
|
}
|
|
875
|
-
findRouteByTargetType(
|
|
876
|
-
const
|
|
877
|
-
|
|
1027
|
+
findRouteByTargetType(tool) {
|
|
1028
|
+
const routeId = this.dbManager.getActiveRouteIdForTool(tool);
|
|
1029
|
+
if (!routeId)
|
|
1030
|
+
return undefined;
|
|
1031
|
+
return this.dbManager.getRoute(routeId);
|
|
878
1032
|
}
|
|
879
1033
|
findNextAvailableServiceName(allRules, startIndex, routeId) {
|
|
880
1034
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1175,8 +1329,7 @@ class ProxyServer {
|
|
|
1175
1329
|
return undefined;
|
|
1176
1330
|
const body = req.body;
|
|
1177
1331
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
1178
|
-
const
|
|
1179
|
-
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);
|
|
1180
1333
|
// 高智商规则优先于 model-mapping,确保 !!/推断命中时不会被模型映射覆盖
|
|
1181
1334
|
if (contentType === 'high-iq') {
|
|
1182
1335
|
const highIqRules = enabledRules.filter(rule => rule.contentType === 'high-iq');
|
|
@@ -1292,8 +1445,7 @@ class ProxyServer {
|
|
|
1292
1445
|
const body = req.body;
|
|
1293
1446
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
1294
1447
|
const candidates = [];
|
|
1295
|
-
const
|
|
1296
|
-
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);
|
|
1297
1449
|
const prioritizeContentType = contentType === 'high-iq';
|
|
1298
1450
|
const modelMappingRules = requestModel
|
|
1299
1451
|
? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
@@ -1446,6 +1598,16 @@ class ProxyServer {
|
|
|
1446
1598
|
}
|
|
1447
1599
|
getContentTypeDetectors() {
|
|
1448
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
|
+
},
|
|
1449
1611
|
{
|
|
1450
1612
|
type: 'image-understanding',
|
|
1451
1613
|
match: (_req, body) => this.containsImageContentInLatestMessage(body.messages) || this.containsImageContent(body.input),
|
|
@@ -1533,6 +1695,10 @@ class ProxyServer {
|
|
|
1533
1695
|
image_understanding: 'image-understanding',
|
|
1534
1696
|
'image-understanding': 'image-understanding',
|
|
1535
1697
|
vision: 'image-understanding',
|
|
1698
|
+
compact: 'compact',
|
|
1699
|
+
compaction: 'compact',
|
|
1700
|
+
summarize: 'compact',
|
|
1701
|
+
summary: 'compact',
|
|
1536
1702
|
};
|
|
1537
1703
|
return mapping[normalized] || null;
|
|
1538
1704
|
}
|
|
@@ -2101,21 +2267,13 @@ class ProxyServer {
|
|
|
2101
2267
|
// 向下兼容:支持旧类型 'claude-code'
|
|
2102
2268
|
return sourceType === 'claude' || sourceType === 'claude-code';
|
|
2103
2269
|
}
|
|
2104
|
-
/** 判断是否为 Claude Chat 类型 */
|
|
2105
|
-
isClaudeChatSource(sourceType) {
|
|
2106
|
-
return sourceType === 'claude-chat';
|
|
2107
|
-
}
|
|
2108
2270
|
isOpenAISource(sourceType) {
|
|
2109
2271
|
// 向下兼容:支持旧类型 'openai-responses'
|
|
2110
2272
|
return sourceType === 'openai' || sourceType === 'openai-responses';
|
|
2111
2273
|
}
|
|
2112
2274
|
/** 判断是否为 OpenAI Chat 类型 */
|
|
2113
2275
|
isOpenAIChatSource(sourceType) {
|
|
2114
|
-
return sourceType === 'openai-chat'
|
|
2115
|
-
}
|
|
2116
|
-
/** 判断是否为 OpenAI 类型(包括 OpenAI Chat 和 OpenAI Responses) */
|
|
2117
|
-
isOpenAIType(sourceType) {
|
|
2118
|
-
return sourceType === 'openai' || sourceType === 'openai-chat' || sourceType === 'openai-responses' || sourceType === 'deepseek-reasoning-chat';
|
|
2276
|
+
return sourceType === 'openai-chat';
|
|
2119
2277
|
}
|
|
2120
2278
|
/** 判断是否为 Gemini 类型 */
|
|
2121
2279
|
isGeminiSource(sourceType) {
|
|
@@ -2245,7 +2403,7 @@ class ProxyServer {
|
|
|
2245
2403
|
headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
|
|
2246
2404
|
}
|
|
2247
2405
|
}
|
|
2248
|
-
// 使用 Authorization 认证(适用于 openai-chat, openai-responses
|
|
2406
|
+
// 使用 Authorization 认证(适用于 openai-chat, openai-responses 等)
|
|
2249
2407
|
else {
|
|
2250
2408
|
headers.authorization = `Bearer ${effectiveApiKey}`;
|
|
2251
2409
|
}
|
|
@@ -2282,6 +2440,17 @@ class ProxyServer {
|
|
|
2282
2440
|
}
|
|
2283
2441
|
return vendor.apiKey || '';
|
|
2284
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
|
+
}
|
|
2285
2454
|
copyResponseHeaders(responseHeaders, res) {
|
|
2286
2455
|
Object.keys(responseHeaders).forEach((key) => {
|
|
2287
2456
|
if (!['content-encoding', 'transfer-encoding', 'connection', 'content-length'].includes(key.toLowerCase())) {
|
|
@@ -2400,8 +2569,8 @@ class ProxyServer {
|
|
|
2400
2569
|
return ProxyServer.extractSessionIdFromUserId(rawUserId);
|
|
2401
2570
|
}
|
|
2402
2571
|
else if (type === 'codex') {
|
|
2403
|
-
// Codex 使用 headers
|
|
2404
|
-
const sessionId = request.headers['session_id'];
|
|
2572
|
+
// Codex 使用 headers 中的 session-id 或 session_id(兼容新旧版本)
|
|
2573
|
+
const sessionId = request.headers['session-id'] || request.headers['session_id'];
|
|
2405
2574
|
if (typeof sessionId === 'string') {
|
|
2406
2575
|
return sessionId;
|
|
2407
2576
|
}
|
|
@@ -2592,48 +2761,19 @@ class ProxyServer {
|
|
|
2592
2761
|
* @param targetModel 目标模型名称(可选)
|
|
2593
2762
|
* @returns 转换后往服务商API接口的数据
|
|
2594
2763
|
*/
|
|
2595
|
-
transformRequestToUpstream(tool, source, payloadData, targetModel) {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
if (
|
|
2604
|
-
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
return (0, transformers_1.transformRequestFromClaudeToChatCompletions)(payloadData, targetModel);
|
|
2609
|
-
}
|
|
2610
|
-
// claudecode发送给openai responses接口
|
|
2611
|
-
if (this.isOpenAISource(source)) {
|
|
2612
|
-
return (0, transformers_1.transformRequestFromClaudeToResponses)(payloadData, targetModel);
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
// Codex 发起的请求(仅支持 Responses API 格式)
|
|
2616
|
-
if (tool === 'codex') {
|
|
2617
|
-
// Codex 发送给 OpenAI Responses
|
|
2618
|
-
if (this.isOpenAISource(source)) {
|
|
2619
|
-
return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
|
|
2620
|
-
}
|
|
2621
|
-
// Codex 发送给 OpenAI Chat
|
|
2622
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2623
|
-
// 将 responses 格式转换为 chat completions 格式
|
|
2624
|
-
return (0, transformers_1.transformRequestFromResponsesToChatCompletions)(payloadData, targetModel);
|
|
2625
|
-
}
|
|
2626
|
-
// Codex 发送给 Gemini
|
|
2627
|
-
if (this.isGeminiChatSource(source) || this.isGeminiSource(source)) {
|
|
2628
|
-
return (0, transformers_1.transformRequestFromResponsesToGemini)(payloadData, targetModel);
|
|
2629
|
-
}
|
|
2630
|
-
// Codex 发送给 Claude
|
|
2631
|
-
if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
|
|
2632
|
-
return (0, transformers_1.transformRequestFromResponsesToClaude)(payloadData, targetModel);
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
// 默认: 直接返回原始数据(如果需要,应用模型覆盖)
|
|
2636
|
-
return (0, transformers_1.applyPayloadOverride)(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;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return body;
|
|
2637
2777
|
}
|
|
2638
2778
|
/**
|
|
2639
2779
|
* 将来自API接口的响应数据,转换为工具需要的数据结构
|
|
@@ -2642,158 +2782,102 @@ class ProxyServer {
|
|
|
2642
2782
|
* @param responseData
|
|
2643
2783
|
*/
|
|
2644
2784
|
transformResponseToTool(tool, source, responseData) {
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
}
|
|
2649
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2650
|
-
return (0, transformers_1.transformResponseFromChatCompletionsToClaude)(responseData);
|
|
2651
|
-
}
|
|
2652
|
-
if (this.isOpenAISource(source)) {
|
|
2653
|
-
return (0, transformers_1.transformResponseFromResponsesToClaude)(responseData);
|
|
2654
|
-
}
|
|
2655
|
-
if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
|
|
2656
|
-
return (0, transformers_1.transformResponseFromGeminiToClaude)(responseData);
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
if (tool === 'codex') {
|
|
2660
|
-
// Codex 仅支持 Responses API 格式
|
|
2661
|
-
// Codex 接收来自 OpenAI Responses 的响应
|
|
2662
|
-
if (this.isOpenAISource(source)) {
|
|
2663
|
-
return responseData;
|
|
2664
|
-
}
|
|
2665
|
-
// Codex 接收来自 OpenAI Chat 的响应(转换为 Responses 格式)
|
|
2666
|
-
if (this.isOpenAIChatSource(source)) {
|
|
2667
|
-
return (0, transformers_1.transformResponseFromChatCompletionsToResponses)(responseData);
|
|
2668
|
-
}
|
|
2669
|
-
// Codex 接收来自 Claude 的响应(转换为 Responses 格式)
|
|
2670
|
-
if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
|
|
2671
|
-
return (0, transformers_1.transformResponseFromClaudeToResponses)(responseData);
|
|
2672
|
-
}
|
|
2673
|
-
// Codex 接收来自 Gemini 的响应(转换为 Responses 格式)
|
|
2674
|
-
if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
|
|
2675
|
-
return (0, transformers_1.transformResponseFromGeminiToResponses)(responseData);
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
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 });
|
|
2678
2788
|
}
|
|
2679
2789
|
/**
|
|
2680
2790
|
* 获取流式响应转换器
|
|
2681
2791
|
* @param targetType 目标工具类型
|
|
2682
2792
|
* @param sourceType 数据源类型
|
|
2683
|
-
* @param model 模型名称
|
|
2684
2793
|
* @returns 转换器实例和相关信息
|
|
2685
2794
|
*/
|
|
2686
|
-
transformSSEToTool(targetType, sourceType
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
extractUsage: (usage) => ({
|
|
2707
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2708
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2709
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2710
|
-
}),
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
// Claude Code 接收来自 Gemini 的响应
|
|
2714
|
-
if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2715
|
-
return {
|
|
2716
|
-
converter: new streaming_1.GeminiToClaudeEventTransform({ model }),
|
|
2717
|
-
extractUsage: (usage) => ({
|
|
2718
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2719
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2720
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
2721
|
-
}),
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
// Codex 接收流式响应
|
|
2726
|
-
if (targetType === 'codex') {
|
|
2727
|
-
// Codex 接收来自 Claude 的响应
|
|
2728
|
-
if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
|
|
2729
|
-
console.log('[Proxy] Using Claude -> Responses API stream converter');
|
|
2730
|
-
return {
|
|
2731
|
-
converter: new streaming_1.ClaudeToResponsesEventTransform({ model }),
|
|
2732
|
-
extractUsage: (usage) => ({
|
|
2733
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2734
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2735
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2736
|
-
}),
|
|
2737
|
-
};
|
|
2738
|
-
}
|
|
2739
|
-
// Codex 接收来自 OpenAI Chat 的响应
|
|
2740
|
-
if (this.isOpenAIChatSource(sourceType)) {
|
|
2741
|
-
console.log('[Proxy] Using OpenAI Chat -> Responses API stream converter');
|
|
2742
|
-
return {
|
|
2743
|
-
converter: new streaming_1.ChatCompletionsToResponsesEventTransform({ model }),
|
|
2744
|
-
extractUsage: (usage) => ({
|
|
2745
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2746
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2747
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2748
|
-
}),
|
|
2749
|
-
};
|
|
2750
|
-
}
|
|
2751
|
-
// Codex 接收来自 Gemini 的响应
|
|
2752
|
-
if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2753
|
-
console.log('[Proxy] Using Gemini -> Responses API stream converter');
|
|
2754
|
-
return {
|
|
2755
|
-
converter: new streaming_1.GeminiToResponsesEventTransform({ model }),
|
|
2756
|
-
extractUsage: (usage) => ({
|
|
2757
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
2758
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
2759
|
-
totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
|
|
2760
|
-
}),
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
// 默认:返回空转换器(不需要转换)
|
|
2765
|
-
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 };
|
|
2766
2815
|
}
|
|
2767
2816
|
extractTokenUsageFromResponse(responseData, sourceType) {
|
|
2817
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2768
2818
|
if (!responseData)
|
|
2769
2819
|
return undefined;
|
|
2770
|
-
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
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
|
+
};
|
|
2773
2831
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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
|
+
};
|
|
2777
2842
|
}
|
|
2778
|
-
|
|
2779
|
-
if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
|
|
2780
|
-
// 如果 responseData 直接包含 input_tokens/output_tokens,说明它本身就是 usage 对象
|
|
2843
|
+
if (format === 'claude') {
|
|
2781
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') {
|
|
2782
|
-
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
|
+
};
|
|
2783
2851
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
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
|
+
};
|
|
2786
2861
|
}
|
|
2862
|
+
// 通用 fallback
|
|
2787
2863
|
const usage = responseData.usage;
|
|
2788
2864
|
if (!usage)
|
|
2789
2865
|
return undefined;
|
|
2790
|
-
// OpenAI 使用 prompt_tokens 和 completion_tokens
|
|
2791
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') {
|
|
2792
|
-
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
|
+
};
|
|
2793
2873
|
}
|
|
2794
|
-
// Claude 使用 input_tokens 和 output_tokens
|
|
2795
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') {
|
|
2796
|
-
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
|
+
};
|
|
2797
2881
|
}
|
|
2798
2882
|
return undefined;
|
|
2799
2883
|
}
|
|
@@ -2805,7 +2889,7 @@ class ProxyServer {
|
|
|
2805
2889
|
const rawSourceType = service.sourceType || 'openai-chat';
|
|
2806
2890
|
// 标准化 sourceType,将旧类型转换为新类型(向下兼容)
|
|
2807
2891
|
const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
|
|
2808
|
-
const targetType =
|
|
2892
|
+
const targetType = this.inferToolFromRequest(req);
|
|
2809
2893
|
const sessionId = this.defaultExtractSessionId(req, targetType) || '-';
|
|
2810
2894
|
const vendor = this.dbManager.getVendorByServiceId(service.id);
|
|
2811
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) || '-'}`);
|
|
@@ -2813,11 +2897,35 @@ class ProxyServer {
|
|
|
2813
2897
|
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
2814
2898
|
const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
|
|
2815
2899
|
let relayedForLog = !useOriginalConfig;
|
|
2816
|
-
|
|
2900
|
+
let originalToolRequestBody = this.cloneRequestBody(req.body || {});
|
|
2817
2901
|
let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
|
|
2818
2902
|
let usageForLog;
|
|
2819
2903
|
let logged = false;
|
|
2820
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
|
+
}
|
|
2821
2929
|
// MCP 图像理解处理
|
|
2822
2930
|
let tempImageFiles = [];
|
|
2823
2931
|
let useMCPProcessing = false;
|
|
@@ -3174,8 +3282,22 @@ class ProxyServer {
|
|
|
3174
3282
|
try {
|
|
3175
3283
|
// 使用统一的请求转换方法
|
|
3176
3284
|
const payloadForTransform = this.cloneRequestBody(originalToolRequestBody);
|
|
3177
|
-
|
|
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);
|
|
3178
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
|
+
}
|
|
3179
3301
|
// 应用 max_output_tokens 限制
|
|
3180
3302
|
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
3181
3303
|
if (this.shouldDefaultStreamingForClaudeBridge(req, targetType, sourceType, requestBody)
|
|
@@ -3186,16 +3308,16 @@ class ProxyServer {
|
|
|
3186
3308
|
const streamRequested = this.isStreamRequested(req, requestBody, targetType, sourceType);
|
|
3187
3309
|
// Build the full URL by appending the request path to the service API URL
|
|
3188
3310
|
let pathToRequest = req.path;
|
|
3189
|
-
if (
|
|
3311
|
+
if (targetType === 'claude-code' && req.path.startsWith('/claude-code')) {
|
|
3190
3312
|
pathToRequest = req.path.slice('/claude-code'.length);
|
|
3191
3313
|
}
|
|
3192
|
-
else if (
|
|
3314
|
+
else if (targetType === 'codex' && req.path.startsWith('/codex')) {
|
|
3193
3315
|
pathToRequest = req.path.slice('/codex'.length);
|
|
3194
3316
|
}
|
|
3195
3317
|
// 使用 mapRequestPathToUpstreamUrl 统一构建上游 URL
|
|
3196
3318
|
const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3197
|
-
const apiUrl = service
|
|
3198
|
-
const upstreamUrl = this.mapRequestPathToUpstreamUrl(
|
|
3319
|
+
const apiUrl = this.resolveEffectiveApiUrl(service);
|
|
3320
|
+
const upstreamUrl = this.mapRequestPathToUpstreamUrl(targetType, sourceType, pathToRequest, apiUrl, model, streamRequested);
|
|
3199
3321
|
const config = {
|
|
3200
3322
|
method: req.method,
|
|
3201
3323
|
url: upstreamUrl,
|
|
@@ -3279,416 +3401,17 @@ class ProxyServer {
|
|
|
3279
3401
|
}
|
|
3280
3402
|
if (isEventStream && response.data) {
|
|
3281
3403
|
res.status(response.status);
|
|
3282
|
-
// 统一走默认流式链路(transformSSEToTool + 统一断连保护),避免历史分支行为不一致
|
|
3283
|
-
const useLegacySpecialSSEBranches = false;
|
|
3284
|
-
if (useLegacySpecialSSEBranches && targetType === 'claude-code' && this.isOpenAIType(sourceType)) {
|
|
3285
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3286
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3287
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3288
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3289
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3290
|
-
const converter = new streaming_1.OpenAIToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3291
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3292
|
-
// 收集响应头
|
|
3293
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3294
|
-
// 监听事件收集器的完成事件,确保所有chunks都被收集
|
|
3295
|
-
const finalizeChunks = () => {
|
|
3296
|
-
const usage = converter.getUsage();
|
|
3297
|
-
console.log('[Proxy] Claude Code stream: converter usage:', usage);
|
|
3298
|
-
if (usage) {
|
|
3299
|
-
usageForLog = (0, transformers_1.extractTokenUsageFromClaudeUsage)(usage);
|
|
3300
|
-
console.log('[Proxy] Claude Code stream: usageForLog from converter:', usageForLog);
|
|
3301
|
-
}
|
|
3302
|
-
// 尝试从event collector中提取usage(作为补充)
|
|
3303
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3304
|
-
console.log('[Proxy] Claude Code stream: extracted usage from eventCollector:', extractedUsage);
|
|
3305
|
-
if (!usageForLog && extractedUsage) {
|
|
3306
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3307
|
-
console.log('[Proxy] Claude Code stream: usageForLog from eventCollector:', usageForLog);
|
|
3308
|
-
}
|
|
3309
|
-
// 收集stream chunks(每个chunk是一个完整的SSE事件)
|
|
3310
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3311
|
-
// 将所有 chunks 合并成完整的响应体用于日志记录
|
|
3312
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3313
|
-
console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3314
|
-
console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
3315
|
-
console.log('[Proxy] Claude Code stream: final usageForLog before finalizeLog:', usageForLog);
|
|
3316
|
-
void finalizeLog(res.statusCode);
|
|
3317
|
-
};
|
|
3318
|
-
// 在pipeline完成且eventCollector flush后执行
|
|
3319
|
-
eventCollector.on('finish', () => {
|
|
3320
|
-
console.log('[Proxy] EventCollector finished, collecting chunks...');
|
|
3321
|
-
finalizeChunks();
|
|
3322
|
-
});
|
|
3323
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3324
|
-
res.on('finish', () => {
|
|
3325
|
-
console.log('[Proxy] Response finished');
|
|
3326
|
-
if (!streamChunksForLog) {
|
|
3327
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3328
|
-
finalizeChunks();
|
|
3329
|
-
}
|
|
3330
|
-
});
|
|
3331
|
-
// 监听 res 的错误事件
|
|
3332
|
-
res.on('error', (err) => {
|
|
3333
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3334
|
-
});
|
|
3335
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3336
|
-
var _a, _b;
|
|
3337
|
-
if (error) {
|
|
3338
|
-
console.error('[Proxy] Pipeline error for claude-code:', error);
|
|
3339
|
-
// 记录到错误日志 - 包含请求详情和实际转发信息
|
|
3340
|
-
try {
|
|
3341
|
-
// 获取供应商信息
|
|
3342
|
-
const vendors = this.dbManager.getVendors();
|
|
3343
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3344
|
-
yield this.dbManager.addErrorLog({
|
|
3345
|
-
timestamp: Date.now(),
|
|
3346
|
-
method: req.method,
|
|
3347
|
-
path: req.path,
|
|
3348
|
-
statusCode: 500,
|
|
3349
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3350
|
-
errorStack: error.stack,
|
|
3351
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3352
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3353
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3354
|
-
responseHeaders: responseHeadersForLog,
|
|
3355
|
-
// 添加请求详情
|
|
3356
|
-
ruleId: rule.id,
|
|
3357
|
-
targetType,
|
|
3358
|
-
targetServiceId: service.id,
|
|
3359
|
-
targetServiceName: service.name,
|
|
3360
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3361
|
-
vendorId: service.vendorId,
|
|
3362
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3363
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3364
|
-
responseTime: Date.now() - startTime,
|
|
3365
|
-
});
|
|
3366
|
-
}
|
|
3367
|
-
catch (logError) {
|
|
3368
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3369
|
-
}
|
|
3370
|
-
// 尝试向客户端发送错误事件
|
|
3371
|
-
try {
|
|
3372
|
-
if (!res.writableEnded) {
|
|
3373
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({
|
|
3374
|
-
type: 'error',
|
|
3375
|
-
error: {
|
|
3376
|
-
type: 'api_error',
|
|
3377
|
-
message: 'Stream processing error occurred'
|
|
3378
|
-
}
|
|
3379
|
-
})}\n\n`;
|
|
3380
|
-
res.write(errorEvent);
|
|
3381
|
-
res.end();
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
catch (writeError) {
|
|
3385
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3386
|
-
}
|
|
3387
|
-
yield finalizeLog(500, error.message);
|
|
3388
|
-
}
|
|
3389
|
-
}));
|
|
3390
|
-
return;
|
|
3391
|
-
}
|
|
3392
|
-
if (useLegacySpecialSSEBranches && targetType === 'codex' && this.isClaudeSource(sourceType)) {
|
|
3393
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3394
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3395
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3396
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3397
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3398
|
-
const converter = new streaming_1.ClaudeToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3399
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3400
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3401
|
-
// 监听事件收集器的完成事件,确保所有chunks都被收集
|
|
3402
|
-
const finalizeChunks = () => {
|
|
3403
|
-
const usage = converter.getUsage();
|
|
3404
|
-
console.log('[Proxy] Codex stream: converter usage:', usage);
|
|
3405
|
-
if (usage) {
|
|
3406
|
-
usageForLog = (0, transformers_1.extractTokenUsageFromOpenAIUsage)(usage);
|
|
3407
|
-
console.log('[Proxy] Codex stream: usageForLog from converter:', usageForLog);
|
|
3408
|
-
}
|
|
3409
|
-
// 尝试从event collector中提取usage(作为补充)
|
|
3410
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3411
|
-
console.log('[Proxy] Codex stream: extracted usage from eventCollector:', extractedUsage);
|
|
3412
|
-
if (!usageForLog && extractedUsage) {
|
|
3413
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3414
|
-
console.log('[Proxy] Codex stream: usageForLog from eventCollector:', usageForLog);
|
|
3415
|
-
}
|
|
3416
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3417
|
-
// 将所有 chunks 合并成完整的响应体用于日志记录
|
|
3418
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3419
|
-
console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3420
|
-
console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
3421
|
-
console.log('[Proxy] Codex stream: final usageForLog before finalizeLog:', usageForLog);
|
|
3422
|
-
void finalizeLog(res.statusCode);
|
|
3423
|
-
};
|
|
3424
|
-
// 在pipeline完成且eventCollector flush后执行
|
|
3425
|
-
eventCollector.on('finish', () => {
|
|
3426
|
-
console.log('[Proxy] EventCollector finished (codex), collecting chunks...');
|
|
3427
|
-
finalizeChunks();
|
|
3428
|
-
});
|
|
3429
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3430
|
-
res.on('finish', () => {
|
|
3431
|
-
console.log('[Proxy] Response finished (codex)');
|
|
3432
|
-
if (!streamChunksForLog) {
|
|
3433
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3434
|
-
finalizeChunks();
|
|
3435
|
-
}
|
|
3436
|
-
});
|
|
3437
|
-
// 监听 res 的错误事件
|
|
3438
|
-
res.on('error', (err) => {
|
|
3439
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3440
|
-
});
|
|
3441
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3442
|
-
var _a, _b;
|
|
3443
|
-
if (error) {
|
|
3444
|
-
console.error('[Proxy] Pipeline error for codex:', error);
|
|
3445
|
-
// 记录到错误日志 - 包含请求详情和实际转发信息
|
|
3446
|
-
try {
|
|
3447
|
-
// 获取供应商信息
|
|
3448
|
-
const vendors = this.dbManager.getVendors();
|
|
3449
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3450
|
-
yield this.dbManager.addErrorLog({
|
|
3451
|
-
timestamp: Date.now(),
|
|
3452
|
-
method: req.method,
|
|
3453
|
-
path: req.path,
|
|
3454
|
-
statusCode: 500,
|
|
3455
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3456
|
-
errorStack: error.stack,
|
|
3457
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3458
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3459
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3460
|
-
responseHeaders: responseHeadersForLog,
|
|
3461
|
-
// 添加请求详情
|
|
3462
|
-
ruleId: rule.id,
|
|
3463
|
-
targetType,
|
|
3464
|
-
targetServiceId: service.id,
|
|
3465
|
-
targetServiceName: service.name,
|
|
3466
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3467
|
-
vendorId: service.vendorId,
|
|
3468
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3469
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3470
|
-
responseTime: Date.now() - startTime,
|
|
3471
|
-
});
|
|
3472
|
-
}
|
|
3473
|
-
catch (logError) {
|
|
3474
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3475
|
-
}
|
|
3476
|
-
// 尝试向客户端发送错误事件
|
|
3477
|
-
try {
|
|
3478
|
-
if (!res.writableEnded) {
|
|
3479
|
-
const errorEvent = `data: ${JSON.stringify({
|
|
3480
|
-
error: 'Stream processing error occurred'
|
|
3481
|
-
})}\n\n`;
|
|
3482
|
-
res.write(errorEvent);
|
|
3483
|
-
res.end();
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
catch (writeError) {
|
|
3487
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3488
|
-
}
|
|
3489
|
-
yield finalizeLog(500, error.message);
|
|
3490
|
-
}
|
|
3491
|
-
}));
|
|
3492
|
-
return;
|
|
3493
|
-
}
|
|
3494
|
-
// Gemini / Gemini Chat -> Claude Code 流式转换
|
|
3495
|
-
if (useLegacySpecialSSEBranches && targetType === 'claude-code' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
3496
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3497
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3498
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3499
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3500
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3501
|
-
const converter = new streaming_1.GeminiToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3502
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3503
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3504
|
-
const finalizeChunks = () => {
|
|
3505
|
-
const usage = converter.getUsage();
|
|
3506
|
-
if (usage) {
|
|
3507
|
-
usageForLog = {
|
|
3508
|
-
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
3509
|
-
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
3510
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
else {
|
|
3514
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3515
|
-
if (extractedUsage) {
|
|
3516
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3520
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3521
|
-
console.log('[Proxy] Gemini stream request finished (claude-code), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3522
|
-
void finalizeLog(res.statusCode);
|
|
3523
|
-
};
|
|
3524
|
-
eventCollector.on('finish', () => {
|
|
3525
|
-
console.log('[Proxy] EventCollector finished (gemini->claude-code), collecting chunks...');
|
|
3526
|
-
finalizeChunks();
|
|
3527
|
-
});
|
|
3528
|
-
res.on('finish', () => {
|
|
3529
|
-
console.log('[Proxy] Response finished (gemini->claude-code)');
|
|
3530
|
-
if (!streamChunksForLog) {
|
|
3531
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3532
|
-
finalizeChunks();
|
|
3533
|
-
}
|
|
3534
|
-
});
|
|
3535
|
-
res.on('error', (err) => {
|
|
3536
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3537
|
-
});
|
|
3538
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3539
|
-
var _a, _b;
|
|
3540
|
-
if (error) {
|
|
3541
|
-
console.error('[Proxy] Pipeline error for gemini->claude-code:', error);
|
|
3542
|
-
try {
|
|
3543
|
-
const vendors = this.dbManager.getVendors();
|
|
3544
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3545
|
-
yield this.dbManager.addErrorLog({
|
|
3546
|
-
timestamp: Date.now(),
|
|
3547
|
-
method: req.method,
|
|
3548
|
-
path: req.path,
|
|
3549
|
-
statusCode: 500,
|
|
3550
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3551
|
-
errorStack: error.stack,
|
|
3552
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3553
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3554
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3555
|
-
responseHeaders: responseHeadersForLog,
|
|
3556
|
-
ruleId: rule.id,
|
|
3557
|
-
targetType,
|
|
3558
|
-
targetServiceId: service.id,
|
|
3559
|
-
targetServiceName: service.name,
|
|
3560
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3561
|
-
vendorId: service.vendorId,
|
|
3562
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3563
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3564
|
-
responseTime: Date.now() - startTime,
|
|
3565
|
-
});
|
|
3566
|
-
}
|
|
3567
|
-
catch (logError) {
|
|
3568
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3569
|
-
}
|
|
3570
|
-
try {
|
|
3571
|
-
if (!res.writableEnded) {
|
|
3572
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({
|
|
3573
|
-
type: 'error',
|
|
3574
|
-
error: {
|
|
3575
|
-
type: 'api_error',
|
|
3576
|
-
message: 'Stream processing error occurred'
|
|
3577
|
-
}
|
|
3578
|
-
})}\n\n`;
|
|
3579
|
-
res.write(errorEvent);
|
|
3580
|
-
res.end();
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
catch (writeError) {
|
|
3584
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3585
|
-
}
|
|
3586
|
-
yield finalizeLog(500, error.message);
|
|
3587
|
-
}
|
|
3588
|
-
}));
|
|
3589
|
-
return;
|
|
3590
|
-
}
|
|
3591
|
-
// Gemini / Gemini Chat -> Codex 流式转换
|
|
3592
|
-
if (useLegacySpecialSSEBranches && targetType === 'codex' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
3593
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
3594
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
3595
|
-
res.setHeader('Connection', 'keep-alive');
|
|
3596
|
-
const parser = new streaming_1.SSEParserTransform();
|
|
3597
|
-
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3598
|
-
const converter = new streaming_1.GeminiToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
|
|
3599
|
-
const serializer = new streaming_1.SSESerializerTransform();
|
|
3600
|
-
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3601
|
-
const finalizeChunks = () => {
|
|
3602
|
-
const usage = converter.getUsage();
|
|
3603
|
-
if (usage) {
|
|
3604
|
-
usageForLog = {
|
|
3605
|
-
inputTokens: usage.prompt_tokens,
|
|
3606
|
-
outputTokens: usage.completion_tokens,
|
|
3607
|
-
totalTokens: usage.total_tokens,
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
3610
|
-
else {
|
|
3611
|
-
const extractedUsage = eventCollector.extractUsage();
|
|
3612
|
-
if (extractedUsage) {
|
|
3613
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
streamChunksForLog = eventCollector.getChunks();
|
|
3617
|
-
responseBodyForLog = streamChunksForLog.join('\n');
|
|
3618
|
-
console.log('[Proxy] Gemini stream request finished (codex), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
3619
|
-
void finalizeLog(res.statusCode);
|
|
3620
|
-
};
|
|
3621
|
-
eventCollector.on('finish', () => {
|
|
3622
|
-
console.log('[Proxy] EventCollector finished (gemini->codex), collecting chunks...');
|
|
3623
|
-
finalizeChunks();
|
|
3624
|
-
});
|
|
3625
|
-
res.on('finish', () => {
|
|
3626
|
-
console.log('[Proxy] Response finished (gemini->codex)');
|
|
3627
|
-
if (!streamChunksForLog) {
|
|
3628
|
-
console.log('[Proxy] Chunks not collected yet, forcing collection...');
|
|
3629
|
-
finalizeChunks();
|
|
3630
|
-
}
|
|
3631
|
-
});
|
|
3632
|
-
res.on('error', (err) => {
|
|
3633
|
-
console.error('[Proxy] Response stream error:', err);
|
|
3634
|
-
});
|
|
3635
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3636
|
-
var _a, _b;
|
|
3637
|
-
if (error) {
|
|
3638
|
-
console.error('[Proxy] Pipeline error for gemini->codex:', error);
|
|
3639
|
-
try {
|
|
3640
|
-
const vendors = this.dbManager.getVendors();
|
|
3641
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3642
|
-
yield this.dbManager.addErrorLog({
|
|
3643
|
-
timestamp: Date.now(),
|
|
3644
|
-
method: req.method,
|
|
3645
|
-
path: req.path,
|
|
3646
|
-
statusCode: 500,
|
|
3647
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3648
|
-
errorStack: error.stack,
|
|
3649
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3650
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3651
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3652
|
-
responseHeaders: responseHeadersForLog,
|
|
3653
|
-
ruleId: rule.id,
|
|
3654
|
-
targetType,
|
|
3655
|
-
targetServiceId: service.id,
|
|
3656
|
-
targetServiceName: service.name,
|
|
3657
|
-
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3658
|
-
vendorId: service.vendorId,
|
|
3659
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3660
|
-
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
3661
|
-
responseTime: Date.now() - startTime,
|
|
3662
|
-
});
|
|
3663
|
-
}
|
|
3664
|
-
catch (logError) {
|
|
3665
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3666
|
-
}
|
|
3667
|
-
try {
|
|
3668
|
-
if (!res.writableEnded) {
|
|
3669
|
-
const errorEvent = `data: ${JSON.stringify({
|
|
3670
|
-
error: 'Stream processing error occurred'
|
|
3671
|
-
})}\n\n`;
|
|
3672
|
-
res.write(errorEvent);
|
|
3673
|
-
res.end();
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
catch (writeError) {
|
|
3677
|
-
console.error('[Proxy] Failed to send error event:', writeError);
|
|
3678
|
-
}
|
|
3679
|
-
yield finalizeLog(500, error.message);
|
|
3680
|
-
}
|
|
3681
|
-
}));
|
|
3682
|
-
return;
|
|
3683
|
-
}
|
|
3684
3404
|
// 默认stream处理(无转换)
|
|
3685
3405
|
const parser = new streaming_1.SSEParserTransform();
|
|
3686
3406
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3687
3407
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
3688
3408
|
const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
|
|
3409
|
+
const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
|
|
3410
|
+
? new ClaudeCompactResponseSanitizer()
|
|
3411
|
+
: null;
|
|
3689
3412
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3690
3413
|
// 使用 transformSSEToTool 方法选择转换器
|
|
3691
|
-
const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType
|
|
3414
|
+
const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
|
|
3692
3415
|
this.copyResponseHeaders(responseHeaders, res);
|
|
3693
3416
|
// 收集日志:responseBody/streamChunks 记录上游原始响应;downstreamResponseBody 记录实际下发内容
|
|
3694
3417
|
const finalizeChunks = () => {
|
|
@@ -3721,7 +3444,12 @@ class ProxyServer {
|
|
|
3721
3444
|
ensureResponseWritable();
|
|
3722
3445
|
return yield new Promise((resolve, reject) => {
|
|
3723
3446
|
if (converter) {
|
|
3724
|
-
|
|
3447
|
+
const streamStages = [response.data, parser, eventCollector, converter];
|
|
3448
|
+
if (compactResponseSanitizer) {
|
|
3449
|
+
streamStages.push(compactResponseSanitizer);
|
|
3450
|
+
}
|
|
3451
|
+
streamStages.push(serializer, downstreamChunkCollector, res);
|
|
3452
|
+
(0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], streamStages[3], ...streamStages.slice(4), (error) => {
|
|
3725
3453
|
if (error) {
|
|
3726
3454
|
reject(error);
|
|
3727
3455
|
return;
|
|
@@ -3730,7 +3458,12 @@ class ProxyServer {
|
|
|
3730
3458
|
});
|
|
3731
3459
|
return;
|
|
3732
3460
|
}
|
|
3733
|
-
|
|
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) => {
|
|
3734
3467
|
if (error) {
|
|
3735
3468
|
reject(error);
|
|
3736
3469
|
return;
|
|
@@ -3850,15 +3583,18 @@ class ProxyServer {
|
|
|
3850
3583
|
}
|
|
3851
3584
|
// 使用统一的响应转换方法
|
|
3852
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;
|
|
3853
3589
|
// 提取 token usage(从原始响应数据中提取)
|
|
3854
3590
|
usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
|
|
3855
3591
|
console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
|
|
3856
3592
|
this.copyResponseHeaders(responseHeaders, res);
|
|
3857
|
-
if (
|
|
3593
|
+
if (normalizedConverted && normalizedConverted !== responseData) {
|
|
3858
3594
|
// 非流式:responseBody 记录上游原始响应,downstreamResponseBody 记录转换后下发内容
|
|
3859
3595
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
3860
|
-
downstreamResponseBodyForLog = JSON.stringify(
|
|
3861
|
-
res.status(response.status).json(
|
|
3596
|
+
downstreamResponseBodyForLog = JSON.stringify(normalizedConverted);
|
|
3597
|
+
res.status(response.status).json(normalizedConverted);
|
|
3862
3598
|
}
|
|
3863
3599
|
else {
|
|
3864
3600
|
// 没有转换,使用原始数据
|
|
@@ -3976,7 +3712,7 @@ class ProxyServer {
|
|
|
3976
3712
|
}
|
|
3977
3713
|
// 根据请求类型返回适当格式的错误响应
|
|
3978
3714
|
const streamRequested = this.isStreamRequested(req, req.body || {}, targetType, sourceType);
|
|
3979
|
-
if (
|
|
3715
|
+
if (targetType === 'claude-code') {
|
|
3980
3716
|
// 对于 Claude Code,返回符合 Claude API 标准的错误响应
|
|
3981
3717
|
const claudeError = {
|
|
3982
3718
|
type: 'error',
|
|
@@ -4018,13 +3754,13 @@ class ProxyServer {
|
|
|
4018
3754
|
// 这个方法主要用于初始化和日志记录
|
|
4019
3755
|
// 修改数据库后无需调用此方法,配置会自动生效
|
|
4020
3756
|
const allRoutes = this.dbManager.getRoutes();
|
|
4021
|
-
const
|
|
3757
|
+
const allRoutesList = allRoutes;
|
|
4022
3758
|
const allServices = this.dbManager.getAPIServices();
|
|
4023
3759
|
// 保留缓存以备将来可能的性能优化需求
|
|
4024
|
-
this.routes =
|
|
3760
|
+
this.routes = allRoutesList;
|
|
4025
3761
|
if (this.rules) {
|
|
4026
3762
|
this.rules.clear();
|
|
4027
|
-
for (const route of
|
|
3763
|
+
for (const route of allRoutesList) {
|
|
4028
3764
|
const routeRules = this.dbManager.getRules(route.id);
|
|
4029
3765
|
const sortedRules = [...routeRules].sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
|
|
4030
3766
|
this.rules.set(route.id, sortedRules);
|
|
@@ -4037,7 +3773,7 @@ class ProxyServer {
|
|
|
4037
3773
|
services.set(service.id, service);
|
|
4038
3774
|
});
|
|
4039
3775
|
}
|
|
4040
|
-
console.log(`Initialized with ${
|
|
3776
|
+
console.log(`Initialized with ${allRoutesList.length} routes and ${allServices.length} services (all config read from database in real-time)`);
|
|
4041
3777
|
});
|
|
4042
3778
|
}
|
|
4043
3779
|
updateConfig(config) {
|
|
@@ -4051,5 +3787,523 @@ class ProxyServer {
|
|
|
4051
3787
|
yield this.reloadRoutes();
|
|
4052
3788
|
});
|
|
4053
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
|
+
}
|
|
4054
4308
|
}
|
|
4055
4309
|
exports.ProxyServer = ProxyServer;
|