aicodeswitch 4.0.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +6 -5
  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 +887 -633
  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-Dl-B9pXM.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,7 +286,46 @@ class ProxyServer {
167
286
  });
168
287
  }
169
288
  initialize() {
170
- // Dynamic proxy middleware
289
+ // === 标准 API 路径前置中间件 ===
290
+ // 处理 /v1/models 和 4 个可绑定的标准 API 路径
291
+ this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
292
+ const apiPath = matchApiPath(req.path);
293
+ if (!apiPath) {
294
+ return next();
295
+ }
296
+ // /v1/models: 直接返回静态模型列表
297
+ if (apiPath === '/v1/models') {
298
+ // 鉴权
299
+ if (this.config.apiKey) {
300
+ const authHeader = req.headers.authorization;
301
+ const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
302
+ if (!providedKey || providedKey !== this.config.apiKey) {
303
+ res.status(401).json({ error: { message: 'Invalid API key' } });
304
+ return;
305
+ }
306
+ }
307
+ res.json(buildModelsResponse(this.dbManager.getApiPathModels()));
308
+ return;
309
+ }
310
+ // 其余 4 个路径:查找绑定
311
+ const bindings = this.dbManager.getApiPathBindings();
312
+ const binding = bindings.find((b) => b.apiPath === apiPath);
313
+ if (!binding || !binding.routeId) {
314
+ res.status(404).json({ error: { message: `API path ${apiPath} is not bound to any route. Please configure it in Route Mapping settings.` } });
315
+ return;
316
+ }
317
+ // 加载绑定的路由
318
+ const allRoutes = this.dbManager.getRoutes();
319
+ const route = allRoutes.find((r) => r.id === binding.routeId);
320
+ if (!route) {
321
+ return res.status(404).json({ error: { message: `Bound route '${binding.routeId}' not found or inactive. Please check Route Mapping settings.` } });
322
+ }
323
+ // 推断客户端格式
324
+ const clientFormat = apiPathToClientFormat(apiPath);
325
+ // 复用完整的代理请求处理
326
+ yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath);
327
+ }));
328
+ // Dynamic proxy middleware (原有的 /claude-code, /codex 逻辑)
171
329
  this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
172
330
  var _a, _b, _c, _d, _e;
173
331
  // 仅处理支持的目标路径
@@ -196,7 +354,7 @@ class ProxyServer {
196
354
  return res.status(404).json({ error: 'No matching route found and no original config available' });
197
355
  }
198
356
  // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
199
- const forcedContentType = yield this.prepareHighIqRouting(req, route, route.targetType);
357
+ const forcedContentType = yield this.prepareHighIqRouting(req, route, this.inferTargetTypeFromPath(req.path) || 'claude-code');
200
358
  const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
