aicodeswitch 5.1.2 → 5.2.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 (32) hide show
  1. package/README.md +1 -0
  2. package/bin/restore.js +14 -7
  3. package/bin/utils/managed-fields.js +62 -0
  4. package/dist/server/access-keys/index.js +173 -0
  5. package/dist/server/access-keys/key-logger.js +358 -0
  6. package/dist/server/access-keys/key-resolver.js +51 -0
  7. package/dist/server/access-keys/key-session-tracker.js +217 -0
  8. package/dist/server/access-keys/manager.js +206 -0
  9. package/dist/server/access-keys/policy-manager.js +144 -0
  10. package/dist/server/access-keys/quota-checker.js +197 -0
  11. package/dist/server/access-keys/usage-tracker.js +279 -0
  12. package/dist/server/auth.js +16 -4
  13. package/dist/server/coding-plan-headers.js +121 -0
  14. package/dist/server/config-managed-fields.js +2 -0
  15. package/dist/server/conversions/index.js +8 -0
  16. package/dist/server/conversions/utils/tool-result.js +35 -0
  17. package/dist/server/fs-database.js +72 -1
  18. package/dist/server/main.js +1162 -13
  19. package/dist/server/proxy-server.js +662 -128
  20. package/dist/server/rules-status-service.js +32 -3
  21. package/dist/server/session-launcher.js +282 -0
  22. package/dist/server/session-migration.js +419 -0
  23. package/dist/server/transformers/chunk-collector.js +28 -1
  24. package/dist/server/transformers/model-rewrite-transform.js +128 -0
  25. package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
  26. package/dist/ui/assets/index-Cws89pD2.js +828 -0
  27. package/dist/ui/assets/index-CzfKxImD.css +1 -0
  28. package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
  29. package/dist/ui/index.html +2 -2
  30. package/package.json +1 -1
  31. package/dist/ui/assets/index-BHR12ImE.css +0 -1
  32. package/dist/ui/assets/index-CumAhpXg.js +0 -517
@@ -50,6 +50,7 @@ const axios_1 = __importDefault(require("axios"));
50
50
  const stream_1 = require("stream");
51
51
  const crypto_1 = __importDefault(require("crypto"));
52
52
  const streaming_1 = require("./transformers/streaming");
53
+ const model_rewrite_transform_1 = require("./transformers/model-rewrite-transform");
53
54
  const chunk_collector_1 = require("./transformers/chunk-collector");
54
55
  const rules_status_service_1 = require("./rules-status-service");
55
56
  const index_1 = require("./conversions/index");
@@ -60,6 +61,8 @@ const type_migration_1 = require("./type-migration");
60
61
  const original_config_reader_1 = require("./original-config-reader");
61
62
  const compact_1 = require("./conversions/compact");
62
63
  const coding_plan_1 = require("./coding-plan");
64
+ const coding_plan_headers_1 = require("./coding-plan-headers");
65
+ const auth_1 = require("./auth");
63
66
  const SUPPORTED_TARGETS = ['claude-code', 'codex'];
64
67
  /** 默认模型列表 */
65
68
  const DEFAULT_MODELS = [
@@ -209,6 +212,12 @@ class ProxyServer {
209
212
  writable: true,
210
213
  value: void 0
211
214
  });
215
+ Object.defineProperty(this, "accessKeyModule", {
216
+ enumerable: true,
217
+ configurable: true,
218
+ writable: true,
219
+ value: null
220
+ });
212
221
  // 请求去重缓存:用于防止同一个请求被重复计数(如网络重试)
213
222
  // key: requestHash, value: timestamp
