aicodeswitch 5.1.3 → 5.2.1

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.
@@ -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");
@@ -61,6 +62,7 @@ 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");
63
64
  const coding_plan_headers_1 = require("./coding-plan-headers");
65
+ const auth_1 = require("./auth");
64
66
  const SUPPORTED_TARGETS = ['claude-code', 'codex'];
65
67
  /** 默认模型列表 */
66
68
  const DEFAULT_MODELS = [
@@ -210,6 +212,12 @@ class ProxyServer {
210
212
  writable: true,
211
213
  value: void 0
212
214
  });
215
+ Object.defineProperty(this, "accessKeyModule", {
216
+ enumerable: true,
217
+ configurable: true,
218
+ writable: true,
219
+ value: null
220
+ });
213
221
  // 请求去重缓存:用于防止同一个请求被重复计数(如网络重试)
214
222
  // key: requestHash, value: timestamp
215
223
  Object.defineProperty(this, "requestDedupeCache", {
@@ -236,6 +244,73 @@ class ProxyServer {
236
244
  this.config = dbManager.getConfig();
237
245
  this.app = app;
238
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
+ }
239
314
  inferTargetTypeFromPath(path) {
240
315
  if (path === '/claude-code' || path.startsWith('/claude-code/')) {
241
316
  return 'claude-code';
@@ -297,63 +372,161 @@ class ProxyServer {
297
372
  // /v1/models: 直接返回静态模型列表
298
373
  if (apiPath === '/v1/models') {
299
374
  // 鉴权
300
- if (this.config.apiKey) {
301
- const authHeader = req.headers.authorization;
302
- const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
303
- if (!providedKey || providedKey !== this.config.apiKey) {
304
- 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 功能未启用' } });
305
380
  return;
306
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;
307
394
  }
308
395
  res.json(buildModelsResponse(this.dbManager.getApiPathModels()));
309
396
  return;
310
397
  }
311
398
  // 其余 4 个路径:查找绑定
312
- const bindings = this.dbManager.getApiPathBindings();
313
- const binding = bindings.find((b) => b.apiPath === apiPath);
314
- if (!binding || !binding.routeId) {
315
- 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');
316
415
  return;
317
416
  }
318
417
  // 推断客户端格式
319
418
  const clientFormat = apiPathToClientFormat(apiPath);
320
- // 加载绑定的路由(默认从 API 路径绑定获取)
419
+ // 确定路由来源
321
420
  const allRoutes = this.dbManager.getRoutes();
322
- let route = allRoutes.find((r) => r.id === binding.routeId);
323
- // 会话级路由覆盖:优先检查会话是否绑定了特定路由
324
- const sessionId = this.extractSessionIdForFormat(req, clientFormat);
325
- if (sessionId) {
326
- const session = this.dbManager.getSession(sessionId);
327
- if (session === null || session === void 0 ? void 0 : session.routeId) {
328
- const boundRoute = allRoutes.find((r) => r.id === session.routeId);
329
- if (boundRoute) {
330
- console.log(`[SESSION-ROUTE] API path ${apiPath} session ${sessionId} using bound route: ${boundRoute.name}`);
331
- route = boundRoute;
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;
332
431
  }
333
- else {
334
- console.log(`[SESSION-ROUTE] Bound route ${session.routeId} not found for session ${sessionId}, clearing binding`);
335
- this.dbManager.unbindSessionRoute(sessionId).catch(console.error);
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
+ }
336
469
  }
337
470
  }
338
471
  }
339
472
  if (!route) {
340
- 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.` } });
341
474
  }
342
475
  // 复用完整的代理请求处理
343
- yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath);
476
+ yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath, accessKeyCtx);
344
477
  }));
345
478
  // Dynamic proxy middleware (原有的 /claude-code, /codex 逻辑)
346
479
  this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
347
- var _a, _b, _c, _d, _e;
480
+ var _a, _b, _c, _d, _e, _f;
348
481
  // 仅处理支持的目标路径
349
482
  if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
350
483
  return next();
351
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
+ }
352
510
  const requestStartAt = Date.now();
353
511
  let hasRelayAttempt = false;
354
512
  try {
355
513
  const pathTargetType = this.inferTargetTypeFromPath(req.path);
356
- 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
+ }
357
530
  if (!route) {
358
531
  // 没有找到激活的路由,尝试使用原始配置
359
532
  const fallbackResult = yield this.handleFallbackToOriginalConfig(req, res, requestStartAt);
@@ -372,7 +545,7 @@ class ProxyServer {
372
545
  }
373
546
  // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
374
547
  const forcedContentType = yield this.prepareHighIqRouting(req, route, this.inferTargetTypeFromPath(req.path) || 'claude-code');
375
- 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
376
549
  if (!enableFailover) {
377
550
  // 故障切换已禁用,使用传统的单一规则匹配
378
551
  const rule = yield this.findMatchingRule(route.id, req, forcedContentType);
@@ -455,7 +628,7 @@ class ProxyServer {
455
628
  lastFailedService = service;
456
629
  // 检测是否是 timeout 错误
457
630
  const isTimeout = error.code === 'ECONNABORTED' ||
458
- ((_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')) ||
459
632
  (error.errno && error.errno === 'ETIMEDOUT');
460
633
  // 判断错误类型并加入黑名单
461
634
  if (isTimeout) {
@@ -527,13 +700,13 @@ class ProxyServer {
527
700
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
528
701
  // 添加请求详情
529
702
  targetType,
530
- 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,
531
704
  responseTime: Date.now() - requestStartAt,
532
705
  // 添加最后失败的服务信息
533
706
  ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
534
707
  targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
535
708
  targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
536
- 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),
537
710
  vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
538
711
  vendorName: _lastFailedVendor === null || _lastFailedVendor === void 0 ? void 0 : _lastFailedVendor.name,
539
712
  });
@@ -561,29 +734,38 @@ class ProxyServer {
561
734
  }
562
735
  catch (error) {
563
736
  console.error('Proxy error:', error);
564
- yield this.logToolRequest(req, {
565
- statusCode: 500,
566
- responseTime: Date.now() - requestStartAt,
567
- targetType: this.inferTargetTypeFromPath(req.path),
568
- error: error.message,
569
- tags: this.buildRelayTags(hasRelayAttempt),
570
- });
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
+ }
571
752
  // Add error log - 包含请求详情
572
- const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
573
- yield this.dbManager.addErrorLog({
574
- timestamp: Date.now(),
575
- method: req.method,
576
- path: req.path,
577
- statusCode: 500,
578
- errorMessage: error.message,
579
- errorStack: error.stack,
580
- requestHeaders: this.normalizeHeaders(req.headers),
581
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
582
- // 添加请求详情
583
- targetType,
584
- requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
585
- responseTime: Date.now() - requestStartAt,
586
- });
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
+ }
587
769
  // 根据路径判断目标类型并返回适当的错误格式
588
770
  const isClaudeCode = req.path.startsWith('/claude-code/');
589
771
  if (this.isResponseCommitted(res)) {
@@ -614,26 +796,72 @@ class ProxyServer {
614
796
  }
615
797
  createFixedRouteHandler(targetType) {
616
798
  return (req, res) => __awaiter(this, void 0, void 0, function* () {
617
- var _a, _b, _c, _d, _e;
799
+ var _a, _b, _c, _d, _e, _f;
618
800
  const requestStartAt = Date.now();
619
801
  let hasRelayAttempt = false;
802
+ let accessKeyCtx = null;
620
803
  try {
621
- // 检查API Key验证
622
- if (this.config.apiKey) {
623
- const authHeader = req.headers.authorization;
624
- const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
625
- if (!providedKey || providedKey !== this.config.apiKey) {
626
- yield this.logToolRequest(req, {
627
- statusCode: 401,
628
- responseTime: Date.now() - requestStartAt,
629
- targetType,
630
- error: 'Invalid API key',
631
- tags: this.buildRelayTags(false),
632
- });
633
- 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;
634
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);
635
864
  }
636
- const route = this.findRouteByTargetType(targetType);
637
865
  if (!route) {
638
866
  yield this.logToolRequest(req, {
639
867
  statusCode: 404,
@@ -647,7 +875,7 @@ class ProxyServer {
647
875
  // 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
648
876
  const forcedContentType = yield this.prepareHighIqRouting(req, route, targetType);
649
877
  // 检查是否启用故障切换
650
- 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
651
879
  if (!enableFailover) {
652
880
  // 故障切换已禁用,使用传统的单一规则匹配
653
881
  const rule = yield this.findMatchingRule(route.id, req, forcedContentType);
@@ -730,7 +958,7 @@ class ProxyServer {
730
958
  lastFailedService = service;
731
959
  // 检测是否是 timeout 错误
732
960
  const isTimeout = error.code === 'ECONNABORTED' ||
733
- ((_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')) ||
734
962
  (error.errno && error.errno === 'ETIMEDOUT');
735
963
  // 判断错误类型并加入黑名单
736
964
  if (isTimeout) {
@@ -800,13 +1028,13 @@ class ProxyServer {
800
1028
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
801
1029
  // 添加请求详情
802
1030
  targetType,
803
- 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,
804
1032
  responseTime: Date.now() - requestStartAt,
805
1033
  // 添加最后失败的服务信息
806
1034
  ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
807
1035
  targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
808
1036
  targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
809
- 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),
810
1038
  vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
811
1039
  vendorName: _lastFailedVendor2 === null || _lastFailedVendor2 === void 0 ? void 0 : _lastFailedVendor2.name,
812
1040
  });
@@ -834,28 +1062,35 @@ class ProxyServer {
834
1062
  }
835
1063
  catch (error) {
836
1064
  console.error(`Fixed route error for ${targetType}:`, error);
837
- yield this.logToolRequest(req, {
838
- statusCode: 500,
839
- responseTime: Date.now() - requestStartAt,
840
- targetType,
841
- error: error.message,
842
- tags: this.buildRelayTags(hasRelayAttempt),
843
- });
844
- // Add error log - 包含请求详情(使用函数参数 targetType)
845
- yield this.dbManager.addErrorLog({
846
- timestamp: Date.now(),
847
- method: req.method,
848
- path: req.path,
849
- statusCode: 500,
850
- errorMessage: error.message,
851
- errorStack: error.stack,
852
- requestHeaders: this.normalizeHeaders(req.headers),
853
- requestBody: req.body ? JSON.stringify(req.body) : undefined,
854
- // 添加请求详情
855
- targetType,
856
- requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
857
- responseTime: Date.now() - requestStartAt,
858
- });
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
+ }
859
1094
  if (this.isResponseCommitted(res)) {
860
1095
  return;
861
1096
  }
@@ -1483,7 +1718,9 @@ class ProxyServer {
1483
1718
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
1484
1719
  const candidates = [];
1485
1720
  const contentType = forcedContentType || this.determineContentType(req, this.inferTargetTypeFromPath(req.path) || 'claude-code', routeId);
1486
- const prioritizeContentType = contentType === 'high-iq';
1721
+ // 所有特定内容类型(compact, thinking, long-context 等)优先于 model-mapping,
1722
+ // 保持与 findMatchingRule 中的优先级顺序一致
1723
+ const prioritizeContentType = contentType !== 'default';
1487
1724
  const modelMappingRules = requestModel
1488
1725
  ? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
1489
1726
  rule.replacedModel &&
@@ -1628,6 +1865,9 @@ class ProxyServer {
1628
1865
  const sessionId = this.defaultExtractSessionId(req, targetType);
1629
1866
  for (const detector of this.getContentTypeDetectors()) {
1630
1867
  if (detector.match(req, body, sessionId, routeId)) {
1868
+ if (detector.type === 'compact') {
1869
+ console.log('[CONTENT-TYPE] Detected compact request');
1870
+ }
1631
1871
  return detector.type;
1632
1872
  }
1633
1873
  }
@@ -2422,9 +2662,9 @@ class ProxyServer {
2422
2662
  headers[key] = value.join(', ');
2423
2663
  }
2424
2664
  }
2425
- // 确定认证方式:优先使用服务配置的 authType
2665
+ // 确定认证方式:优先使用服务配置的 authType,若继承供应商则使用供应商的 authType
2426
2666
  // 注意:向下兼容 'auto' 字符串值(前端已移除 AuthType.AUTO 枚举,但旧数据可能包含此值)
2427
- const authType = service.authType || types_1.AuthType.AUTH_TOKEN;
2667
+ const authType = this.resolveEffectiveAuthType(service);
2428
2668
  // 向下兼容:检测旧数据的 'auto' 值
2429
2669
  // TODO: 删除
2430
2670
  const isAuto = authType === 'auto';
@@ -2492,6 +2732,17 @@ class ProxyServer {
2492
2732
  }
2493
2733
  return vendor.apiBaseUrl;
2494
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
+ }
2495
2746
  copyResponseHeaders(responseHeaders, res) {
2496
2747
  Object.keys(responseHeaders).forEach((key) => {
2497
2748
  if (!['content-encoding', 'transfer-encoding', 'connection', 'content-length'].includes(key.toLowerCase())) {
@@ -2942,7 +3193,7 @@ class ProxyServer {
2942
3193
  }
2943
3194
  proxyRequest(req, res, route, rule, service, options) {
2944
3195
  return __awaiter(this, void 0, void 0, function* () {
2945
- 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;
2946
3197
  res.locals.skipLog = true;
2947
3198
  const startTime = Date.now();
2948
3199
  const rawSourceType = service.sourceType || 'openai-chat';
@@ -3118,7 +3369,7 @@ class ProxyServer {
3118
3369
  // 标记规则正在使用
3119
3370
  rules_status_service_1.rulesStatusBroadcaster.markRuleInUse(route.id, rule.id);
3120
3371
  const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
3121
- var _a, _b;
3372
+ var _a, _b, _c, _d, _e, _f, _g;
3122
3373
  if (logged)
3123
3374
  return;
3124
3375
  const isError = statusCode >= 400;
@@ -3134,11 +3385,109 @@ class ProxyServer {
3134
3385
  return;
3135
3386
  }
3136
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
+ }
3137
3486
  // 供应商信息已在函数顶部获取
3138
3487
  const vendors = this.dbManager.getVendors();
3139
3488
  const vendorForLog = vendors.find(v => v.id === service.vendorId);
3140
3489
  // 从请求体中提取模型信息
3141
- 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;
3142
3491
  const tagsForLog = this.buildRelayTags(relayedForLog, useOriginalConfig);
3143
3492
  if (extraTagsForLog.length > 0) {
3144
3493
  tagsForLog.push(...extraTagsForLog);
@@ -3477,6 +3826,9 @@ class ProxyServer {
3477
3826
  const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
3478
3827
  ? new ClaudeCompactResponseSanitizer()
3479
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;
3480
3832
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
3481
3833
  // 使用 transformSSEToTool 方法选择转换器
3482
3834
  const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
@@ -3516,7 +3868,11 @@ class ProxyServer {
3516
3868
  if (compactResponseSanitizer) {
3517
3869
  streamStages.push(compactResponseSanitizer);
3518
3870
  }
3519
- streamStages.push(serializer, downstreamChunkCollector, res);
3871
+ streamStages.push(serializer);
3872
+ if (modelRewriter) {
3873
+ streamStages.push(modelRewriter);
3874
+ }
3875
+ streamStages.push(downstreamChunkCollector, res);
3520
3876
  (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], streamStages[3], ...streamStages.slice(4), (error) => {
3521
3877
  if (error) {
3522
3878
  reject(error);
@@ -3530,7 +3886,11 @@ class ProxyServer {
3530
3886
  if (compactResponseSanitizer) {
3531
3887
  streamStages.push(compactResponseSanitizer);
3532
3888
  }
3533
- streamStages.push(serializer, downstreamChunkCollector, res);
3889
+ streamStages.push(serializer);
3890
+ if (modelRewriter) {
3891
+ streamStages.push(modelRewriter);
3892
+ }
3893
+ streamStages.push(downstreamChunkCollector, res);
3534
3894
  (0, stream_1.pipeline)(streamStages[0], streamStages[1], streamStages[2], ...streamStages.slice(3), (error) => {
3535
3895
  if (error) {
3536
3896
  reject(error);
@@ -3567,10 +3927,10 @@ class ProxyServer {
3567
3927
  targetType,
3568
3928
  targetServiceId: service.id,
3569
3929
  targetServiceName: service.name,
3570
- 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),
3571
3931
  vendorId: service.vendorId,
3572
3932
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3573
- 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,
3574
3934
  responseTime: Date.now() - startTime,
3575
3935
  });
3576
3936
  }
@@ -3614,10 +3974,10 @@ class ProxyServer {
3614
3974
  targetType,
3615
3975
  targetServiceId: service.id,
3616
3976
  targetServiceName: service.name,
3617
- 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),
3618
3978
  vendorId: service.vendorId,
3619
3979
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3620
- 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,
3621
3981
  responseTime: Date.now() - startTime,
3622
3982
  });
3623
3983
  }
@@ -3636,7 +3996,7 @@ class ProxyServer {
3636
3996
  let responseData = response.data;
3637
3997
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
3638
3998
  const raw = yield this.readStreamBody(response.data);
3639
- responseData = (_h = this.safeJsonParse(raw)) !== null && _h !== void 0 ? _h : raw;
3999
+ responseData = (_j = this.safeJsonParse(raw)) !== null && _j !== void 0 ? _j : raw;
3640
4000
  }
3641
4001
  // 收集响应头
3642
4002
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
@@ -3657,6 +4017,10 @@ class ProxyServer {
3657
4017
  // 提取 token usage(从原始响应数据中提取)
3658
4018
  usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
3659
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);
3660
4024
  this.copyResponseHeaders(responseHeaders, res);
3661
4025
  if (normalizedConverted && normalizedConverted !== responseData) {
3662
4026
  // 非流式:responseBody 记录上游原始响应,downstreamResponseBody 记录转换后下发内容
@@ -3719,10 +4083,10 @@ class ProxyServer {
3719
4083
  targetType,
3720
4084
  targetServiceId: service.id,
3721
4085
  targetServiceName: service.name,
3722
- 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),
3723
4087
  vendorId: service.vendorId,
3724
4088
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3725
- 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,
3726
4090
  upstreamRequest: upstreamRequestForLog,
3727
4091
  responseTime: Date.now() - startTime,
3728
4092
  });
@@ -3737,7 +4101,7 @@ class ProxyServer {
3737
4101
  console.error('Proxy error:', error);
3738
4102
  // 检测是否是 timeout 错误
3739
4103
  const isTimeout = error.code === 'ECONNABORTED' ||
3740
- ((_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')) ||
3741
4105
  (error.errno && error.errno === 'ETIMEDOUT');
3742
4106
  const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
3743
4107
  const baseErrorMessage = isTimeout
@@ -3763,10 +4127,10 @@ class ProxyServer {
3763
4127
  targetType,
3764
4128
  targetServiceId: service.id,
3765
4129
  targetServiceName: service.name,
3766
- 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),
3767
4131
  vendorId: service.vendorId,
3768
4132
  vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3769
- 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,
3770
4134
  upstreamRequest: upstreamRequestForLog,
3771
4135
  responseHeaders: responseHeadersForLog,
3772
4136
  responseTime: Date.now() - startTime,
@@ -3862,32 +4226,39 @@ class ProxyServer {
3862
4226
  * 处理通过标准 API 路径(/v1/messages, /v1/responses 等)进入的代理请求。
3863
4227
  * 与原有 proxyRequest 逻辑独立,复用规则匹配、故障切换等机制。
3864
4228
  */
3865
- handleApiPathProxyRequest(req, res, route, clientFormat, apiPath) {
4229
+ handleApiPathProxyRequest(req, res, route, clientFormat, apiPath, accessKeyCtx) {
3866
4230
  return __awaiter(this, void 0, void 0, function* () {
3867
- var _a, _b;
4231
+ var _a, _b, _c;
3868
4232
  const requestStartAt = Date.now();
3869
- // 鉴权
3870
- if (this.config.apiKey) {
3871
- const authHeader = req.headers.authorization;
3872
- const providedKey = authHeader === null || authHeader === void 0 ? void 0 : authHeader.replace('Bearer ', '');
3873
- 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})`);
3874
4238
  yield this.logToolRequest(req, {
3875
- statusCode: 401,
4239
+ statusCode: 511,
3876
4240
  responseTime: Date.now() - requestStartAt,
3877
- error: 'Invalid API key',
4241
+ error: 'Authentication required',
3878
4242
  tags: this.buildRelayTags(false),
3879
4243
  });
3880
- // 根据客户端格式返回错误
3881
- if (clientFormat === 'claude') {
3882
- res.status(401).json({ type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } });
3883
- }
3884
- else {
3885
- res.status(401).json({ error: { message: 'Invalid API key' } });
3886
- }
4244
+ this.sendAuthError(res, clientFormat === 'claude');
4245
+ return;
4246
+ }
4247
+ }
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');
3887
4254
  return;
3888
4255
  }
4256
+ // 并发 +1
4257
+ this.accessKeyModule.quotaChecker.onRequestStart(accessKeyCtx.accessKey.id, accessKeyCtx.policy);
4258
+ // 注入上下文到 req 对象,供 proxyRequestForApiPath 内部的 finalizeLog 使用
4259
+ req._accessKeyCtx = accessKeyCtx;
3889
4260
  }