201
359
  if (!enableFailover) {
202
360
  // 故障切换已禁用,使用传统的单一规则匹配
@@ -210,7 +368,7 @@ class ProxyServer {
210
368
  yield this.logToolRequest(req, {
211
369
  statusCode: 404,
212
370
  responseTime: Date.now() - requestStartAt,
213
- targetType: route.targetType,
371
+ targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
214
372
  error: 'No matching rule found',
215
373
  tags: this.buildRelayTags(false),
216
374
  });
@@ -221,7 +379,7 @@ class ProxyServer {
221
379
  yield this.logToolRequest(req, {
222
380
  statusCode: 500,
223
381
  responseTime: Date.now() - requestStartAt,
224
- targetType: route.targetType,
382
+ targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
225
383
  error: 'Target service not configured',
226
384
  tags: this.buildRelayTags(false),
227
385
  });
@@ -242,7 +400,7 @@ class ProxyServer {
242
400
  yield this.logToolRequest(req, {
243
401
  statusCode: 404,
244
402
  responseTime: Date.now() - requestStartAt,
245
- targetType: route.targetType,
403
+ targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
246
404
  error: 'No matching rule found',
247
405
  tags: this.buildRelayTags(false),
248
406
  });
@@ -333,7 +491,7 @@ class ProxyServer {
333
491
  yield this.logToolRequest(req, {
334
492
  statusCode: 503,
335
493
  responseTime: Date.now() - requestStartAt,
336
- targetType: route.targetType,
494
+ targetType: this.inferTargetTypeFromPath(req.path) || 'claude-code',
337
495
  error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
338
496
  tags: this.buildRelayTags(hasRelayAttempt),
339
497
  });
@@ -692,9 +850,6 @@ class ProxyServer {
692
850
  * 从数据库实时获取所有活跃路由
693
851
  * @returns 活跃路由列表
694
852
  */
695
- getActiveRoutes() {
696
- return this.dbManager.getRoutes().filter(route => route.isActive);
697
- }
698
853
  /**
699
854
  * 从数据库实时获取指定路由的规则
700
855
  * @param routeId 路由ID
@@ -708,20 +863,19 @@ class ProxyServer {
708
863
  return this.dbManager.getRule(ruleId);
709
864
  }
710
865
  findMatchingRoute(req) {
711
- // 根据请求路径确定目标类型
712
- let targetType;
866
+ let tool;
713
867
  if (req.path.startsWith('/claude-code/')) {
714
- targetType = 'claude-code';
868
+ tool = 'claude-code';
715
869
  }
716
870
  else if (req.path.startsWith('/codex/')) {
717
- targetType = 'codex';
871
+ tool = 'codex';
718
872
  }
719
- if (!targetType) {
873
+ if (!tool)
720
874
  return undefined;
721
- }
722
- // 返回匹配目标类型且处于活跃状态的路由
723
- const activeRoutes = this.getActiveRoutes();
724
- 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);
725
879
  }
726
880
  /**
727
881
  * 当没有激活的路由时,fallback 到原始配置
@@ -772,8 +926,6 @@ class ProxyServer {
772
926
  const tempRoute = {
773
927
  id: 'fallback-route',
774
928
  name: 'Fallback to Original Config',
775
- targetType: targetType,
776
- isActive: true,
777
929
  createdAt: Date.now(),
778
930
  updatedAt: Date.now(),
779
931
  };
@@ -872,9 +1024,11 @@ class ProxyServer {
872
1024
  }
873
1025
  return undefined;
874
1026
  }
875
- findRouteByTargetType(targetType) {
876
- const activeRoutes = this.getActiveRoutes();
877
- 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);
878
1032
  }
879
1033
  findNextAvailableServiceName(allRules, startIndex, routeId) {
880
1034
  return __awaiter(this, void 0, void 0, function* () {
@@ -1175,8 +1329,7 @@ class ProxyServer {
1175
1329
  return undefined;
1176
1330
  const body = req.body;
1177
1331
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
1178
- const route = this.dbManager.getRoutes().find(r => r.id === routeId);
1179
- const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
1332
+ const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
1180
1333
  // 高智商规则优先于 model-mapping,确保 !!/推断命中时不会被模型映射覆盖
1181
1334
  if (contentType === 'high-iq') {
1182
1335
  const highIqRules = enabledRules.filter(rule => rule.contentType === 'high-iq');
@@ -1292,8 +1445,7 @@ class ProxyServer {
1292
1445
  const body = req.body;
1293
1446
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
1294
1447
  const candidates = [];
1295
- const route = this.dbManager.getRoutes().find(r => r.id === routeId);
1296
- const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
1448
+ const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
1297
1449
  const prioritizeContentType = contentType === 'high-iq';
1298
1450
  const modelMappingRules = requestModel
1299
1451
  ? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
@@ -1446,6 +1598,16 @@ class ProxyServer {
1446
1598
  }
1447
1599
  getContentTypeDetectors() {
1448
1600
  return [
1601
+ {
1602
+ type: 'compact',
1603
+ match: (req, body) => {
1604
+ if ((0, compact_1.isCodexCompactRequest)(req.path) || (0, compact_1.isCodexCompactRequest)(req.originalUrl)) {
1605
+ return true;
1606
+ }
1607
+ const messages = this.extractConversationMessages(body);
1608
+ return (0, compact_1.isLastClaudeMessageCompact)(messages);
1609
+ },
1610
+ },
1449
1611
  {
1450
1612
  type: 'image-understanding',
1451
1613
  match: (_req, body) => this.containsImageContentInLatestMessage(body.messages) || this.containsImageContent(body.input),
@@ -1533,6 +1695,10 @@ class ProxyServer {
1533
1695
  image_understanding: 'image-understanding',
1534
1696
  'image-understanding': 'image-understanding',
1535
1697
  vision: 'image-understanding',
1698
+ compact: 'compact',
1699
+ compaction: 'compact',
1700
+ summarize: 'compact',
1701
+ summary: 'compact',
1536
1702
  };
1537
1703
  return mapping[normalized] || null;
1538
1704
  }
@@ -2101,21 +2267,13 @@ class ProxyServer {
2101
2267
  // 向下兼容:支持旧类型 'claude-code'
2102
2268
  return sourceType === 'claude' || sourceType === 'claude-code';
2103
2269
  }
2104
- /** 判断是否为 Claude Chat 类型 */
2105
- isClaudeChatSource(sourceType) {
2106
- return sourceType === 'claude-chat';
2107
- }
2108
2270
  isOpenAISource(sourceType) {
2109
2271
  // 向下兼容:支持旧类型 'openai-responses'
2110
2272
  return sourceType === 'openai' || sourceType === 'openai-responses';
2111
2273
  }
2112
2274
  /** 判断是否为 OpenAI Chat 类型 */
2113
2275
  isOpenAIChatSource(sourceType) {
2114
- return sourceType === 'openai-chat' || sourceType === 'deepseek-reasoning-chat';
2115
- }
2116
- /** 判断是否为 OpenAI 类型(包括 OpenAI Chat 和 OpenAI Responses) */
2117
- isOpenAIType(sourceType) {
2118
- return sourceType === 'openai' || sourceType === 'openai-chat' || sourceType === 'openai-responses' || sourceType === 'deepseek-reasoning-chat';
2276
+ return sourceType === 'openai-chat';
2119
2277
  }
2120
2278
  /** 判断是否为 Gemini 类型 */
2121
2279
  isGeminiSource(sourceType) {
@@ -2245,7 +2403,7 @@ class ProxyServer {
2245
2403
  headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
2246
2404
  }
2247
2405
  }
2248
- // 使用 Authorization 认证(适用于 openai-chat, openai-responses, deepseek-reasoning-chat 等)
2406
+ // 使用 Authorization 认证(适用于 openai-chat, openai-responses 等)
2249
2407
  else {
2250
2408
  headers.authorization = `Bearer ${effectiveApiKey}`;
2251
2409
  }
@@ -2282,6 +2440,17 @@ class ProxyServer {
2282
2440
  }
2283
2441
  return vendor.apiKey || '';
2284
2442
  }
2443
+ resolveEffectiveApiUrl(service) {
2444
+ if (service.inheritVendorApiBaseUrl !== true) {
2445
+ return service.apiUrl;
2446
+ }
2447
+ const vendor = this.dbManager.getVendorByServiceId(service.id);
2448
+ if (!vendor || !vendor.apiBaseUrl) {
2449
+ console.warn(`[Proxy] Service ${service.id} is set to inherit vendor API base URL, but vendor/url is missing`);
2450
+ return service.apiUrl;
2451
+ }
2452
+ return vendor.apiBaseUrl;
2453
+ }
2285
2454
  copyResponseHeaders(responseHeaders, res) {
2286
2455
  Object.keys(responseHeaders).forEach((key) => {
2287
2456
  if (!['content-encoding', 'transfer-encoding', 'connection', 'content-length'].includes(key.toLowerCase())) {
@@ -2400,8 +2569,8 @@ class ProxyServer {
2400
2569
  return ProxyServer.extractSessionIdFromUserId(rawUserId);
2401
2570
  }
2402
2571
  else if (type === 'codex') {
2403
- // Codex 使用 headers.session_id
2404
- const sessionId = request.headers['session_id'];
2572
+ // Codex 使用 headers 中的 session-id 或 session_id(兼容新旧版本)
2573
+ const sessionId = request.headers['session-id'] || request.headers['session_id'];
2405
2574
  if (typeof sessionId === 'string') {
2406
2575
  return sessionId;
2407
2576
  }
@@ -2592,48 +2761,19 @@ class ProxyServer {
2592
2761
  * @param targetModel 目标模型名称(可选)
2593
2762
  * @returns 转换后往服务商API接口的数据
2594
2763
  */
2595
- transformRequestToUpstream(tool, source, payloadData, targetModel) {
2596
- // Claude Code 发起的请求
2597
- if (tool === 'claude-code') {
2598
- // claudecode向claude发送的请求,无需转换,但需要应用模型覆盖
2599
- if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
2600
- return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
2601
- }
2602
- // claudecode发送给gemini
2603
- if (this.isGeminiChatSource(source) || this.isGeminiSource(source)) {
2604
- return (0, transformers_1.transformRequestFromClaudeToGemini)(payloadData, targetModel);
2605
- }
2606
- // claudecode发送给openai chat completion接口
2607
- if (this.isOpenAIChatSource(source)) {
2608
- return (0, transformers_1.transformRequestFromClaudeToChatCompletions)(payloadData, targetModel);
2609
- }
2610
- // claudecode发送给openai responses接口
2611
- if (this.isOpenAISource(source)) {
2612
- return (0, transformers_1.transformRequestFromClaudeToResponses)(payloadData, targetModel);
2613
- }
2614
- }
2615
- // Codex 发起的请求(仅支持 Responses API 格式)
2616
- if (tool === 'codex') {
2617
- // Codex 发送给 OpenAI Responses
2618
- if (this.isOpenAISource(source)) {
2619
- return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
2620
- }
2621
- // Codex 发送给 OpenAI Chat
2622
- if (this.isOpenAIChatSource(source)) {
2623
- // 将 responses 格式转换为 chat completions 格式
2624
- return (0, transformers_1.transformRequestFromResponsesToChatCompletions)(payloadData, targetModel);
2625
- }
2626
- // Codex 发送给 Gemini
2627
- if (this.isGeminiChatSource(source) || this.isGeminiSource(source)) {
2628
- return (0, transformers_1.transformRequestFromResponsesToGemini)(payloadData, targetModel);
2629
- }
2630
- // Codex 发送给 Claude
2631
- if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
2632
- return (0, transformers_1.transformRequestFromResponsesToClaude)(payloadData, targetModel);
2633
- }
2634
- }
2635
- // 默认: 直接返回原始数据(如果需要,应用模型覆盖)
2636
- return (0, transformers_1.applyPayloadOverride)(payloadData, targetModel);
2764
+ transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig) {
2765
+ const clientFormat = tool === 'codex' ? 'responses' : 'claude';
2766
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
2767
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
2768
+ const body = result.body;
2769
+ // 模型覆盖:OpenAI 模型族保持原样,其余覆盖为 targetModel
2770
+ if (targetModel) {
2771
+ const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
2772
+ if (!isOpenAIModel) {
2773
+ body.model = targetModel;
2774
+ }
2775
+ }
2776
+ return body;
2637
2777
  }
2638
2778
  /**
2639
2779
  * 将来自API接口的响应数据,转换为工具需要的数据结构
@@ -2642,158 +2782,102 @@ class ProxyServer {
2642
2782
  * @param responseData
2643
2783
  */
2644
2784
  transformResponseToTool(tool, source, responseData) {
2645
- if (tool === 'claude-code') {
2646
- if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
2647
- return responseData;
2648
- }
2649
- if (this.isOpenAIChatSource(source)) {
2650
- return (0, transformers_1.transformResponseFromChatCompletionsToClaude)(responseData);
2651
- }
2652
- if (this.isOpenAISource(source)) {
2653
- return (0, transformers_1.transformResponseFromResponsesToClaude)(responseData);
2654
- }
2655
- if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
2656
- return (0, transformers_1.transformResponseFromGeminiToClaude)(responseData);
2657
- }
2658
- }
2659
- if (tool === 'codex') {
2660
- // Codex 仅支持 Responses API 格式
2661
- // Codex 接收来自 OpenAI Responses 的响应
2662
- if (this.isOpenAISource(source)) {
2663
- return responseData;
2664
- }
2665
- // Codex 接收来自 OpenAI Chat 的响应(转换为 Responses 格式)
2666
- if (this.isOpenAIChatSource(source)) {
2667
- return (0, transformers_1.transformResponseFromChatCompletionsToResponses)(responseData);
2668
- }
2669
- // Codex 接收来自 Claude 的响应(转换为 Responses 格式)
2670
- if (this.isClaudeSource(source) || this.isClaudeChatSource(source)) {
2671
- return (0, transformers_1.transformResponseFromClaudeToResponses)(responseData);
2672
- }
2673
- // Codex 接收来自 Gemini 的响应(转换为 Responses 格式)
2674
- if (this.isGeminiSource(source) || this.isGeminiChatSource(source)) {
2675
- return (0, transformers_1.transformResponseFromGeminiToResponses)(responseData);
2676
- }
2677
- }
2785
+ const clientFormat = tool === 'codex' ? 'responses' : 'claude';
2786
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
2787
+ return (0, index_1.transformResponse)({ fromFormat: upstreamFormat, toFormat: clientFormat, response: responseData });
2678
2788
  }
2679
2789
  /**
2680
2790
  * 获取流式响应转换器
2681
2791
  * @param targetType 目标工具类型
2682
2792
  * @param sourceType 数据源类型
2683
- * @param model 模型名称
2684
2793
  * @returns 转换器实例和相关信息
2685
2794
  */
2686
- transformSSEToTool(targetType, sourceType, model) {
2687
- // Claude Code 接收流式响应
2688
- if (targetType === 'claude-code') {
2689
- // Claude Code 接收来自 OpenAI Chat 的响应
2690
- if (this.isOpenAIChatSource(sourceType)) {
2691
- console.log('[Proxy] Using OpenAI Chat -> Claude API stream converter');
2692
- return {
2693
- converter: new streaming_1.ChatCompletionsToClaudeEventTransform({ model }),
2694
- extractUsage: (usage) => ({
2695
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2696
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2697
- cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
2698
- }),
2699
- };
2700
- }
2701
- // Claude Code 接收来自 OpenAI Responses 的响应
2702
- if (this.isOpenAISource(sourceType)) {
2703
- console.log('[Proxy] Using Responses API -> Claude API stream converter');
2704
- return {
2705
- converter: new streaming_1.ResponsesToClaudeEventTransform({ model }),
2706
- extractUsage: (usage) => ({
2707
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2708
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2709
- cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
2710
- }),
2711
- };
2712
- }
2713
- // Claude Code 接收来自 Gemini 的响应
2714
- if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
2715
- return {
2716
- converter: new streaming_1.GeminiToClaudeEventTransform({ model }),
2717
- extractUsage: (usage) => ({
2718
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2719
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2720
- cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
2721
- }),
2722
- };
2723
- }
2724
- }
2725
- // Codex 接收流式响应
2726
- if (targetType === 'codex') {
2727
- // Codex 接收来自 Claude 的响应
2728
- if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
2729
- console.log('[Proxy] Using Claude -> Responses API stream converter');
2730
- return {
2731
- converter: new streaming_1.ClaudeToResponsesEventTransform({ model }),
2732
- extractUsage: (usage) => ({
2733
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2734
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2735
- totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
2736
- }),
2737
- };
2738
- }
2739
- // Codex 接收来自 OpenAI Chat 的响应
2740
- if (this.isOpenAIChatSource(sourceType)) {
2741
- console.log('[Proxy] Using OpenAI Chat -> Responses API stream converter');
2742
- return {
2743
- converter: new streaming_1.ChatCompletionsToResponsesEventTransform({ model }),
2744
- extractUsage: (usage) => ({
2745
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2746
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2747
- totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
2748
- }),
2749
- };
2750
- }
2751
- // Codex 接收来自 Gemini 的响应
2752
- if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
2753
- console.log('[Proxy] Using Gemini -> Responses API stream converter');
2754
- return {
2755
- converter: new streaming_1.GeminiToResponsesEventTransform({ model }),
2756
- extractUsage: (usage) => ({
2757
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2758
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2759
- totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
2760
- }),
2761
- };
2762
- }
2763
- }
2764
- // 默认:返回空转换器(不需要转换)
2765
- return { converter: null };
2795
+ transformSSEToTool(targetType, sourceType) {
2796
+ const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
2797
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(sourceType);
2798
+ if (upstreamFormat === clientFormat) {
2799
+ return { converter: null };
2800
+ }
2801
+ const streamConverter = (0, index_1.createStreamConverter)({ fromFormat: upstreamFormat, toFormat: clientFormat });
2802
+ const adapter = new stream_converter_adapter_1.StreamConverterAdapter(streamConverter);
2803
+ const extractUsage = clientFormat === 'claude'
2804
+ ? (usage) => ({
2805
+ inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2806
+ outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2807
+ cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
2808
+ })
2809
+ : (usage) => ({
2810
+ inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
2811
+ outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
2812
+ totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
2813
+ });
2814
+ return { converter: adapter, extractUsage };
2766
2815
  }
2767
2816
  extractTokenUsageFromResponse(responseData, sourceType) {
2817
+ var _a, _b, _c, _d, _e, _f;
2768
2818
  if (!responseData)
2769
2819
  return undefined;
2770
- // Gemini 使用 usageMetadata 字段
2771
- if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
2772
- 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
+ };
2773
2831
  }
2774
- // OpenAI 使用 usage 字段
2775
- if (this.isOpenAIChatSource(sourceType) || this.isOpenAISource(sourceType)) {
2776
- 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
+ };
2777
2842
  }
2778
- // Claude:responseData 可能是 usage 对象本身,也可能包含 usage 字段
2779
- if (this.isClaudeSource(sourceType) || this.isClaudeChatSource(sourceType)) {
2780
- // 如果 responseData 直接包含 input_tokens/output_tokens,说明它本身就是 usage 对象
2843
+ if (format === 'claude') {
2781
2844
  if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) === 'number' || typeof (responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) === 'number') {
2782
- return (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
+ };
2783
2851
  }
2784
- // 否则尝试从 usage 字段提取
2785
- 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
+ };
2786
2861
  }
2862
+ // 通用 fallback
2787
2863
  const usage = responseData.usage;
2788
2864
  if (!usage)
2789
2865
  return undefined;
2790
- // OpenAI 使用 prompt_tokens 和 completion_tokens
2791
2866
  if (typeof (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) === 'number' || typeof (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) === 'number') {
2792
- return (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
+ };
2793
2873
  }
2794
- // Claude 使用 input_tokens 和 output_tokens
2795
2874
  if (typeof (usage === null || usage === void 0 ? void 0 : usage.input_tokens) === 'number' || typeof (usage === null || usage === void 0 ? void 0 : usage.output_tokens) === 'number') {
2796
- return (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
+ };
2797
2881
  }
2798
2882
  return undefined;
2799
2883
  }