214
223
  Object.defineProperty(this, "requestDedupeCache", {
@@ -235,6 +244,73 @@ class ProxyServer {
235
244
  this.config = dbManager.getConfig();
236
245
  this.app = app;
237
246
  }
247
+ /** 设置 AccessKey 模块引用 */
248
+ setAccessKeyModule(module) {
249
+ this.accessKeyModule = module;
250
+ }
251
+ /** 获取 AccessKey 模块引用 */
252
+ getAccessKeyModule() {
253
+ return this.accessKeyModule;
254
+ }
255
+ /**
256
+ * 从请求中提取 API Key(支持三种 Header,按优先级依次尝试)
257
+ */
258
+ extractApiKey(req) {
259
+ const authHeader = req.headers.authorization;
260
+ if (authHeader) {
261
+ const key = authHeader.replace('Bearer ', '').trim();
262
+ if (key)
263
+ return key;
264
+ }
265
+ const xApiKey = req.headers['x-api-key'];
266
+ if (typeof xApiKey === 'string' && xApiKey.trim())
267
+ return xApiKey.trim();
268
+ const xGoogApiKey = req.headers['x-goog-api-key'];
269
+ if (typeof xGoogApiKey === 'string' && xGoogApiKey.trim())
270
+ return xGoogApiKey.trim();
271
+ return null;
272
+ }
273
+ /** 构建 AccessKey 相关的错误响应(匹配错误码格式) */
274
+ sendAccessKeyError(res, error, isClaudeFormat = false) {
275
+ res.setHeader('request-id', `req_ak_${Date.now()}`);
276
+ res.setHeader('connection', 'close');
277
+ if (isClaudeFormat) {
278
+ // Claude API 格式
279
+ res.status(error.httpStatus).json({
280
+ type: 'error',
281
+ error: {
282
+ type: error.type,
283
+ message: error.message,
284
+ }
285
+ });
286
+ }
287
+ else {
288
+ // OpenAI 格式
289
+ res.status(error.httpStatus).json({
290
+ error: {
291
+ type: error.type,
292
+ code: error.code,
293
+ message: error.message,
294
+ }
295
+ });
296
+ }
297
+ }
298
+ /**
299
+ * 发送 AUTH 鉴权失败响应。
300
+ * 使用 511(Network Authentication Required)避免客户端将认证失败误判为官方 API 错误而持续重试。
301
+ * @param isClaudeFormat 是否使用 Claude API 错误格式(vs OpenAI 格式)
302
+ */
303
+ sendAuthError(res, isClaudeFormat) {
304
+ const message = 'Authentication required. Please provide a valid AccessKey.';
305
+ res.setHeader('request-id', `req_ak_${Date.now()}`);
306
+ res.setHeader('connection', 'close');
307
+ if (isClaudeFormat) {
308
+ res.status(511).json({ type: 'error', error: { type: 'api_error', message } });
309
+ }
310
+ else {
311
+ res.status(511).json({ error: { type: 'api_error', code: 'system_error', message } });
312
+ }
313
+ }
238
314
  inferTargetTypeFromPath(path) {
239
315
  if (path === '/claude-code' || path.startsWith('/claude-code/')) {
240
316
  return 'claude-code';
@@ -296,47 +372,161 @@ class ProxyServer {
296
372
  // /v1/models: 直接返回静态模型列表
297
373
  if (apiPath === '/v1/models') {
298
374
  // 鉴权
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' } });
375
+ const apiKeyValue = this.extractApiKey(req);
376
+ if (apiKeyValue === null || apiKeyValue === void 0 ? void 0 : apiKeyValue.startsWith('sk_')) {
377
+ // AccessKey 鉴权
378
+ if (!this.accessKeyModule) {
379
+ res.status(401).json({ error: { type: 'authentication_error', code: 'INVALID_API_KEY', message: 'AccessKey 功能未启用' } });
304
380
  return;
305
381
  }
382
+ const result = this.accessKeyModule.keyResolver.resolve(apiKeyValue);
383
+ if (!result || 'error' in result) {
384
+ const err = result ? result.error : { type: 'authentication_error', code: 'INVALID_API_KEY', message: '无效的 API Key', httpStatus: 401 };
385
+ this.sendAccessKeyError(res, err);
386
+ return;
387
+ }
388
+ }
389
+ else if ((0, auth_1.isAuthEnabled)()) {
390
+ // AUTH 已启用 → 仅允许 AccessKey 认证
391
+ console.log(`\x1b[31m[AUTH] 511\x1b[0m ${req.method} ${req.path} — 未提供有效的 AccessKey`);
392
+ this.sendAuthError(res, false);
393
+ return;
306
394
  }
307
395
  res.json(buildModelsResponse(this.dbManager.getApiPathModels()));
308
396
  return;
309
397
  }
310
398
  // 其余 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.` } });
399
+ // 检查是否为 AccessKey 请求
400
+ const apiKeyValue = this.extractApiKey(req);
401
+ let accessKeyCtx = null;
402
+ if ((apiKeyValue === null || apiKeyValue === void 0 ? void 0 : apiKeyValue.startsWith('sk_')) && this.accessKeyModule) {
403
+ const result = this.accessKeyModule.keyResolver.resolve(apiKeyValue);
404
+ if (!result || 'error' in result) {
405
+ const err = result ? result.error : { type: 'authentication_error', code: 'INVALID_API_KEY', message: '无效的 API Key', httpStatus: 401 };
406
+ this.sendAccessKeyError(res, err, apiPathToClientFormat(apiPath) === 'claude');
407
+ return;
408
+ }
409
+ accessKeyCtx = result;
410
+ }
411
+ else if ((0, auth_1.isAuthEnabled)()) {
412
+ // AUTH 已启用 → 仅允许 AccessKey 认证
413
+ console.log(`\x1b[31m[AUTH] 511\x1b[0m ${req.method} ${req.path} — 未提供有效的 AccessKey`);
414
+ this.sendAuthError(res, apiPathToClientFormat(apiPath) === 'claude');
315
415
  return;
316
416
  }
317
- // 加载绑定的路由
417
+ // 推断客户端格式
418
+ const clientFormat = apiPathToClientFormat(apiPath);
419
+ // 确定路由来源
318
420
  const allRoutes = this.dbManager.getRoutes();
319
- const route = allRoutes.find((r) => r.id === binding.routeId);
421
+ let route;
422
+ if (accessKeyCtx) {
423
+ // AccessKey 请求:从策略的 routeId 获取路由
424
+ const policyRouteId = accessKeyCtx.policy.routeId;
425
+ if (policyRouteId && policyRouteId !== 'system') {
426
+ // 策略绑定了具体路由
427
+ route = allRoutes.find((r) => r.id === policyRouteId);
428
+ if (!route) {
429
+ this.sendAccessKeyError(res, { type: 'permission_error', code: 'NO_ROUTE_CONFIGURED', message: '策略绑定的路由不存在', httpStatus: 403 }, clientFormat === 'claude');
430
+ return;
431
+ }
432
+ }
433
+ else {
434
+ // routeId 为空或 'system':按系统默认路由
435
+ const bindings = this.dbManager.getApiPathBindings();
436
+ const binding = bindings.find((b) => b.apiPath === apiPath);
437
+ if (!binding || !binding.routeId) {
438
+ res.status(404).json({ error: { message: `API path ${apiPath} is not bound to any route. Please configure it in Route Mapping settings.` } });
439
+ return;
440
+ }
441
+ route = allRoutes.find((r) => r.id === binding.routeId);
442
+ }
443
+ }
444
+ else {
445
+ // 正常请求:从 API 路径绑定获取路由
446
+ const bindings = this.dbManager.getApiPathBindings();
447
+ const binding = bindings.find((b) => b.apiPath === apiPath);
448
+ if (!binding || !binding.routeId) {
449
+ res.status(404).json({ error: { message: `API path ${apiPath} is not bound to any route. Please configure it in Route Mapping settings.` } });
450
+ return;
451
+ }
452
+ route = allRoutes.find((r) => r.id === binding.routeId);
453
+ }
454
+ // 会话级路由覆盖:仅对非 AccessKey 请求生效
455
+ if (!accessKeyCtx) {
456
+ const sessionId = this.extractSessionIdForFormat(req, clientFormat);
457
+ if (sessionId) {
458
+ const session = this.dbManager.getSession(sessionId);
459
+ if (session === null || session === void 0 ? void 0 : session.routeId) {
460
+ const boundRoute = allRoutes.find((r) => r.id === session.routeId);
461
+ if (boundRoute) {
462
+ console.log(`[SESSION-ROUTE] API path ${apiPath} session ${sessionId} using bound route: ${boundRoute.name}`);
463
+ route = boundRoute;
464
+ }
465
+ else {
466
+ console.log(`[SESSION-ROUTE] Bound route ${session.routeId} not found for session ${sessionId}, clearing binding`);
467
+ this.dbManager.unbindSessionRoute(sessionId).catch(console.error);
468
+ }
469
+ }
470
+ }
471
+ }
320
472
  if (!route) {
321
- return res.status(404).json({ error: { message: `Bound route '${binding.routeId}' not found or inactive. Please check Route Mapping settings.` } });
473
+ return res.status(404).json({ error: { message: `Bound route not found or inactive.` } });
322
474
  }
323
- // 推断客户端格式
324
- const clientFormat = apiPathToClientFormat(apiPath);
325
475
  // 复用完整的代理请求处理
326
- yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath);
476
+ yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath, accessKeyCtx);
327
477
  }));
328
478
  // Dynamic proxy middleware (原有的 /claude-code, /codex 逻辑)
329
479
  this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
330
- var _a, _b, _c, _d, _e;
480
+ var _a, _b, _c, _d, _e, _f;
331
481
  // 仅处理支持的目标路径
332
482
  if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
333
483
  return next();
334
484
  }
485
+ // AUTH 鉴权检查
486
+ const apiKeyValue = this.extractApiKey(req);
487
+ if ((apiKeyValue === null || apiKeyValue === void 0 ? void 0 : apiKeyValue.startsWith('sk_')) && this.accessKeyModule) {
488
+ const result = this.accessKeyModule.keyResolver.resolve(apiKeyValue);
489
+ if (!result || 'error' in result) {
490
+ const err = result ? result.error : { type: 'authentication_error', code: 'INVALID_API_KEY', message: '无效的 API Key', httpStatus: 401 };
491
+ this.sendAccessKeyError(res, err, req.path.startsWith('/claude-code/'));
492
+ return;
493
+ }
494
+ // 配额检查
495
+ const usage = yield this.accessKeyModule.usageTracker.getUsage(result.accessKey.id);
496
+ const quotaResult = this.accessKeyModule.quotaChecker.checkQuota(result.policy, usage, result.accessKey.id, (_a = req.body) === null || _a === void 0 ? void 0 : _a.model);
497
+ if (quotaResult) {
498
+ this.sendAccessKeyError(res, { type: 'rate_limit_error', code: quotaResult.error, message: quotaResult.message, httpStatus: quotaResult.httpStatus }, req.path.startsWith('/claude-code/'));
499
+ return;
500
+ }
501
+ this.accessKeyModule.quotaChecker.onRequestStart(result.accessKey.id, result.policy);
502
+ req._accessKeyCtx = result;
503
+ }
504
+ else if ((0, auth_1.isAuthEnabled)()) {
505
+ // AUTH 已启用 → 仅允许 AccessKey 认证
506
+ console.log(`\x1b[31m[AUTH] 511\x1b[0m ${req.method} ${req.path} — 未提供有效的 AccessKey`);
507
+ this.sendAuthError(res, req.path.startsWith('/claude-code/'));
508
+ return;
509
+ }
335
510
  const requestStartAt = Date.now();
336
511
  let hasRelayAttempt = false;
