aicodeswitch 4.0.3 → 5.0.0

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