@@ -2805,7 +2889,7 @@ class ProxyServer {
2805
2889
  const rawSourceType = service.sourceType || 'openai-chat';
2806
2890
  // 标准化 sourceType,将旧类型转换为新类型(向下兼容)
2807
2891
  const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
2808
- const targetType = route.targetType;
2892
+ const targetType = this.inferToolFromRequest(req);
2809
2893
  const sessionId = this.defaultExtractSessionId(req, targetType) || '-';
2810
2894
  const vendor = this.dbManager.getVendorByServiceId(service.id);
2811
2895
  console.log(`\x1b[32m[Request Start]\x1b[0m client=${targetType}, session=${sessionId}, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}, model=${rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model) || '-'}`);
@@ -2813,11 +2897,35 @@ class ProxyServer {
2813
2897
  const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
2814
2898
  const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
2815
2899
  let relayedForLog = !useOriginalConfig;
2816
- const originalToolRequestBody = this.cloneRequestBody(req.body || {});
2900
+ let originalToolRequestBody = this.cloneRequestBody(req.body || {});
2817
2901
  let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
2818
2902
  let usageForLog;
2819
2903
  let logged = false;
2820
2904
  const extraTagsForLog = [];
2905
+ // 编程套餐限制检查
2906
+ const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
2907
+ if (!this.checkCodingPlan(req, res, service, clientFormat))
2908
+ return;
2909
+ // Compact 请求消息清理:确保 tool_use/tool_result 配对完整
2910
+ if (rule.contentType === 'compact' && targetType === 'claude-code') {
2911
+ if (Array.isArray(originalToolRequestBody === null || originalToolRequestBody === void 0 ? void 0 : originalToolRequestBody.messages)) {
2912
+ originalToolRequestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(originalToolRequestBody.messages);
2913
+ if (this.isClaudeSource(sourceType)) {
2914
+ originalToolRequestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(originalToolRequestBody.messages);
2915
+ }
2916
+ }
2917
+ originalToolRequestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(originalToolRequestBody);
2918
+ if (Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
2919
+ requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
2920
+ if (this.isClaudeSource(sourceType)) {
2921
+ requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
2922
+ }
2923
+ }
2924
+ requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
2925
+ if (Array.isArray(originalToolRequestBody === null || originalToolRequestBody === void 0 ? void 0 : originalToolRequestBody.messages)) {
2926
+ console.log('[Compact-Sanitize] initial unpaired tool_use count:', (0, compact_1.countUnpairedClaudeToolUses)(originalToolRequestBody.messages));
2927
+ }
2928
+ }
2821
2929
  // MCP 图像理解处理
2822
2930
  let tempImageFiles = [];
2823
2931
  let useMCPProcessing = false;
@@ -3174,8 +3282,22 @@ class ProxyServer {
3174
3282
  try {
3175
3283
  // 使用统一的请求转换方法
3176
3284
  const payloadForTransform = this.cloneRequestBody(originalToolRequestBody);
3177
- const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel);
3285
+ // 获取 provider config 用于驱动 request body 后处理(thinking 参数注入、reasoning 历史修复等)
3286
+ const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3287
+ const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3288
+ const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3289
+ const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig);
3178
3290
  requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
3291
+ // 对最终即将发送到上游的 Claude compact 请求再做一次兜底清理,
3292
+ // 避免中间转换/覆盖步骤重新引入未配对的 tool_use。
3293
+ if (rule.contentType === 'compact' && targetType === 'claude-code' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
3294
+ requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
3295
+ if (this.isClaudeSource(sourceType)) {
3296
+ requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
3297
+ }
3298
+ requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
3299
+ console.log('[Compact-Sanitize] final unpaired tool_use count:', (0, compact_1.countUnpairedClaudeToolUses)(requestBody.messages));
3300
+ }
3179
3301
  // 应用 max_output_tokens 限制
3180
3302
  requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
3181
3303
  if (this.shouldDefaultStreamingForClaudeBridge(req, targetType, sourceType, requestBody)
@@ -3186,16 +3308,16 @@ class ProxyServer {
3186
3308
  const streamRequested = this.isStreamRequested(req, requestBody, targetType, sourceType);
3187
3309
  // Build the full URL by appending the request path to the service API URL
3188
3310
  let pathToRequest = req.path;
3189
- if (route.targetType === 'claude-code' && req.path.startsWith('/claude-code')) {
3311
+ if (targetType === 'claude-code' && req.path.startsWith('/claude-code')) {
3190
3312
  pathToRequest = req.path.slice('/claude-code'.length);
3191
3313
  }
3192
- else if (route.targetType === 'codex' && req.path.startsWith('/codex')) {
3314
+ else if (targetType === 'codex' && req.path.startsWith('/codex')) {
3193
3315
  pathToRequest = req.path.slice('/codex'.length);
3194
3316
  }
3195
3317
  // 使用 mapRequestPathToUpstreamUrl 统一构建上游 URL
3196
3318
  const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3197
- const apiUrl = service.apiUrl;
3198
- 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);
3199
3321
  const config = {
3200
3322
  method: req.method,
3201
3323
  url: upstreamUrl,
@@ -3279,416 +3401,17 @@ class ProxyServer {
3279
3401
  }
3280
3402
  if (isEventStream && response.data) {
3281
3403
  res.status(response.status);
3282
- // 统一走默认流式链路(transformSSEToTool + 统一断连保护),避免历史分支行为不一致
3283
- const useLegacySpecialSSEBranches = false;
3284
- if (useLegacySpecialSSEBranches && targetType === 'claude-code' && this.isOpenAIType(sourceType)) {
3285
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
3286
- res.setHeader('Cache-Control', 'no-cache');
3287
- res.setHeader('Connection', 'keep-alive');
3288
- const parser = new streaming_1.SSEParserTransform();
3289
- const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3290
- const converter = new streaming_1.OpenAIToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
3291
- const serializer = new streaming_1.SSESerializerTransform();
3292
- // 收集响应头
3293
- responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3294
- // 监听事件收集器的完成事件,确保所有chunks都被收集
3295
- const finalizeChunks = () => {
3296
- const usage = converter.getUsage();
3297
- console.log('[Proxy] Claude Code stream: converter usage:', usage);
3298
- if (usage) {
3299
- usageForLog = (0, transformers_1.extractTokenUsageFromClaudeUsage)(usage);
3300
- console.log('[Proxy] Claude Code stream: usageForLog from converter:', usageForLog);
3301
- }
3302
- // 尝试从event collector中提取usage(作为补充)
3303
- const extractedUsage = eventCollector.extractUsage();
3304
- console.log('[Proxy] Claude Code stream: extracted usage from eventCollector:', extractedUsage);
3305
- if (!usageForLog && extractedUsage) {
3306
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
3307
- console.log('[Proxy] Claude Code stream: usageForLog from eventCollector:', usageForLog);
3308
- }
3309
- // 收集stream chunks(每个chunk是一个完整的SSE事件)
3310
- streamChunksForLog = eventCollector.getChunks();
3311
- // 将所有 chunks 合并成完整的响应体用于日志记录
3312
- responseBodyForLog = streamChunksForLog.join('\n');
3313
- console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
3314
- console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
3315
- console.log('[Proxy] Claude Code stream: final usageForLog before finalizeLog:', usageForLog);
3316
- void finalizeLog(res.statusCode);
3317
- };
3318
- // 在pipeline完成且eventCollector flush后执行
3319
- eventCollector.on('finish', () => {
3320
- console.log('[Proxy] EventCollector finished, collecting chunks...');
3321
- finalizeChunks();
3322
- });
3323
- // 备用:如果eventCollector的finish没有触发,监听res的finish
3324
- res.on('finish', () => {
3325
- console.log('[Proxy] Response finished');
3326
- if (!streamChunksForLog) {
3327
- console.log('[Proxy] Chunks not collected yet, forcing collection...');
3328
- finalizeChunks();
3329
- }
3330
- });
3331
- // 监听 res 的错误事件
3332
- res.on('error', (err) => {
3333
- console.error('[Proxy] Response stream error:', err);
3334
- });
3335
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
3336
- var _a, _b;
3337
- if (error) {
3338
- console.error('[Proxy] Pipeline error for claude-code:', error);
3339
- // 记录到错误日志 - 包含请求详情和实际转发信息
3340
- try {
3341
- // 获取供应商信息
3342
- const vendors = this.dbManager.getVendors();
3343
- const vendor = vendors.find(v => v.id === service.vendorId);
3344
- yield this.dbManager.addErrorLog({
3345
- timestamp: Date.now(),
3346
- method: req.method,
3347
- path: req.path,
3348
- statusCode: 500,
3349
- errorMessage: error.message || 'Stream processing error',
3350
- errorStack: error.stack,
3351
- requestHeaders: this.normalizeHeaders(req.headers),
3352
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3353
- upstreamRequest: upstreamRequestForLog,
3354
- responseHeaders: responseHeadersForLog,
3355
- // 添加请求详情
3356
- ruleId: rule.id,
3357
- targetType,
3358
- targetServiceId: service.id,
3359
- targetServiceName: service.name,
3360
- targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
3361
- vendorId: service.vendorId,
3362
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3363
- requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
3364
- responseTime: Date.now() - startTime,
3365
- });
3366
- }
3367
- catch (logError) {
3368
- console.error('[Proxy] Failed to log error:', logError);
3369
- }
3370
- // 尝试向客户端发送错误事件
3371
- try {
3372
- if (!res.writableEnded) {
3373
- const errorEvent = `event: error\ndata: ${JSON.stringify({
3374
- type: 'error',
3375
- error: {
3376
- type: 'api_error',
3377
- message: 'Stream processing error occurred'
3378
- }
3379
- })}\n\n`;
3380
- res.write(errorEvent);
3381
- res.end();
3382
- }
3383
- }
3384
- catch (writeError) {
3385
- console.error('[Proxy] Failed to send error event:', writeError);
3386
- }
3387
- yield finalizeLog(500, error.message);
3388
- }
3389
- }));
3390
- return;
3391
- }
3392
- if (useLegacySpecialSSEBranches && targetType === 'codex' && this.isClaudeSource(sourceType)) {
3393
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
3394
- res.setHeader('Cache-Control', 'no-cache');
3395
- res.setHeader('Connection', 'keep-alive');
3396
- const parser = new streaming_1.SSEParserTransform();
3397
- const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3398
- const converter = new streaming_1.ClaudeToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
3399
- const serializer = new streaming_1.SSESerializerTransform();
3400
- responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3401
- // 监听事件收集器的完成事件,确保所有chunks都被收集
3402
- const finalizeChunks = () => {
3403
- const usage = converter.getUsage();
3404
- console.log('[Proxy] Codex stream: converter usage:', usage);
3405
- if (usage) {
3406
- usageForLog = (0, transformers_1.extractTokenUsageFromOpenAIUsage)(usage);
3407
- console.log('[Proxy] Codex stream: usageForLog from converter:', usageForLog);
3408
- }
3409
- // 尝试从event collector中提取usage(作为补充)
3410
- const extractedUsage = eventCollector.extractUsage();
3411
- console.log('[Proxy] Codex stream: extracted usage from eventCollector:', extractedUsage);
3412
- if (!usageForLog && extractedUsage) {
3413
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
3414
- console.log('[Proxy] Codex stream: usageForLog from eventCollector:', usageForLog);
3415
- }
3416
- streamChunksForLog = eventCollector.getChunks();
3417
- // 将所有 chunks 合并成完整的响应体用于日志记录
3418
- responseBodyForLog = streamChunksForLog.join('\n');
3419
- console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
3420
- console.log('[Proxy] Response body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
3421
- console.log('[Proxy] Codex stream: final usageForLog before finalizeLog:', usageForLog);
3422
- void finalizeLog(res.statusCode);
3423
- };
3424
- // 在pipeline完成且eventCollector flush后执行
3425
- eventCollector.on('finish', () => {
3426
- console.log('[Proxy] EventCollector finished (codex), collecting chunks...');
3427
- finalizeChunks();
3428
- });
3429
- // 备用:如果eventCollector的finish没有触发,监听res的finish
3430
- res.on('finish', () => {
3431
- console.log('[Proxy] Response finished (codex)');
3432
- if (!streamChunksForLog) {
3433
- console.log('[Proxy] Chunks not collected yet, forcing collection...');
3434
- finalizeChunks();
3435
- }
3436
- });
3437
- // 监听 res 的错误事件
3438
- res.on('error', (err) => {
3439
- console.error('[Proxy] Response stream error:', err);
3440
- });
3441
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
3442
- var _a, _b;
3443
- if (error) {
3444
- console.error('[Proxy] Pipeline error for codex:', error);
3445
- // 记录到错误日志 - 包含请求详情和实际转发信息
3446
- try {
3447
- // 获取供应商信息
3448
- const vendors = this.dbManager.getVendors();
3449
- const vendor = vendors.find(v => v.id === service.vendorId);
3450
- yield this.dbManager.addErrorLog({
3451
- timestamp: Date.now(),
3452
- method: req.method,
3453
- path: req.path,
3454
- statusCode: 500,
3455
- errorMessage: error.message || 'Stream processing error',
3456
- errorStack: error.stack,
3457
- requestHeaders: this.normalizeHeaders(req.headers),
3458
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3459
- upstreamRequest: upstreamRequestForLog,
3460
- responseHeaders: responseHeadersForLog,
3461
- // 添加请求详情
3462
- ruleId: rule.id,
3463
- targetType,
3464
- targetServiceId: service.id,
3465
- targetServiceName: service.name,
3466
- targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
3467
- vendorId: service.vendorId,
3468
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3469
- requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
3470
- responseTime: Date.now() - startTime,
3471
- });
3472
- }
3473
- catch (logError) {
3474
- console.error('[Proxy] Failed to log error:', logError);
3475
- }
3476
- // 尝试向客户端发送错误事件
3477
- try {
3478
- if (!res.writableEnded) {
3479
- const errorEvent = `data: ${JSON.stringify({
3480
- error: 'Stream processing error occurred'
3481
- })}\n\n`;
3482
- res.write(errorEvent);
3483
- res.end();
3484
- }
3485
- }
3486
- catch (writeError) {
3487
- console.error('[Proxy] Failed to send error event:', writeError);
3488
- }
3489
- yield finalizeLog(500, error.message);
3490
- }
3491
- }));
3492
- return;
3493
- }
3494
- // Gemini / Gemini Chat -> Claude Code 流式转换
3495
- if (useLegacySpecialSSEBranches && targetType === 'claude-code' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
3496
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
3497
- res.setHeader('Cache-Control', 'no-cache');
3498
- res.setHeader('Connection', 'keep-alive');
3499
- const parser = new streaming_1.SSEParserTransform();
3500
- const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3501
- const converter = new streaming_1.GeminiToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
3502
- const serializer = new streaming_1.SSESerializerTransform();
3503
- responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3504
- const finalizeChunks = () => {
3505
- const usage = converter.getUsage();
3506
- if (usage) {
3507
- usageForLog = {
3508
- inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
3509
- outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
3510
- cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
3511
- };
3512
- }
3513
- else {
3514
- const extractedUsage = eventCollector.extractUsage();
3515
- if (extractedUsage) {
3516
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
3517
- }
3518
- }
3519
- streamChunksForLog = eventCollector.getChunks();
3520
- responseBodyForLog = streamChunksForLog.join('\n');
3521
- console.log('[Proxy] Gemini stream request finished (claude-code), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
3522
- void finalizeLog(res.statusCode);
3523
- };
3524
- eventCollector.on('finish', () => {
3525
- console.log('[Proxy] EventCollector finished (gemini->claude-code), collecting chunks...');
3526
- finalizeChunks();
3527
- });
3528
- res.on('finish', () => {
3529
- console.log('[Proxy] Response finished (gemini->claude-code)');
3530
- if (!streamChunksForLog) {
3531
- console.log('[Proxy] Chunks not collected yet, forcing collection...');
3532
- finalizeChunks();
3533
- }
3534
- });
3535
- res.on('error', (err) => {
3536
- console.error('[Proxy] Response stream error:', err);
3537
- });
3538
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
3539
- var _a, _b;
3540
- if (error) {
3541
- console.error('[Proxy] Pipeline error for gemini->claude-code:', error);
3542
- try {
3543
- const vendors = this.dbManager.getVendors();
3544
- const vendor = vendors.find(v => v.id === service.vendorId);
3545
- yield this.dbManager.addErrorLog({
3546
- timestamp: Date.now(),
3547
- method: req.method,
3548
- path: req.path,
3549
- statusCode: 500,
3550
- errorMessage: error.message || 'Stream processing error',
3551
- errorStack: error.stack,
3552
- requestHeaders: this.normalizeHeaders(req.headers),
3553
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3554
- upstreamRequest: upstreamRequestForLog,
3555
- responseHeaders: responseHeadersForLog,
3556
- ruleId: rule.id,
3557
- targetType,
3558
- targetServiceId: service.id,
3559
- targetServiceName: service.name,
3560
- targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
3561
- vendorId: service.vendorId,
3562
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3563
- requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
3564
- responseTime: Date.now() - startTime,
3565
- });
3566
- }
3567
- catch (logError) {
3568
- console.error('[Proxy] Failed to log error:', logError);
3569
- }
3570
- try {
3571
- if (!res.writableEnded) {
3572
- const errorEvent = `event: error\ndata: ${JSON.stringify({
3573
- type: 'error',
3574
- error: {
3575
- type: 'api_error',
3576
- message: 'Stream processing error occurred'
3577
- }
3578
- })}\n\n`;
3579
- res.write(errorEvent);
3580
- res.end();
3581
- }
3582
- }
3583
- catch (writeError) {
3584
- console.error('[Proxy] Failed to send error event:', writeError);
3585
- }
3586
- yield finalizeLog(500, error.message);
3587
- }
3588
- }));
3589
- return;
3590
- }
3591
- // Gemini / Gemini Chat -> Codex 流式转换
3592
- if (useLegacySpecialSSEBranches && targetType === 'codex' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
3593
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
3594
- res.setHeader('Cache-Control', 'no-cache');
3595
- res.setHeader('Connection', 'keep-alive');
3596
- const parser = new streaming_1.SSEParserTransform();
3597
- const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3598
- const converter = new streaming_1.GeminiToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
3599
- const serializer = new streaming_1.SSESerializerTransform();
3600
- responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3601
- const finalizeChunks = () => {
3602
- const usage = converter.getUsage();
3603
- if (usage) {
3604
- usageForLog = {
3605
- inputTokens: usage.prompt_tokens,
3606
- outputTokens: usage.completion_tokens,
3607
- totalTokens: usage.total_tokens,
3608
- };
3609
- }
3610
- else {
3611
- const extractedUsage = eventCollector.extractUsage();
3612
- if (extractedUsage) {
3613
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
3614
- }
3615
- }
3616
- streamChunksForLog = eventCollector.getChunks();
3617
- responseBodyForLog = streamChunksForLog.join('\n');
3618
- console.log('[Proxy] Gemini stream request finished (codex), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
3619
- void finalizeLog(res.statusCode);
3620
- };
3621
- eventCollector.on('finish', () => {
3622
- console.log('[Proxy] EventCollector finished (gemini->codex), collecting chunks...');
3623
- finalizeChunks();
3624
- });
3625
- res.on('finish', () => {
3626
- console.log('[Proxy] Response finished (gemini->codex)');
3627
- if (!streamChunksForLog) {
3628
- console.log('[Proxy] Chunks not collected yet, forcing collection...');
3629
- finalizeChunks();
3630
- }
3631
- });
3632
- res.on('error', (err) => {
3633
- console.error('[Proxy] Response stream error:', err);
3634
- });
3635
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
3636
- var _a, _b;
3637
- if (error) {
3638
- console.error('[Proxy] Pipeline error for gemini->codex:', error);
3639
- try {
3640
- const vendors = this.dbManager.getVendors();
3641
- const vendor = vendors.find(v => v.id === service.vendorId);
3642
- yield this.dbManager.addErrorLog({
3643
- timestamp: Date.now(),
3644
- method: req.method,
3645
- path: req.path,
3646
- statusCode: 500,
3647
- errorMessage: error.message || 'Stream processing error',
3648
- errorStack: error.stack,
3649
- requestHeaders: this.normalizeHeaders(req.headers),
3650
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
3651
- upstreamRequest: upstreamRequestForLog,
3652
- responseHeaders: responseHeadersForLog,
3653
- ruleId: rule.id,
3654
- targetType,
3655
- targetServiceId: service.id,
3656
- targetServiceName: service.name,
3657
- targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
3658
- vendorId: service.vendorId,
3659
- vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3660
- requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
3661
- responseTime: Date.now() - startTime,
3662
- });
3663
- }
3664
- catch (logError) {
3665
- console.error('[Proxy] Failed to log error:', logError);
3666
- }
3667
- try {
3668
- if (!res.writableEnded) {
3669
- const errorEvent = `data: ${JSON.stringify({
3670
- error: 'Stream processing error occurred'
3671
- })}\n\n`;
3672
- res.write(errorEvent);
3673
- res.end();
3674
- }
3675
- }
3676
- catch (writeError) {
3677
- console.error('[Proxy] Failed to send error event:', writeError);
3678
- }
3679
- yield finalizeLog(500, error.message);
3680
- }
3681
- }));
3682
- return;
3683
- }
3684
3404
  // 默认stream处理(无转换)