337
512
  try {
338
513
  const pathTargetType = this.inferTargetTypeFromPath(req.path);
339
- const route = this.findMatchingRoute(req);
514
+ // AccessKey 请求:从策略的 routeId 获取路由;否则从工具绑定获取
515
+ const accessKeyCtx = req._accessKeyCtx;
516
+ let route;
517
+ if ((accessKeyCtx === null || accessKeyCtx === void 0 ? void 0 : accessKeyCtx.policy.routeId) && accessKeyCtx.policy.routeId !== 'system') {
518
+ // 策略绑定了具体路由
519
+ route = this.dbManager.getRoutes().find((r) => r.id === accessKeyCtx.policy.routeId);
520
+ if (!route) {
521
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKeyCtx.accessKey.id);
522
+ this.sendAccessKeyError(res, { type: 'permission_error', code: 'NO_ROUTE_CONFIGURED', message: '策略绑定的路由不存在', httpStatus: 403 }, req.path.startsWith('/claude-code/'));
523
+ return;
524
+ }
525
+ }
526
+ else {
527
+ // routeId 为空或 'system':按系统默认路由
528
+ route = this.findMatchingRoute(req);
529
+ }
340
530
  if (!route) {
341
531
  // 没有找到激活的路由,尝试使用原始配置
342
532
  const fallbackResult = yield this.handleFallbackToOriginalConfig(req, res, requestStartAt);
@@ -355,7 +545,7 @@ class ProxyServer {
355
545
  }
356
546
  // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
357
547
  const forcedContentType = yield this.prepareHighIqRouting(req, route, this.inferTargetTypeFromPath(req.path) || 'claude-code');
358
- const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
548
+ const enableFailover = ((_b = this.config) === null || _b === void 0 ? void 0 : _b.enableFailover) !== false; // 默认为 true
359
549
  if (!enableFailover) {
360
550
  // 故障切换已禁用,使用传统的单一规则匹配
361
551
  const rule = yield this.findMatchingRule(route.id, req, forcedContentType);
@@ -438,7 +628,7 @@ class ProxyServer {
438
628
  lastFailedService = service;
439
629
  // 检测是否是 timeout 错误
440
630
  const isTimeout = error.code === 'ECONNABORTED' ||
441
- ((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
631
+ ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
442
632
  (error.errno && error.errno === 'ETIMEDOUT');
443
633
  // 判断错误类型并加入黑名单
444
634
  if (isTimeout) {
@@ -510,13 +700,13 @@ class ProxyServer {
510
700
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
511
701
  // 添加请求详情
512
702
  targetType,
513
- requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
703
+ requestModel: (_d = req.body) === null || _d === void 0 ? void 0 : _d.model,
514
704
  responseTime: Date.now() - requestStartAt,
515
705
  // 添加最后失败的服务信息
516
706
  ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
517
707
  targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
518
708
  targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
519
- targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
709
+ targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
520
710
  vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
521
711
  vendorName: _lastFailedVendor === null || _lastFailedVendor === void 0 ? void 0 : _lastFailedVendor.name,
522
712
  });
@@ -544,29 +734,38 @@ class ProxyServer {
544
734
  }
545
735
  catch (error) {
546
736
  console.error('Proxy error:', error);
547
- yield this.logToolRequest(req, {
548
- statusCode: 500,
549
- responseTime: Date.now() - requestStartAt,
550
- targetType: this.inferTargetTypeFromPath(req.path),
551
- error: error.message,
552
- tags: this.buildRelayTags(hasRelayAttempt),
553
- });
737
+ // AccessKey 错误处理:递减并发计数
738
+ const accessKeyCtx = req._accessKeyCtx;
739
+ if (accessKeyCtx && this.accessKeyModule) {
740
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKeyCtx.accessKey.id);
741
+ yield this.accessKeyModule.usageTracker.recordError(accessKeyCtx.accessKey.id);
742
+ }
743
+ else {
744
+ yield this.logToolRequest(req, {
745
+ statusCode: 500,
746
+ responseTime: Date.now() - requestStartAt,
747
+ targetType: this.inferTargetTypeFromPath(req.path),
748
+ error: error.message,
749
+ tags: this.buildRelayTags(hasRelayAttempt),
750
+ });
751
+ }
554
752
  // Add error log - 包含请求详情
555
- const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
556
- yield this.dbManager.addErrorLog({
557
- timestamp: Date.now(),
558
- method: req.method,
559
- path: req.path,
560
- statusCode: 500,
561
- errorMessage: error.message,
562
- errorStack: error.stack,
563
- requestHeaders: this.normalizeHeaders(req.headers),
564
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
565
- // 添加请求详情
566
- targetType,
567
- requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
568
- responseTime: Date.now() - requestStartAt,
569
- });
753
+ if (!accessKeyCtx) {
754
+ const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
755
+ yield this.dbManager.addErrorLog({
756
+ timestamp: Date.now(),
757
+ method: req.method,
758
+ path: req.path,
759
+ statusCode: 500,
760
+ errorMessage: error.message,
761
+ errorStack: error.stack,
762
+ requestHeaders: this.normalizeHeaders(req.headers),
763
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
764
+ targetType,
765
+ requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
766
+ responseTime: Date.now() - requestStartAt,
767
+ });
768
+ }
570
769
  // 根据路径判断目标类型并返回适当的错误格式
571
770
  const isClaudeCode = req.path.startsWith('/claude-code/');
572
771
  if (this.isResponseCommitted(res)) {
@@ -597,26 +796,72 @@ class ProxyServer {
597
796
  }
598
797
  createFixedRouteHandler(targetType) {
599
798
  return (req, res) => __awaiter(this, void 0, void 0, function* () {
600
- var _a, _b, _c, _d, _e;
799
+ var _a, _b, _c, _d, _e, _f;
601
800
  const requestStartAt = Date.now();
602
801
  let hasRelayAttempt = false;
802
+ let accessKeyCtx = null;
603
803
  try {
604
- // 检查API Key验证
605
- if (this.config.apiKey) {
606
- const authHeader = req.headers.authorization;
607
- const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
608
- if (!providedKey || providedKey !== this.config.apiKey) {
609
- yield this.logToolRequest(req, {
610
- statusCode: 401,
611
- responseTime: Date.now() - requestStartAt,
612
- targetType,
613
- error: 'Invalid API key',
614
- tags: this.buildRelayTags(false),
615
- });
616
- return res.status(401).json({ error: 'Invalid API key' });
804
+ // 检查 API Key 验证(支持 AccessKey 和全局 apiKey)
805
+ const apiKeyValue = this.extractApiKey(req);
806
+ if ((apiKeyValue === null || apiKeyValue === void 0 ? void 0 : apiKeyValue.startsWith('sk_')) && this.accessKeyModule) {
807
+ // AccessKey 鉴权
808
+ const result = this.accessKeyModule.keyResolver.resolve(apiKeyValue);
809
+ if (!result || 'error' in result) {
810
+ const err = result ? result.error : { type: 'authentication_error', code: 'INVALID_API_KEY', message: '无效的 API Key', httpStatus: 401 };
811
+ this.sendAccessKeyError(res, err, targetType === 'claude-code');
812
+ return;
813
+ }
814
+ accessKeyCtx = result;
815
+ // 配额检查
816
+ const model = (_a = req.body) === null || _a === void 0 ? void 0 : _a.model;
817
+ const usage = yield this.accessKeyModule.usageTracker.getUsage(accessKeyCtx.accessKey.id);
818
+ const quotaResult = this.accessKeyModule.quotaChecker.checkQuota(accessKeyCtx.policy, usage, accessKeyCtx.accessKey.id, model);
819
+ if (quotaResult) {
820
+ this.sendAccessKeyError(res, { type: 'rate_limit_error', code: quotaResult.error, message: quotaResult.message, httpStatus: quotaResult.httpStatus }, targetType === 'claude-code');
821
+ return;
617
822
  }
823
+ // 并发 +1
824
+ this.accessKeyModule.quotaChecker.onRequestStart(accessKeyCtx.accessKey.id, accessKeyCtx.policy);
825
+ }
826
+ else if ((0, auth_1.isAuthEnabled)()) {
827
+ // AUTH 已启用 → 仅允许 AccessKey 认证
828
+ console.log(`\x1b[31m[AUTH] 511\x1b[0m ${req.method} ${req.path} — 未提供有效的 AccessKey (targetType: ${targetType})`);
829
+ yield this.logToolRequest(req, {
830
+ statusCode: 511,
831
+ responseTime: Date.now() - requestStartAt,
832
+ targetType,
833
+ error: 'Authentication required',
834
+ tags: this.buildRelayTags(false),
835
+ });
836
+ this.sendAuthError(res, targetType === 'claude-code');
837
+ return;
838
+ }
839
+ // 注入 AccessKey 上下文到请求对象,供 proxyRequest 内部的 finalizeLog 使用
840
+ if (accessKeyCtx) {
841
+ req._accessKeyCtx = accessKeyCtx;
842
+ }
843
+ // 确定路由:AccessKey 请求从策略获取,否则从工具绑定获取
844
+ let route;
845
+ if (accessKeyCtx) {
846
+ const policyRouteId = accessKeyCtx.policy.routeId;
847
+ if (policyRouteId && policyRouteId !== 'system') {
848
+ // 策略绑定了具体路由
849
+ const allRoutes = this.dbManager.getRoutes();
850
+ route = allRoutes.find((r) => r.id === policyRouteId);
851
+ if (!route) {
852
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKeyCtx.accessKey.id);
853
+ this.sendAccessKeyError(res, { type: 'permission_error', code: 'NO_ROUTE_CONFIGURED', message: '策略绑定的路由不存在', httpStatus: 403 }, targetType === 'claude-code');
854
+ return;
855
+ }
856
+ }
857
+ else {
858
+ // routeId 为空或 'system':按系统默认路由
859
+ route = this.findRouteByTargetType(targetType);
860
+ }
861
+ }
862
+ else {
863
+ route = this.findRouteByTargetType(targetType);
618
864
  }
619
- const route = this.findRouteByTargetType(targetType);
620
865
  if (!route) {
621
866
  yield this.logToolRequest(req, {
622
867
  statusCode: 404,
@@ -630,7 +875,7 @@ class ProxyServer {
630
875
  // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
631
876
  const forcedContentType = yield this.prepareHighIqRouting(req, route, targetType);
632
877
  // 检查是否启用故障切换
633
- const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
878
+ const enableFailover = ((_b = this.config) === null || _b === void 0 ? void 0 : _b.enableFailover) !== false; // 默认为 true
634
879
  if (!enableFailover) {
635
880
  // 故障切换已禁用,使用传统的单一规则匹配
636
881
  const rule = yield this.findMatchingRule(route.id, req, forcedContentType);
@@ -713,7 +958,7 @@ class ProxyServer {
713
958
  lastFailedService = service;
714
959
  // 检测是否是 timeout 错误
715
960
  const isTimeout = error.code === 'ECONNABORTED' ||
716
- ((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
961
+ ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
717
962
  (error.errno && error.errno === 'ETIMEDOUT');
718
963
  // 判断错误类型并加入黑名单
719
964
  if (isTimeout) {
@@ -783,13 +1028,13 @@ class ProxyServer {
783
1028
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
784
1029
  // 添加请求详情
785
1030
  targetType,
786
- requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
1031
+ requestModel: (_d = req.body) === null || _d === void 0 ? void 0 : _d.model,
787
1032
  responseTime: Date.now() - requestStartAt,
788
1033
  // 添加最后失败的服务信息
789
1034
  ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
790
1035
  targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
791
1036
  targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
792
- targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
1037
+ targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
793
1038
  vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
794
1039
  vendorName: _lastFailedVendor2 === null || _lastFailedVendor2 === void 0 ? void 0 : _lastFailedVendor2.name,
795
1040
  });
@@ -817,28 +1062,35 @@ class ProxyServer {
817
1062
  }
818
1063
  catch (error) {
819
1064
  console.error(`Fixed route error for ${targetType}:`, error);
820
- yield this.logToolRequest(req, {
821
- statusCode: 500,
822
- responseTime: Date.now() - requestStartAt,
823
- targetType,
824
- error: error.message,
825
- tags: this.buildRelayTags(hasRelayAttempt),
826
- });
827
- // Add error log - 包含请求详情(使用函数参数 targetType)
828
- yield this.dbManager.addErrorLog({
829
- timestamp: Date.now(),
830
- method: req.method,
831
- path: req.path,
832
- statusCode: 500,
833
- errorMessage: error.message,
834
- errorStack: error.stack,
835
- requestHeaders: this.normalizeHeaders(req.headers),
836
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
837
- // 添加请求详情
838
- targetType,
839
- requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
840
- responseTime: Date.now() - requestStartAt,
841
- });
1065
+ // AccessKey 错误处理:递减并发计数
1066
+ const accessKeyCtx = req._accessKeyCtx;
1067
+ if (accessKeyCtx && this.accessKeyModule) {
1068
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKeyCtx.accessKey.id);
1069
+ yield this.accessKeyModule.usageTracker.recordError(accessKeyCtx.accessKey.id);
1070
+ }
1071
+ else {
1072
+ yield this.logToolRequest(req, {
1073
+ statusCode: 500,
1074
+ responseTime: Date.now() - requestStartAt,
1075
+ targetType,
1076
+ error: error.message,
1077
+ tags: this.buildRelayTags(hasRelayAttempt),
1078
+ });
1079
+ // Add error log - 包含请求详情(使用函数参数 targetType)
1080
+ yield this.dbManager.addErrorLog({
1081
+ timestamp: Date.now(),
1082
+ method: req.method,
1083
+ path: req.path,
1084
+ statusCode: 500,
1085
+ errorMessage: error.message,
1086
+ errorStack: error.stack,
1087
+ requestHeaders: this.normalizeHeaders(req.headers),
1088
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1089
+ targetType,
1090
+ requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
1091
+ responseTime: Date.now() - requestStartAt,
1092
+ });
1093
+ }
842
1094
  if (this.isResponseCommitted(res)) {
843
1095
  return;
844
1096
  }
@@ -872,6 +1124,24 @@ class ProxyServer {
872
1124
  }
873
1125
  if (!tool)
874
1126
  return undefined;
1127
+ // 优先检查会话级路由绑定
1128
+ const sessionId = this.defaultExtractSessionId(req, tool);
1129
+ if (sessionId) {
1130
+ const session = this.dbManager.getSession(sessionId);
1131
+ if (session === null || session === void 0 ? void 0 : session.routeId) {
1132
+ const boundRoute = this.dbManager.getRoute(session.routeId);
1133
+ if (boundRoute) {
1134
+ console.log(`[SESSION-ROUTE] Session ${sessionId} using bound route: ${boundRoute.name} (${boundRoute.id})`);
1135
+ return boundRoute;
1136
+ }
1137
+ else {
1138
+ // 路由已被删除,自动清除绑定
1139
+ console.log(`[SESSION-ROUTE] Bound route ${session.routeId} not found for session ${sessionId}, clearing binding`);
1140
+ this.dbManager.unbindSessionRoute(sessionId).catch(console.error);
1141
+ }
1142
+ }
1143
+ }
1144
+ // 回退到全局工具绑定
875
1145
  const routeId = this.dbManager.getActiveRouteIdForTool(tool);
876
1146
  if (!routeId)
877
1147
  return undefined;
@@ -1040,7 +1310,9 @@ class ProxyServer {
1040
1310
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, routeId, rule.contentType);
1041
1311
  if (isBlacklisted)
1042
1312
  continue;
1043
- return service.name;
1313
+ const vendors = this.dbManager.getVendors();
1314
+ const vendor = vendors.find(v => v.id === service.vendorId);
1315
+ return vendor ? `${vendor.name}-${service.name}` : service.name;
1044
1316
  }
1045
1317
  return undefined;
1046
1318
  });
@@ -1049,7 +1321,7 @@ class ProxyServer {
1049
1321
  if (!forwardedToServiceName) {
1050
1322
  return '';
1051
1323
  }
1052
- return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
1324
+ return `;已自动转发给「${forwardedToServiceName}」服务继续处理`;
1053
1325
  }
1054
1326
  /**
1055
1327
  * 解析规则的有效超时时间(毫秒)。
@@ -1446,7 +1718,9 @@ class ProxyServer {
1446
1718
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
1447
1719
  const candidates = [];
1448
1720
  const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
1449
- const prioritizeContentType = contentType === 'high-iq';
1721
+ // 所有特定内容类型(compact, thinking, long-context 等)优先于 model-mapping,
1722
+ // 保持与 findMatchingRule 中的优先级顺序一致
1723
+ const prioritizeContentType = contentType !== 'default';
1450
1724
  const modelMappingRules = requestModel
1451
1725
  ? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
1452
1726
  rule.replacedModel &&
@@ -1591,6 +1865,9 @@ class ProxyServer {
1591
1865
  const sessionId = this.defaultExtractSessionId(req, targetType);
1592
1866
  for (const detector of this.getContentTypeDetectors()) {
1593
1867
  if (detector.match(req, body, sessionId, routeId)) {
1868
+ if (detector.type === 'compact') {
1869
+ console.log('[CONTENT-TYPE] Detected compact request');
1870
+ }
1594
1871
  return detector.type;
1595
1872
  }
1596
1873
  }
@@ -2385,9 +2662,9 @@ class ProxyServer {
2385
2662
  headers[key] = value.join(', ');
2386
2663
  }
2387
2664
  }
2388
- // 确定认证方式:优先使用服务配置的 authType
2665
+ // 确定认证方式:优先使用服务配置的 authType,若继承供应商则使用供应商的 authType
2389
2666
  // 注意:向下兼容 'auto' 字符串值(前端已移除 AuthType.AUTO 枚举,但旧数据可能包含此值)
2390
- const authType = service.authType || types_1.AuthType.AUTH_TOKEN;
2667
+ const authType = this.resolveEffectiveAuthType(service);
2391
2668
  // 向下兼容:检测旧数据的 'auto' 值
2392
2669
  // TODO: 删除
2393
2670
  const isAuto = authType === 'auto';
@@ -2427,6 +2704,10 @@ class ProxyServer {
2427
2704
  const bodyStr = JSON.stringify(requestBody);
2428
2705
  headers['content-length'] = Buffer.byteLength(bodyStr, 'utf8').toString();
2429
2706
  }
2707
+ // 编程套餐 Headers 覆盖:当服务启用了编程套餐时,替换为编程工具的标准 Headers
2708
+ if (service.enableCodingPlan) {
2709
+ (0, coding_plan_headers_1.applyCodingPlanHeaders)(headers, sourceType);
2710
+ }
2430
2711
  return headers;
2431
2712
  }
2432
2713
  resolveEffectiveApiKey(service) {
@@ -2451,6 +2732,17 @@ class ProxyServer {
2451
2732
  }
2452
2733
  return vendor.apiBaseUrl;
2453
2734
  }
2735
+ resolveEffectiveAuthType(service) {
2736
+ if (service.inheritVendorAuthType !== true) {
2737
+ return service.authType || types_1.AuthType.AUTH_TOKEN;
2738
+ }
2739
+ const vendor = this.dbManager.getVendorByServiceId(service.id);
2740
+ if (!vendor || !vendor.authType) {
2741
+ console.warn(`[Proxy] Service ${service.id} is set to inherit vendor authType, but vendor/authType is missing`);
2742
+ return service.authType || types_1.AuthType.AUTH_TOKEN;
2743
+ }
2744
+ return vendor.authType;
2745
+ }
2454
2746
  copyResponseHeaders(responseHeaders, res) {
2455
2747
  Object.keys(responseHeaders).forEach((key) => {
2456
2748
  if (!['content-encoding', 'transfer-encoding', 'connection', 'content-length'].includes(key.toLowerCase())) {
@@ -2599,6 +2891,23 @@ class ProxyServer {
2599
2891
  }
2600
2892
  return rawUserId;
2601
2893
  }
2894
+ /**
2895
+ * 根据客户端格式提取 session ID(用于标准 API 路径的会话级路由覆盖)
2896
+ */
2897
+ extractSessionIdForFormat(request, format) {
2898
+ var _a, _b;
2899
+ if (format === 'claude') {
2900
+ const rawUserId = (_b = (_a = request.body) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.user_id;
2901
+ return ProxyServer.extractSessionIdFromUserId(rawUserId);
2902
+ }
2903
+ // 对于 completions/responses/gemini 格式,尝试从 headers 中提取
2904
+ const sessionId = request.headers['session-id'] || request.headers['session_id'];
2905
+ if (typeof sessionId === 'string')
2906
+ return sessionId;
2907
+ if (Array.isArray(sessionId))
2908
+ return sessionId[0] || null;
2909
+ return null;
2910
+ }
2602
2911
  /**
2603
2912
  * 提取会话标题(默认方法)
2604
2913
  * 对于新会话,尝试从第一条消息的内容中提取标题
@@ -2685,6 +2994,7 @@ class ProxyServer {
2685
2994
  formatSessionTitle(text) {
2686
2995
  // 去除多余空白和换行符,替换为单个空格
2687
2996
  let formatted = text
2997
+ .replace(/<\/?session>/g, '') // 移除 <session></session> 标签
2688
2998
  .replace(/\s+/g, ' ') // 多个空白字符替换为单个空格
2689
2999
  .replace(/[\r\n]+/g, ' ') // 换行符替换为空格
2690
3000
  .trim();
@@ -2883,7 +3193,7 @@ class ProxyServer {
2883
3193
  }
2884
3194
  proxyRequest(req, res, route, rule, service, options) {
2885
3195
  return __awaiter(this, void 0, void 0, function* () {
2886
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
3196
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
2887
3197
  res.locals.skipLog = true;
2888
3198
  const startTime = Date.now();
2889
3199
  const rawSourceType = service.sourceType || 'openai-chat';
@@ -3059,7 +3369,7 @@ class ProxyServer {
3059
3369
  // 标记规则正在使用
3060
3370
  rules_status_service_1.rulesStatusBroadcaster.markRuleInUse(route.id, rule.id);
3061
3371
  const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
3062
- var _a, _b;
3372
+ var _a, _b, _c, _d, _e, _f, _g;
3063
3373
  if (logged)
3064
3374
  return;
3065
3375
  const isError = statusCode >= 400;
@@ -3075,11 +3385,109 @@ class ProxyServer {
3075
3385
  return;
3076
3386
  }
3077
3387
  logged = true;
3388
+ // ========== AccessKey 独立日志和统计 ==========
3389
+ const accessKeyCtx = req._accessKeyCtx;
3390
+ if (accessKeyCtx && this.accessKeyModule) {
3391
+ const { accessKey } = accessKeyCtx;
3392
+ try {
3393
+ // 写入 Key 独立日志
3394
+ yield this.accessKeyModule.keyLogger.addLog(accessKey.id, accessKey.name, {
3395
+ timestamp: Date.now(),
3396
+ method: req.method,
3397
+ path: req.originalUrl || req.path,
3398
+ headers: this.normalizeHeaders(req.headers),
3399
+ body: req.body,
3400
+ statusCode,
3401
+ responseTime: Date.now() - startTime,
3402
+ targetProvider: service.name,
3403
+ usage: usageForLog,
3404
+ error,
3405
+ contentType: rule.contentType,
3406
+ ruleId: rule.id,
3407
+ targetType,
3408
+ targetServiceId: service.id,
3409
+ targetServiceName: service.name,
3410
+ targetModel: rule.targetModel || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.model),
3411
+ vendorId: service.vendorId,
3412
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3413
+ requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
3414
+ tags: this.buildRelayTags(relayedForLog, useOriginalConfig),
3415
+ responseHeaders: responseHeadersForLog,
3416
+ responseBody: responseBodyForLog,
3417
+ streamChunks: streamChunksForLog,
3418
+ upstreamRequest: upstreamRequestForLog,
3419
+ downstreamResponseBody: downstreamResponseBodyForLog !== null && downstreamResponseBodyForLog !== void 0 ? downstreamResponseBodyForLog : responseBodyForLog,
3420
+ });
3421
+ // Token 回写
3422
+ if (usageForLog && statusCode < 400) {
3423
+ yield this.accessKeyModule.usageTracker.recordTokenUsage(accessKey.id, usageForLog);
3424
+ }
3425
+ else if (statusCode < 400) {
3426
+ yield this.accessKeyModule.usageTracker.recordRequest(accessKey.id);
3427
+ }
3428
+ // 错误记录
3429
+ if (statusCode >= 400) {
3430
+ yield this.accessKeyModule.usageTracker.recordError(accessKey.id);
3431
+ }
3432
+ // 密钥级会话追踪
3433
+ if (sessionId && sessionId !== '-' && statusCode < 400) {
3434
+ const sessionTokens = (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) ||
3435
+ (((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0));
3436
+ const sessionTitle = this.defaultExtractSessionTitle(req, sessionId);
3437
+ this.accessKeyModule.keySessionTracker.upsertSession(accessKey.id, {
3438
+ id: sessionId,
3439
+ targetType,
3440
+ title: sessionTitle,
3441
+ firstRequestAt: startTime,
3442
+ lastRequestAt: Date.now(),
3443
+ vendorId: service.vendorId,
3444
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3445
+ serviceId: service.id,
3446
+ serviceName: service.name,
3447
+ model: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
3448
+ totalTokens: sessionTokens,
3449
+ }).catch(err => console.error('[KeySession] upsert error:', err));
3450
+ }
3451
+ }
3452
+ finally {
3453
+ // 并发 -1(无论成功失败)
3454
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKey.id);
3455
+ }
3456
+ // 同步全局统计数据(不写日志,仅更新统计)
3457
+ try {
3458
+ yield this.dbManager.syncStatisticsFromAccessKey({
3459
+ timestamp: Date.now(),
3460
+ method: req.method,
3461
+ path: req.originalUrl || req.path,
3462
+ headers: this.normalizeHeaders(req.headers),
3463
+ body: req.body,
3464
+ statusCode,
3465
+ responseTime: Date.now() - startTime,
3466
+ targetProvider: service.name,
3467
+ usage: usageForLog,
3468
+ error,
3469
+ contentType: rule.contentType,
3470
+ ruleId: rule.id,
3471
+ targetType,
3472
+ targetServiceId: service.id,
3473
+ targetServiceName: service.name,
3474
+ targetModel: rule.targetModel || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
3475
+ vendorId: service.vendorId,
3476
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3477
+ requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
3478
+ tags: this.buildRelayTags(relayedForLog, useOriginalConfig),
3479
+ });
3480
+ }
3481
+ catch (statsErr) {
3482
+ console.error('[AccessKey] Failed to sync global statistics:', statsErr);
3483
+ }
3484
+ return; // ⛔ 跳过现有日志系统
3485
+ }
3078
3486
  // 供应商信息已在函数顶部获取
3079
3487
  const vendors = this.dbManager.getVendors();
3080
3488
  const vendorForLog = vendors.find(v => v.id === service.vendorId);
3081
3489
  // 从请求体中提取模型信息
3082
- const requestModel = (_b = req.body) === null || _b === void 0 ? void 0 : _b.model;
3490
+ const requestModel = (_g = req.body) === null || _g === void 0 ? void 0 : _g.model;
3083
3491
  const tagsForLog = this.buildRelayTags(relayedForLog, useOriginalConfig);
3084
3492
  if (extraTagsForLog.length > 0) {
3085
3493
  tagsForLog.push(...extraTagsForLog);
@@ -3412,10 +3820,15 @@ class ProxyServer {
3412
3820
  const parser = new streaming_1.SSEParserTransform();
3413
3821
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3414
3822
  const serializer = new streaming_1.SSESerializerTransform();
3415
- const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
3823
+ const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
3824
+ rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
3825
+ });
3416
3826
  const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
3417
3827
  ? new ClaudeCompactResponseSanitizer()
3418
3828
  : null;
3829
+ // 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
3830
+ const originalModel = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
3831
+ const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
3419
3832
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3420
3833
  // 使用 transformSSEToTool 方法选择转换器
3421
3834
  const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
@@ -3455,7 +3868,11 @@ class ProxyServer {
3455
3868
  if (compactResponseSanitizer) {
3456
3869
  streamStages.push(compactResponseSanitizer);
3457
3870
  }
3458
- streamStages.push(serializer, downstreamChunkCollector, res);
3871
+ streamStages.push(serializer);
3872
+ if (modelRewriter) {
3873
+ streamStages.push(modelRewriter);
3874
+ }
3875
+ streamStages.push(downstreamChunkCollector, res);
3459
3876
  (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], streamStages[3], ...streamStages.slice(4), (error) => {
3460
3877
  if (error) {
3461
3878
  reject(error);
@@ -3469,7 +3886,11 @@ class ProxyServer {
3469
3886
  if (compactResponseSanitizer) {
3470
3887
  streamStages.push(compactResponseSanitizer);
3471
3888
  }
3472
- streamStages.push(serializer, downstreamChunkCollector, res);
3889
+ streamStages.push(serializer);
3890
+ if (modelRewriter) {
3891
+ streamStages.push(modelRewriter);
3892
+ }
3893
+ streamStages.push(downstreamChunkCollector, res);
3473
3894
  (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], ...streamStages.slice(3), (error) => {
3474
3895
  if (error) {
3475
3896
  reject(error);
@@ -3506,10 +3927,10 @@ class ProxyServer {
3506
3927
  targetType,
3507
3928
  targetServiceId: service.id,
3508
3929
  targetServiceName: service.name,
3509
- targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
3930
+ targetModel: rule.targetModel || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
3510
3931
  vendorId: service.vendorId,
3511
3932
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3512
- requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
3933
+ requestModel: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
3513
3934
  responseTime: Date.now() - startTime,
3514
3935
  });
3515
3936
  }
@@ -3553,10 +3974,10 @@ class ProxyServer {
3553
3974
  targetType,
3554
3975
  targetServiceId: service.id,
3555
3976
  targetServiceName: service.name,
3556
- targetModel: rule.targetModel || ((_f = req.body) === null || _f === void 0 ? void 0 : _f.model),
3977
+ targetModel: rule.targetModel || ((_g = req.body) === null || _g === void 0 ? void 0 : _g.model),
3557
3978
  vendorId: service.vendorId,
3558
3979
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3559
- requestModel: (_g = req.body) === null || _g === void 0 ? void 0 : _g.model,
3980
+ requestModel: (_h = req.body) === null || _h === void 0 ? void 0 : _h.model,
3560
3981
  responseTime: Date.now() - startTime,
3561
3982
  });
3562
3983
  }
@@ -3575,7 +3996,7 @@ class ProxyServer {
3575
3996
  let responseData = response.data;
3576
3997
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
3577
3998
  const raw = yield this.readStreamBody(response.data);
3578
- responseData = (_h = this.safeJsonParse(raw)) !== null && _h !== void 0 ? _h : raw;
3999
+ responseData = (_j = this.safeJsonParse(raw)) !== null && _j !== void 0 ? _j : raw;
3579
4000
  }
3580
4001
  // 收集响应头
3581
4002
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
@@ -3596,6 +4017,10 @@ class ProxyServer {
3596
4017
  // 提取 token usage(从原始响应数据中提取)
3597
4018
  usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
3598
4019
  console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
4020
+ // 回写 model 字段:将上游返回的 model 改写为客户端请求时的原始模型名
4021
+ const originalModel = (_k = req.body) === null || _k === void 0 ? void 0 : _k.model;
4022
+ (0, model_rewrite_transform_1.rewriteResponseModel)(normalizedConverted, originalModel);
4023
+ (0, model_rewrite_transform_1.rewriteResponseModel)(responseData, originalModel);
3599
4024
  this.copyResponseHeaders(responseHeaders, res);
3600
4025
  if (normalizedConverted && normalizedConverted !== responseData) {
3601
4026
  // 非流式:responseBody 记录上游原始响应,downstreamResponseBody 记录转换后下发内容
@@ -3658,10 +4083,10 @@ class ProxyServer {
3658
4083
  targetType,
3659
4084
  targetServiceId: service.id,
3660
4085
  targetServiceName: service.name,
3661
- targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
4086
+ targetModel: rule.targetModel || ((_l = req.body) === null || _l === void 0 ? void 0 : _l.model),
3662
4087
  vendorId: service.vendorId,
3663
4088
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3664
- requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
4089
+ requestModel: (_m = req.body) === null || _m === void 0 ? void 0 : _m.model,
3665
4090
  upstreamRequest: upstreamRequestForLog,
3666
4091
  responseTime: Date.now() - startTime,
3667
4092
  });
@@ -3676,7 +4101,7 @@ class ProxyServer {
3676
4101
  console.error('Proxy error:', error);
3677
4102
  // 检测是否是 timeout 错误
3678
4103
  const isTimeout = error.code === 'ECONNABORTED' ||
3679
- ((_l = error.message) === null || _l === void 0 ? void 0 : _l.toLowerCase().includes('timeout')) ||
4104
+ ((_o = error.message) === null || _o === void 0 ? void 0 : _o.toLowerCase().includes('timeout')) ||
3680
4105
  (error.errno && error.errno === 'ETIMEDOUT');
3681
4106
  const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
3682
4107
  const baseErrorMessage = isTimeout
@@ -3702,10 +4127,10 @@ class ProxyServer {
3702
4127
  targetType,
3703
4128
  targetServiceId: service.id,
3704
4129
  targetServiceName: service.name,
3705
- targetModel: rule.targetModel || ((_m = req.body) === null || _m === void 0 ? void 0 : _m.model),
4130
+ targetModel: rule.targetModel || ((_p = req.body) === null || _p === void 0 ? void 0 : _p.model),
3706
4131
  vendorId: service.vendorId,
3707
4132
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3708
- requestModel: (_o = req.body) === null || _o === void 0 ? void 0 : _o.model,
4133
+ requestModel: (_q = req.body) === null || _q === void 0 ? void 0 : _q.model,
3709
4134
  upstreamRequest: upstreamRequestForLog,
3710
4135
  responseHeaders: responseHeadersForLog,
3711
4136
  responseTime: Date.now() - startTime,
@@ -3801,32 +4226,39 @@ class ProxyServer {
3801
4226
  * 处理通过标准 API 路径(/v1/messages, /v1/responses 等)进入的代理请求。
3802
4227
  * 与原有 proxyRequest 逻辑独立,复用规则匹配、故障切换等机制。
3803
4228
  */
3804
- handleApiPathProxyRequest(req, res, route, clientFormat, apiPath) {
4229
+ handleApiPathProxyRequest(req, res, route, clientFormat, apiPath, accessKeyCtx) {
3805
4230
  return __awaiter(this, void 0, void 0, function* () {
3806
- var _a, _b;
4231
+ var _a, _b, _c;
3807
4232
  const requestStartAt = Date.now();
3808
- // 鉴权
3809
- if (this.config.apiKey) {
3810
- const authHeader = req.headers.authorization;
3811
- const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
3812
- if (!providedKey || providedKey !== this.config.apiKey) {
4233
+ // AccessKey 请求已在上层完成鉴权;非 AccessKey 请求在此鉴权
4234
+ if (!accessKeyCtx) {
4235
+ if ((0, auth_1.isAuthEnabled)()) {
4236
+ // AUTH 已启用 仅允许 AccessKey 认证
4237
+ console.log(`\x1b[31m[AUTH] 511\x1b[0m ${req.method} ${req.path} — 未提供有效的 AccessKey (apiPath: ${apiPath})`);
3813
4238
  yield this.logToolRequest(req, {
3814
- statusCode: 401,
4239
+ statusCode: 511,
3815
4240
  responseTime: Date.now() - requestStartAt,
3816
- error: 'Invalid API key',
4241
+ error: 'Authentication required',
3817
4242
  tags: this.buildRelayTags(false),
3818
4243
  });
3819
- // 根据客户端格式返回错误
3820
- if (clientFormat === 'claude') {
3821
- res.status(401).json({ type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } });
3822
- }
3823
- else {
3824
- res.status(401).json({ error: { message: 'Invalid API key' } });
3825
- }
4244
+ this.sendAuthError(res, clientFormat === 'claude');
3826
4245
  return;
3827
4246
  }
3828
4247
  }
3829
- const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false;
4248
+ if (accessKeyCtx) {
4249
+ const model = (_a = req.body) === null || _a === void 0 ? void 0 : _a.model;
4250
+ const usage = yield this.accessKeyModule.usageTracker.getUsage(accessKeyCtx.accessKey.id);
4251
+ const quotaResult = this.accessKeyModule.quotaChecker.checkQuota(accessKeyCtx.policy, usage, accessKeyCtx.accessKey.id, model);
4252
+ if (quotaResult) {
4253
+ this.sendAccessKeyError(res, { type: 'rate_limit_error', code: quotaResult.error, message: quotaResult.message, httpStatus: quotaResult.httpStatus }, clientFormat === 'claude');
4254
+ return;
4255
+ }
4256
+ // 并发 +1
4257
+ this.accessKeyModule.quotaChecker.onRequestStart(accessKeyCtx.accessKey.id, accessKeyCtx.policy);
4258
+ // 注入上下文到 req 对象,供 proxyRequestForApiPath 内部的 finalizeLog 使用
4259
+ req._accessKeyCtx = accessKeyCtx;
4260
+ }
4261
+ const enableFailover = ((_b = this.config) === null || _b === void 0 ? void 0 : _b.enableFailover) !== false;
3830
4262
  if (!enableFailover) {
3831
4263
  const rule = yield this.findMatchingRule(route.id, req);
3832
4264
  if (!rule) {
@@ -3877,7 +4309,7 @@ class ProxyServer {
3877
4309
  lastFailedRule = rule;
3878
4310
  lastFailedService = service;
3879
4311
  const isTimeout = error.code === 'ECONNABORTED' ||
3880
- ((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
4312
+ ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
3881
4313
  error.errno === 'ETIMEDOUT';
3882
4314
  if (isTimeout) {
3883
4315
  yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout', undefined, 'timeout');
@@ -3920,7 +4352,7 @@ class ProxyServer {
3920
4352
  */
3921
4353
  proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, options) {
3922
4354
  return __awaiter(this, void 0, void 0, function* () {
3923
- var _a, _b, _c, _d;
4355
+ var _a, _b, _c, _d, _e, _f;
3924
4356
  const startTime = Date.now();
3925
4357
  const rawSourceType = service.sourceType || 'openai-chat';
3926
4358
  const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
@@ -3959,9 +4391,93 @@ class ProxyServer {
3959
4391
  requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
3960
4392
  }
3961
4393
  const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
4394
+ var _a, _b, _c, _d, _e;
3962
4395
  if (logged)
3963
4396
  return;
3964
4397
  logged = true;
4398
+ // AccessKey 独立日志处理
4399
+ const accessKeyCtx = req._accessKeyCtx;
4400
+ if (accessKeyCtx && this.accessKeyModule) {
4401
+ try {
4402
+ yield this.accessKeyModule.keyLogger.addLog(accessKeyCtx.accessKey.id, accessKeyCtx.accessKey.name, {
4403
+ timestamp: Date.now(),
4404
+ method: req.method,
4405
+ path: req.originalUrl || req.path,
4406
+ headers: this.normalizeHeaders(req.headers),
4407
+ body: req.body,
4408
+ statusCode,
4409
+ responseTime: Date.now() - startTime,
4410
+ usage: usageForLog,
4411
+ error,
4412
+ contentType: rule.contentType,
4413
+ ruleId: rule.id,
4414
+ targetServiceId: service.id,
4415
+ targetServiceName: service.name,
4416
+ targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
4417
+ vendorId: service.vendorId,
4418
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4419
+ requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
4420
+ tags: this.buildRelayTags(relayedForLog),
4421
+ });
4422
+ if (usageForLog && statusCode < 400) {
4423
+ yield this.accessKeyModule.usageTracker.recordTokenUsage(accessKeyCtx.accessKey.id, usageForLog);
4424
+ }
4425
+ if (statusCode >= 400) {
4426
+ yield this.accessKeyModule.usageTracker.recordError(accessKeyCtx.accessKey.id);
4427
+ }
4428
+ // 密钥级会话追踪
4429
+ const apiSessionTargetType = clientFormat === 'claude' ? 'claude-code' : 'codex';
4430
+ const apiSessionId = this.extractSessionIdForFormat(req, clientFormat);
4431
+ if (apiSessionId && apiSessionId !== '-' && statusCode < 400) {
4432
+ const sessionTokens = (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) ||
4433
+ (((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0));
4434
+ const sessionTitle = this.defaultExtractSessionTitle(req, apiSessionId);
4435
+ this.accessKeyModule.keySessionTracker.upsertSession(accessKeyCtx.accessKey.id, {
4436
+ id: apiSessionId,
4437
+ targetType: apiSessionTargetType,
4438
+ title: sessionTitle,
4439
+ firstRequestAt: startTime,
4440
+ lastRequestAt: Date.now(),
4441
+ vendorId: service.vendorId,
4442
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4443
+ serviceId: service.id,
4444
+ serviceName: service.name,
4445
+ model: rule.targetModel || ((_c = req.body) === null || _c === void 0 ? void 0 : _c.model),
4446
+ totalTokens: sessionTokens,
4447
+ }).catch(err => console.error('[KeySession] upsert error:', err));
4448
+ }
4449
+ }
4450
+ finally {
4451
+ this.accessKeyModule.quotaChecker.onRequestEnd(accessKeyCtx.accessKey.id);
4452
+ }
4453
+ // 同步全局统计数据(不写日志,仅更新统计)
4454
+ try {
4455
+ yield this.dbManager.syncStatisticsFromAccessKey({
4456
+ timestamp: Date.now(),
4457
+ method: req.method,
4458
+ path: req.originalUrl || req.path,
4459
+ headers: this.normalizeHeaders(req.headers),
4460
+ body: req.body,
4461
+ statusCode,
4462
+ responseTime: Date.now() - startTime,
4463
+ usage: usageForLog,
4464
+ error,
4465
+ contentType: rule.contentType,
4466
+ ruleId: rule.id,
4467
+ targetServiceId: service.id,
4468
+ targetServiceName: service.name,
4469
+ targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
4470
+ vendorId: service.vendorId,
4471
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
4472
+ requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
4473
+ tags: this.buildRelayTags(relayedForLog),
4474
+ });
4475
+ }
4476
+ catch (statsErr) {
4477
+ console.error('[AccessKey] Failed to sync global statistics:', statsErr);
4478
+ }
4479
+ return;
4480
+ }
3965
4481
  yield this.logToolRequest(req, {
3966
4482
  statusCode,
3967
4483
  responseTime: Date.now() - startTime,
@@ -4093,8 +4609,13 @@ class ProxyServer {
4093
4609
  const parser = new streaming_1.SSEParserTransform();
4094
4610
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
4095
4611
  const serializer = new streaming_1.SSESerializerTransform();
4096
- const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
4612
+ const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
4613
+ rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
4614
+ });
4097
4615
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4616
+ // 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
4617
+ const originalModel = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
4618
+ const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
4098
4619
  const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
4099
4620
  this.copyResponseHeaders(responseHeaders, res);
4100
4621
  res.status(response.status);
@@ -4117,8 +4638,16 @@ class ProxyServer {
4117
4638
  };
4118
4639
  try {
4119
4640
  yield new Promise((resolve, reject) => {
4641
+ const buildStages = (...upstream) => {
4642
+ const stages = [...upstream, serializer];
4643
+ if (modelRewriter)
4644
+ stages.push(modelRewriter);
4645
+ stages.push(downstreamChunkCollector, res);
4646
+ return stages;
4647
+ };
4120
4648
  if (converter) {
4121
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
4649
+ const stages = buildStages(response.data, parser, eventCollector, converter);
4650
+ stream_1.pipeline(...stages, (error) => {
4122
4651
  if (error) {
4123
4652
  reject(error);
4124
4653
  return;
@@ -4127,7 +4656,8 @@ class ProxyServer {
4127
4656
  });
4128
4657
  }
4129
4658
  else {
4130
- (0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
4659
+ const stages = buildStages(response.data, parser, eventCollector);
4660
+ stream_1.pipeline(...stages, (error) => {
4131
4661
  if (error) {
4132
4662
  reject(error);
4133
4663
  return;
@@ -4157,7 +4687,7 @@ class ProxyServer {
4157
4687
  let responseData = response.data;
4158
4688
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
4159
4689
  const raw = yield this.readStreamBody(response.data);
4160
- responseData = (_d = this.safeJsonParse(raw)) !== null && _d !== void 0 ? _d : raw;
4690
+ responseData = (_e = this.safeJsonParse(raw)) !== null && _e !== void 0 ? _e : raw;
4161
4691
  }
4162
4692
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4163
4693
  if (this.isEmptyResponse(responseData)) {
@@ -4172,6 +4702,10 @@ class ProxyServer {
4172
4702
  ? (0, compact_1.stripClaudeCompactResponseContent)(converted)
4173
4703
  : converted;
4174
4704
  usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
4705
+ // 回写 model 字段:将上游返回的 model 改写为客户端请求时的原始模型名
4706
+ const originalModel = (_f = req.body) === null || _f === void 0 ? void 0 : _f.model;
4707
+ (0, model_rewrite_transform_1.rewriteResponseModel)(normalizedConverted, originalModel);
4708
+ (0, model_rewrite_transform_1.rewriteResponseModel)(responseData, originalModel);
4175
4709
  this.copyResponseHeaders(responseHeaders, res);
4176
4710
  if (normalizedConverted && normalizedConverted !== responseData) {
4177
4711
  responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);