3890
- const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false;
4261
+ const enableFailover = ((_b = this.config) === null || _b === void 0 ? void 0 : _b.enableFailover) !== false;
3891
4262
  if (!enableFailover) {
3892
4263
  const rule = yield this.findMatchingRule(route.id, req);
3893
4264
  if (!rule) {
@@ -3938,7 +4309,7 @@ class ProxyServer {
3938
4309
  lastFailedRule = rule;
3939
4310
  lastFailedService = service;
3940
4311
  const isTimeout = error.code === 'ECONNABORTED' ||
3941
- ((_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')) ||
3942
4313
  error.errno === 'ETIMEDOUT';
3943
4314
  if (isTimeout) {
3944
4315
  yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout', undefined, 'timeout');
@@ -3981,7 +4352,7 @@ class ProxyServer {
3981
4352
  */
3982
4353
  proxyRequestForApiPath(req, res, route, rule, service, clientFormat, apiPath, options) {
3983
4354
  return __awaiter(this, void 0, void 0, function* () {
3984
- var _a, _b, _c, _d;
4355
+ var _a, _b, _c, _d, _e, _f;
3985
4356
  const startTime = Date.now();
3986
4357
  const rawSourceType = service.sourceType || 'openai-chat';
3987
4358
  const sourceType = (0, type_migration_1.normalizeSourceType)(rawSourceType);
@@ -4020,9 +4391,93 @@ class ProxyServer {
4020
4391
  requestBody = (0, compact_1.normalizeClaudeCompactRequestBody)(requestBody);
4021
4392
  }
4022
4393
  const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
4394
+ var _a, _b, _c, _d, _e;
4023
4395
  if (logged)
4024
4396
  return;
4025
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
+ }
4026
4481
  yield this.logToolRequest(req, {
4027
4482
  statusCode,
4028
4483
  responseTime: Date.now() - startTime,
@@ -4158,6 +4613,9 @@ class ProxyServer {
4158
4613
  rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
4159
4614
  });
4160
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;
4161
4619
  const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
4162
4620
  this.copyResponseHeaders(responseHeaders, res);
4163
4621
  res.status(response.status);
@@ -4180,8 +4638,16 @@ class ProxyServer {
4180
4638
  };
4181
4639
  try {
4182
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
+ };
4183
4648
  if (converter) {
4184
- (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) => {
4185
4651
  if (error) {
4186
4652
  reject(error);
4187
4653
  return;
@@ -4190,7 +4656,8 @@ class ProxyServer {
4190
4656
  });
4191
4657
  }
4192
4658
  else {
4193
- (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) => {
4194
4661
  if (error) {
4195
4662
  reject(error);
4196
4663
  return;
@@ -4220,7 +4687,7 @@ class ProxyServer {
4220
4687
  let responseData = response.data;
4221
4688
  if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
4222
4689
  const raw = yield this.readStreamBody(response.data);
4223
- responseData = (_d = this.safeJsonParse(raw)) !== null && _d !== void 0 ? _d : raw;
4690
+ responseData = (_e = this.safeJsonParse(raw)) !== null && _e !== void 0 ? _e : raw;
4224
4691
  }
4225
4692
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4226
4693
  if (this.isEmptyResponse(responseData)) {
@@ -4235,6 +4702,10 @@ class ProxyServer {
4235
4702
  ? (0, compact_1.stripClaudeCompactResponseContent)(converted)
4236
4703
  : converted;
4237
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);
4238
4709
  this.copyResponseHeaders(responseHeaders, res);
4239
4710
  if (normalizedConverted && normalizedConverted !== responseData) {
4240
4711
  responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);