3685
3405
  const parser = new streaming_1.SSEParserTransform();
3686
3406
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3687
3407
  const serializer = new streaming_1.SSESerializerTransform();
3688
3408
  const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
3409
+ const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
3410
+ ? new ClaudeCompactResponseSanitizer()
3411
+ : null;
3689
3412
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3690
3413
  // 使用 transformSSEToTool 方法选择转换器
3691
- const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType, requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3414
+ const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
3692
3415
  this.copyResponseHeaders(responseHeaders, res);
3693
3416
  // 收集日志:responseBody/streamChunks 记录上游原始响应;downstreamResponseBody 记录实际下发内容
3694
3417
  const finalizeChunks = () => {
@@ -3721,7 +3444,12 @@ class ProxyServer {
3721
3444
  ensureResponseWritable();
3722
3445
  return yield new Promise((resolve, reject) => {
3723
3446
  if (converter) {
3724
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
3447
+ const streamStages = [response.data, parser, eventCollector, converter];
3448
+ if (compactResponseSanitizer) {
3449
+ streamStages.push(compactResponseSanitizer);
3450
+ }
3451
+ streamStages.push(serializer, downstreamChunkCollector, res);
3452
+ (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], streamStages[3], ...streamStages.slice(4), (error) => {
3725
3453
  if (error) {
3726
3454
  reject(error);
3727
3455
  return;
@@ -3730,7 +3458,12 @@ class ProxyServer {
3730
3458
  });
3731
3459
  return;
3732
3460
  }
3733
- (0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
3461
+ const streamStages = [response.data, parser, eventCollector];
3462
+ if (compactResponseSanitizer) {
3463
+ streamStages.push(compactResponseSanitizer);
3464
+ }
3465
+ streamStages.push(serializer, downstreamChunkCollector, res);
3466
+ (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], ...streamStages.slice(3), (error) => {
3734
3467
  if (error) {
3735
3468
  reject(error);
3736
3469
  return;
@@ -3850,15 +3583,18 @@ class ProxyServer {
3850
3583
  }
3851
3584
  // 使用统一的响应转换方法
3852
3585
  const converted = this.transformResponseToTool(targetType, sourceType, responseData);
3586
+ const normalizedConverted = rule.contentType === 'compact' && targetType === 'claude-code'
3587
+ ? (0, compact_1.stripClaudeCompactResponseContent)(converted)
3588
+ : converted;
3853
3589
  // 提取 token usage(从原始响应数据中提取)
3854
3590
  usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
3855
3591
  console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
3856
3592
  this.copyResponseHeaders(responseHeaders, res);
3857
- if (converted && converted !== responseData) {
3593
+ if (normalizedConverted && normalizedConverted !== responseData) {
3858
3594
  // 非流式:responseBody 记录上游原始响应,downstreamResponseBody 记录转换后下发内容
3859
3595
  responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
3860
- downstreamResponseBodyForLog = JSON.stringify(converted);
3861
- res.status(response.status).json(converted);
3596
+ downstreamResponseBodyForLog = JSON.stringify(normalizedConverted);
3597
+ res.status(response.status).json(normalizedConverted);
3862
3598
  }
3863
3599
  else {
3864
3600
  // 没有转换,使用原始数据
@@ -3976,7 +3712,7 @@ class ProxyServer {
3976
3712
  }
3977
3713
  // 根据请求类型返回适当格式的错误响应
3978
3714
  const streamRequested = this.isStreamRequested(req, req.body || {}, targetType, sourceType);
3979
- if (route.targetType === 'claude-code') {
3715
+ if (targetType === 'claude-code') {
3980
3716
  // 对于 Claude Code,返回符合 Claude API 标准的错误响应
3981
3717
  const claudeError = {
3982
3718
  type: 'error',
@@ -4018,13 +3754,13 @@ class ProxyServer {
4018
3754
  // 这个方法主要用于初始化和日志记录
4019
3755
  // 修改数据库后无需调用此方法,配置会自动生效
4020
3756
  const allRoutes = this.dbManager.getRoutes();
4021
- const activeRoutes = allRoutes.filter((g) => g.isActive);
3757
+ const allRoutesList = allRoutes;
4022
3758
  const allServices = this.dbManager.getAPIServices();
4023
3759
  // 保留缓存以备将来可能的性能优化需求
4024
- this.routes = activeRoutes;
3760
+ this.routes = allRoutesList;
4025
3761
  if (this.rules) {
4026
3762
  this.rules.clear();
4027
- for (const route of activeRoutes) {
3763
+ for (const route of allRoutesList) {
4028
3764
  const routeRules = this.dbManager.getRules(route.id);
4029
3765
  const sortedRules = [...routeRules].sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
4030
3766
  this.rules.set(route.id, sortedRules);
@@ -4037,7 +3773,7 @@ class ProxyServer {
4037
3773
  services.set(service.id, service);
4038
3774
  });
4039
3775
  }
4040
- console.log(`Initialized with ${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)`);
4041
3777
  });
4042
3778
  }
4043
3779
  updateConfig(config) {
@@ -4051,5 +3787,523 @@ class ProxyServer {
4051
3787
  yield this.reloadRoutes();
4052
3788
  });
4053
3789
  }
3790
+ // ============================================================
3791
+ // 标准 API 路径代理请求处理
3792
+ // ============================================================
3793
+ /**
3794
+ * 处理通过标准 API 路径(/v1/messages, /v1/responses 等)进入的代理请求。
3795
+ * 与原有 proxyRequest 逻辑独立,复用规则匹配、故障切换等机制。
3796
+ */
3797
+ handleApiPathProxyRequest(req, res, route, clientFormat, apiPath) {
3798
+ return __awaiter(this, void 0, void 0, function* () {
3799
+ var _a, _b;
3800
+ const requestStartAt = Date.now();
3801
+ // 鉴权
3802
+ if (this.config.apiKey) {
3803
+ const authHeader = req.headers.authorization;
3804
+ const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
3805
+ if (!providedKey || providedKey !== this.config.apiKey) {
3806
+ yield this.logToolRequest(req, {
3807
+ statusCode: 401,
3808
+ responseTime: Date.now() - requestStartAt,
3809
+ error: 'Invalid API key',
3810
+ tags: this.buildRelayTags(false),
3811
+ });
3812
+ // 根据客户端格式返回错误
3813
+ if (clientFormat === 'claude') {
3814
+ res.status(401).json({ type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } });
3815
+ }
3816
+ else {
3817
+ res.status(401).json({ error: { message: 'Invalid API key' } });
3818
+ }
3819
+ return;
3820
+ }
3821
+ }
3822
+ const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false;
3823
+ if (!enableFailover) {
3824
+ const rule = yield this.findMatchingRule(route.id, req);
3825
+ if (!rule) {
3826
+ return res.status(404).json({ error: { message: 'No matching rule found' } });
3827
+ }
3828
+ const service = this.getServiceById(rule.targetServiceId);
3829
+ if (!service) {
3830
+ return res.status(500).json({ error: { message: 'Target service not configured' } });
3831
+ }
3832
+ try {
3833
+ yield this.proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath);
3834
+ }
3835
+ catch (error) {
3836
+ console.error('[ApiPathProxy] Error:', error.message);
3837
+ this.sendFormatError(res, clientFormat, 500, error.message);
3838
+ }
3839
+ return;
3840
+ }
3841
+ // 故障切换模式
3842
+ const allRules = this.getAllMatchingRules(route.id, req);
3843
+ if (allRules.length === 0) {
3844
+ return res.status(404).json({ error: { message: 'No matching rule found' } });
3845
+ }
3846
+ let lastError = null;
3847
+ let lastFailedRule = null;
3848
+ let lastFailedService = null;
3849
+ for (let i = 0; i < allRules.length; i++) {
3850
+ const rule = allRules[i];
3851
+ const service = this.getServiceById(rule.targetServiceId);
3852
+ if (!service)
3853
+ continue;
3854
+ const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, route.id, rule.contentType);
3855
+ if (isBlacklisted) {
3856
+ console.log(`[ApiPathProxy] Service ${service.name} is blacklisted, skipping...`);
3857
+ continue;
3858
+ }
3859
+ try {
3860
+ const nextServiceName = yield this.findNextAvailableServiceName(allRules, i + 1, route.id);
3861
+ yield this.proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, {
3862
+ failoverEnabled: true,
3863
+ forwardedToServiceName: nextServiceName,
3864
+ });
3865
+ return;
3866
+ }
3867
+ catch (error) {
3868
+ console.error(`[ApiPathProxy] Service ${service.name} failed:`, error.message);
3869
+ lastError = error;
3870
+ lastFailedRule = rule;
3871
+ lastFailedService = service;
3872
+ const isTimeout = error.code === 'ECONNABORTED' ||
3873
+ ((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
3874
+ error.errno === 'ETIMEDOUT';
3875
+ if (isTimeout) {
3876
+ yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout', undefined, 'timeout');
3877
+ rules_status_service_1.rulesStatusBroadcaster.markRuleSuspended(route.id, rule.id, service.id, rule.contentType, '请求超时', 'timeout');
3878
+ }
3879
+ else {
3880
+ const statusCode = this.getErrorStatusCode(error, 500);
3881
+ if (statusCode >= 400) {
3882
+ yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
3883
+ rules_status_service_1.rulesStatusBroadcaster.markRuleSuspended(route.id, rule.id, service.id, rule.contentType, `HTTP ${statusCode} 错误`, 'http');
3884
+ }
3885
+ }
3886
+ continue;
3887
+ }
3888
+ }
3889
+ // Fallback: try the last failed service
3890
+ if (lastFailedRule && lastFailedService) {
3891
+ try {
3892
+ yield this.proxyRequestForApiPath(req, res, route, lastFailedRule, lastFailedService, clientFormat, apiPath, {
3893
+ failoverEnabled: false,
3894
+ });
3895
+ return;
3896
+ }
3897
+ catch (fallbackError) {
3898
+ lastError = fallbackError;
3899
+ }
3900
+ }
3901
+ yield this.logToolRequest(req, {
3902
+ statusCode: 503,
3903
+ responseTime: Date.now() - requestStartAt,
3904
+ error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
3905
+ tags: this.buildRelayTags(true),
3906
+ });
3907
+ this.sendFormatError(res, clientFormat, 503, (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed');
3908
+ });
3909
+ }
3910
+ /**
3911
+ * 对单个规则执行代理请求(标准 API 路径入口)。
3912
+ * 与原有 proxyRequest 类似,但使用 clientFormat 而非 targetType 推断格式。
3913
+ */
3914
+ proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, options) {
3915
+ return __awaiter(this, void 0, void 0, function* () {
3916
+ var _a, _b, _c, _d;
3917
+ const startTime = Date.now();
3918
+ const rawSourceType = service.sourceType || 'openai-chat';
3919
+ const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
3920
+ const vendor = this.dbManager.getVendorByServiceId(service.id);
3921
+ console.log(`\x1b[32m[ApiPathProxy]\x1b[0m path=${apiPath}, clientFormat=${clientFormat}, session=-, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}`);
3922
+ const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
3923
+ let requestBody = this.cloneRequestBody(req.body || {});
3924
+ let usageForLog;
3925
+ let responseBodyForLog;
3926
+ let downstreamResponseBodyForLog;
3927
+ let streamChunksForLog;
3928
+ let responseHeadersForLog;
3929
+ let upstreamRequestForLog;
3930
+ let relayedForLog = true;
3931
+ void downstreamResponseBodyForLog;
3932
+ void streamChunksForLog;
3933
+ void responseHeadersForLog;
3934
+ void upstreamRequestForLog;
3935
+ // 编程套餐限制检查
3936
+ if (!this.checkCodingPlan(req, res, service, clientFormat))
3937
+ return;
3938
+ // Compact 处理(针对 claude 格式的 compact)
3939
+ if (rule.contentType === 'compact' && clientFormat === 'claude') {
3940
+ if (Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
3941
+ requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
3942
+ if (this.isClaudeSource(sourceType)) {
3943
+ requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
3944
+ }
3945
+ }
3946
+ requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
3947
+ }
3948
+ const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
3949
+ if (logged)
3950
+ return;
3951
+ logged = true;
3952
+ yield this.logToolRequest(req, {
3953
+ statusCode,
3954
+ responseTime: Date.now() - startTime,
3955
+ usage: usageForLog,
3956
+ error,
3957
+ tags: this.buildRelayTags(relayedForLog),
3958
+ });
3959
+ });
3960
+ let logged = false;
3961
+ // count_tokens 本地处理
3962
+ if (this.isCountTokensPath(req.path)) {
3963
+ const inputTokens = this.estimateClaudeCountTokens(requestBody);
3964
+ const localTokenResponse = { input_tokens: inputTokens };
3965
+ usageForLog = { inputTokens, outputTokens: 0, totalTokens: inputTokens };
3966
+ res.status(200).json(localTokenResponse);
3967
+ yield finalizeLog(200);
3968
+ return;
3969
+ }
3970
+ // 请求转换:使用 clientFormat 而非硬编码的 tool→format 映射
3971
+ const payloadForTransform = this.cloneRequestBody(requestBody);
3972
+ const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3973
+ const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3974
+ const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3975
+ const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig);
3976
+ requestBody = (_a = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(requestBody)) !== null && _a !== void 0 ? _a : {};
3977
+ // Compact final sanitize
3978
+ if (rule.contentType === 'compact' && clientFormat === 'claude' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
3979
+ requestBody.messages = (0, compact_1.sanitizeClaudeMessagesForCompact)(requestBody.messages);
3980
+ if (this.isClaudeSource(sourceType)) {
3981
+ requestBody.messages = (0, compact_1.flattenClaudeToolBlocksForCompact)(requestBody.messages);
3982
+ }
3983
+ requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
3984
+ }
3985
+ // 应用 max_output_tokens 限制
3986
+ requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
3987
+ // Stream 判断
3988
+ const streamRequested = this.isStreamRequested(req, requestBody);
3989
+ // 构建上游 URL
3990
+ const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3991
+ const apiUrl = this.resolveEffectiveApiUrl(service);
3992
+ const upstreamUrl = this.mapApiPathToUpstreamUrl(apiPath, sourceType, apiUrl, model, streamRequested);
3993
+ upstreamRequestForLog = { url: upstreamUrl, body: requestBody || undefined };
3994
+ const upstreamHeaders = this.buildUpstreamHeaders(req, service, sourceType, streamRequested, requestBody);
3995
+ const upstreamAbortController = new AbortController();
3996
+ const abortUpstreamRequest = (reason) => {
3997
+ if (!upstreamAbortController.signal.aborted) {
3998
+ upstreamAbortController.abort(new Error(`Client disconnected: ${reason}`));
3999
+ }
4000
+ };
4001
+ req.once('aborted', () => abortUpstreamRequest('request aborted'));
4002
+ res.once('close', () => {
4003
+ if (!res.writableEnded)
4004
+ abortUpstreamRequest('response stream closed');
4005
+ });
4006
+ try {
4007
+ const axiosConfig = {
4008
+ method: req.method,
4009
+ url: upstreamUrl,
4010
+ headers: upstreamHeaders,
4011
+ timeout: this.resolveEffectiveTimeout(rule),
4012
+ validateStatus: () => true,
4013
+ responseType: streamRequested ? 'stream' : 'json',
4014
+ signal: upstreamAbortController.signal,
4015
+ };
4016
+ if (Object.keys(req.query).length > 0) {
4017
+ axiosConfig.params = req.query;
4018
+ }
4019
+ if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
4020
+ axiosConfig.data = requestBody;
4021
+ }
4022
+ // 代理配置
4023
+ if (service.enableProxy) {
4024
+ const appConfig = this.dbManager.getConfig();
4025
+ if (appConfig.proxyEnabled && appConfig.proxyUrl) {
4026
+ try {
4027
+ const { HttpsProxyAgent } = yield Promise.resolve().then(() => __importStar(require('https-proxy-agent')));
4028
+ let proxyUrl = appConfig.proxyUrl;
4029
+ const proxyAuth = appConfig.proxyUsername && appConfig.proxyPassword
4030
+ ? `${appConfig.proxyUsername}:${appConfig.proxyPassword}@` : '';
4031
+ if (!proxyUrl.startsWith('http://') && !proxyUrl.startsWith('https://')) {
4032
+ proxyUrl = `http://${proxyAuth}${proxyUrl}`;
4033
+ }
4034
+ else if (proxyAuth) {
4035
+ const urlObj = new URL(proxyUrl);
4036
+ urlObj.username = appConfig.proxyUsername;
4037
+ urlObj.password = appConfig.proxyPassword;
4038
+ proxyUrl = urlObj.toString();
4039
+ }
4040
+ axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl);
4041
+ axiosConfig.httpAgent = new HttpsProxyAgent(proxyUrl);
4042
+ }
4043
+ catch (error) {
4044
+ console.error('[ApiPathProxy] Failed to create proxy agent:', error);
4045
+ }
4046
+ }
4047
+ }
4048
+ const response = yield (0, axios_1.default)(axiosConfig);
4049
+ const responseHeaders = response.headers || {};
4050
+ const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
4051
+ const isEventStream = streamRequested && contentType.includes('text/event-stream');
4052
+ // Handle upstream errors
4053
+ if (response.status >= 400) {
4054
+ let errorResponseData = response.data;
4055
+ if (streamRequested && response.data && typeof response.data.on === 'function') {
4056
+ const raw = yield this.readStreamBody(response.data);
4057
+ errorResponseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
4058
+ }
4059
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4060
+ const errorMessage = typeof errorResponseData === 'string'
4061
+ ? errorResponseData
4062
+ : ((_c = errorResponseData === null || errorResponseData === void 0 ? void 0 : errorResponseData.error) === null || _c === void 0 ? void 0 : _c.message) || (errorResponseData === null || errorResponseData === void 0 ? void 0 : errorResponseData.error) || JSON.stringify(errorResponseData);
4063
+ if (failoverEnabled) {
4064
+ yield finalizeLog(response.status, errorMessage);
4065
+ throw this.createFailoverError(errorMessage, response.status);
4066
+ }
4067
+ this.copyResponseHeaders(responseHeaders, res);
4068
+ if (contentType.includes('application/json')) {
4069
+ res.status(response.status).json(errorResponseData);
4070
+ }
4071
+ else {
4072
+ res.status(response.status).send(errorResponseData);
4073
+ }
4074
+ yield finalizeLog(response.status);
4075
+ return;
4076
+ }
4077
+ if (isEventStream && response.data) {
4078
+ // Stream pipeline
4079
+ const parser = new streaming_1.SSEParserTransform();
4080
+ const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
4081
+ const serializer = new streaming_1.SSESerializerTransform();
4082
+ const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
4083
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4084
+ const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
4085
+ this.copyResponseHeaders(responseHeaders, res);
4086
+ res.status(response.status);
4087
+ const finalizeStreamChunks = () => {
4088
+ streamChunksForLog = eventCollector.getChunks();
4089
+ responseBodyForLog = streamChunksForLog.join('\n');
4090
+ downstreamResponseBodyForLog = downstreamChunkCollector.getChunks().join('');
4091
+ let extractedUsage = eventCollector.extractUsage();
4092
+ if (converter && typeof converter.getUsage === 'function') {
4093
+ const converterUsage = converter.getUsage();
4094
+ if (converterUsage)
4095
+ extractedUsage = converterUsage;
4096
+ }
4097
+ if (extractUsage && extractedUsage) {
4098
+ usageForLog = extractUsage(extractedUsage);
4099
+ }
4100
+ else if (extractedUsage) {
4101
+ usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
4102
+ }
4103
+ };
4104
+ try {
4105
+ yield new Promise((resolve, reject) => {
4106
+ if (converter) {
4107
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
4108
+ if (error) {
4109
+ reject(error);
4110
+ return;
4111
+ }
4112
+ resolve();
4113
+ });
4114
+ }
4115
+ else {
4116
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
4117
+ if (error) {
4118
+ reject(error);
4119
+ return;
4120
+ }
4121
+ resolve();
4122
+ });
4123
+ }
4124
+ });
4125
+ }
4126
+ catch (error) {
4127
+ if (this.isClientDisconnectError(error, res)) {
4128
+ yield finalizeLog(499, 'Client disconnected');
4129
+ return;
4130
+ }
4131
+ console.error('[ApiPathProxy] Stream pipeline error:', error);
4132
+ yield finalizeLog(500, error.message);
4133
+ if (failoverEnabled && !this.isResponseCommitted(res)) {
4134
+ throw this.createFailoverError(error.message, 500, error);
4135
+ }
4136
+ return;
4137
+ }
4138
+ finalizeStreamChunks();
4139
+ yield finalizeLog(res.statusCode);
4140
+ return;
4141
+ }
4142
+ // Non-stream response
4143
+ let responseData = response.data;
4144
+ if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
4145
+ const raw = yield this.readStreamBody(response.data);
4146
+ responseData = (_d = this.safeJsonParse(raw)) !== null && _d !== void 0 ? _d : raw;
4147
+ }
4148
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4149
+ if (this.isEmptyResponse(responseData)) {
4150
+ responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
4151
+ yield finalizeLog(200);
4152
+ res.status(200).end();
4153
+ return;
4154
+ }
4155
+ // 使用 clientFormat 做响应转换
4156
+ const converted = this.transformResponseByFormat(sourceType, clientFormat, responseData);
4157
+ const normalizedConverted = rule.contentType === 'compact' && clientFormat === 'claude'
4158
+ ? (0, compact_1.stripClaudeCompactResponseContent)(converted)
4159
+ : converted;
4160
+ usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
4161
+ this.copyResponseHeaders(responseHeaders, res);
4162
+ if (normalizedConverted && normalizedConverted !== responseData) {
4163
+ responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
4164
+ downstreamResponseBodyForLog = JSON.stringify(normalizedConverted);
4165
+ res.status(response.status).json(normalizedConverted);
4166
+ }
4167
+ else {
4168
+ responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
4169
+ downstreamResponseBodyForLog = responseBodyForLog;
4170
+ if (contentType.includes('application/json')) {
4171
+ res.status(response.status).json(responseData);
4172
+ }
4173
+ else {
4174
+ res.status(response.status).send(responseData);
4175
+ }
4176
+ }
4177
+ yield finalizeLog(res.statusCode);
4178
+ }
4179
+ finally {
4180
+ rules_status_service_1.rulesStatusBroadcaster.markRuleIdle(route.id, rule.id);
4181
+ }
4182
+ });
4183
+ }
4184
+ /**
4185
+ * 使用显式 clientFormat 进行请求转换(取代 tool → format 的硬编码映射)
4186
+ */
4187
+ transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig) {
4188
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
4189
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
4190
+ const body = result.body;
4191
+ if (targetModel) {
4192
+ const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
4193
+ if (!isOpenAIModel) {
4194
+ body.model = targetModel;
4195
+ }
4196
+ }
4197
+ return body;
4198
+ }
4199
+ /**
4200
+ * 使用显式格式进行响应转换
4201
+ */
4202
+ transformResponseByFormat(upstreamFormat, clientFormat, responseData) {
4203
+ const upstream = (0, index_1.sourceTypeToFormat)(upstreamFormat);
4204
+ return (0, index_1.transformResponse)({ fromFormat: upstream, toFormat: clientFormat, response: responseData });
4205
+ }
4206
+ /**
4207
+ * 使用显式格式进行流式转换
4208
+ */
4209
+ transformSSEByFormat(clientFormat, sourceType) {
4210
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(sourceType);
4211
+ if (upstreamFormat === clientFormat) {
4212
+ return { converter: null };
4213
+ }
4214
+ const streamConverter = (0, index_1.createStreamConverter)({ fromFormat: upstreamFormat, toFormat: clientFormat });
4215
+ const adapter = new stream_converter_adapter_1.StreamConverterAdapter(streamConverter);
4216
+ const extractUsage = clientFormat === 'claude'
4217
+ ? (usage) => ({
4218
+ inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
4219
+ outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
4220
+ cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
4221
+ })
4222
+ : (usage) => ({
4223
+ inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
4224
+ outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
4225
+ totalTokens: ((usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0) + ((usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0),
4226
+ });
4227
+ return { converter: adapter, extractUsage };
4228
+ }
4229
+ /**
4230
+ * 为标准 API 路径构建上游 URL
4231
+ */
4232
+ mapApiPathToUpstreamUrl(_apiPath, source, apiUrl, modelName, isStream) {
4233
+ const geminiEndpoint = isStream ? 'streamGenerateContent' : 'generateContent';
4234
+ const buildGeminiUrl = (url) => {
4235
+ if (url.includes('streamGenerateContent')) {
4236
+ const [pathname, search] = url.split('?');
4237
+ if (search === null || search === void 0 ? void 0 : search.includes('alt=sse'))
4238
+ return url;
4239
+ if (search)
4240
+ return `${pathname}?${search}&alt=sse`;
4241
+ return `${pathname}?alt=sse`;
4242
+ }
4243
+ return url;
4244
+ };
4245
+ // Gemini chat 类型:URL 中包含 {modelName} 和 {endPoint} 占位符
4246
+ if (this.isGeminiChatSource(source)) {
4247
+ const url = apiUrl.replace('{modelName}', modelName).replace('{endPoint}', geminiEndpoint);
4248
+ return buildGeminiUrl(url);
4249
+ }
4250
+ // Chat 类型(openai-chat, claude-chat):直接使用 apiUrl
4251
+ if (this.isChatType(source)) {
4252
+ return apiUrl;
4253
+ }
4254
+ // Gemini base 类型
4255
+ if (this.isGeminiSource(source)) {
4256
+ const url = `${apiUrl}/v1beta/models/${modelName}:${geminiEndpoint}`;
4257
+ return buildGeminiUrl(url);
4258
+ }
4259
+ // 对于标准 API 路径,直接根据上游格式拼接
4260
+ const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
4261
+ switch (upstreamFormat) {
4262
+ case 'claude':
4263
+ return `${apiUrl}/v1/messages`;
4264
+ case 'responses':
4265
+ return `${apiUrl}/v1/responses`;
4266
+ case 'completions':
4267
+ return `${apiUrl}/v1/chat/completions`;
4268
+ case 'gemini':
4269
+ return `${apiUrl}/v1beta/models/${modelName}:${geminiEndpoint}`;
4270
+ default:
4271
+ return apiUrl;
4272
+ }
4273
+ }
4274
+ /**
4275
+ * 编程套餐限制检查
4276
+ * 当服务启用了 enableCodingPlan 时,仅允许编程工具发起的请求通过。
4277
+ * @returns true 表示通过检查(可以继续),false 表示已被拒绝(已写入响应)
4278
+ */
4279
+ checkCodingPlan(req, res, service, clientFormat) {
4280
+ if (!service.enableCodingPlan)
4281
+ return true; // 未启用,直接通过
4282
+ const headers = req.headers;
4283
+ const codingCheck = (0, coding_plan_1.isCodingToolRequest)(req.body, clientFormat, headers);
4284
+ if (codingCheck.isCoding)
4285
+ return true; // 是编程工具请求,通过
4286
+ // 非编程工具请求,拒绝
4287
+ console.warn(`\x1b[33m[CodingPlan]\x1b[0m Rejected non-coding request: service=${service.name}, reason=${codingCheck.reason}`);
4288
+ this.sendFormatError(res, clientFormat, 403, '此 API 服务仅允许编程工具调用(如 Claude Code、Codex、Cursor 等)');
4289
+ return false;
4290
+ }
4291
+ /**
4292
+ * 根据客户端格式发送错误响应
4293
+ */
4294
+ sendFormatError(res, clientFormat, statusCode, message) {
4295
+ if (this.isResponseCommitted(res))
4296
+ return;
4297
+ switch (clientFormat) {
4298
+ case 'claude':
4299
+ res.status(statusCode).json({ type: 'error', error: { type: 'api_error', message } });
4300
+ break;
4301
+ case 'gemini':
4302
+ res.status(statusCode).json({ error: { code: statusCode, message } });
4303
+ break;
4304
+ default:
4305
+ res.status(statusCode).json({ error: { message } });
4306
+ }
4307
+ }
4054
4308
  }
4055
4309
  exports.ProxyServer = ProxyServer;