aicodeswitch 5.1.3 → 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.
- package/README.md +1 -0
- package/bin/restore.js +14 -7
- package/bin/utils/managed-fields.js +62 -0
- package/dist/server/access-keys/index.js +173 -0
- package/dist/server/access-keys/key-logger.js +358 -0
- package/dist/server/access-keys/key-resolver.js +51 -0
- package/dist/server/access-keys/key-session-tracker.js +217 -0
- package/dist/server/access-keys/manager.js +206 -0
- package/dist/server/access-keys/policy-manager.js +144 -0
- package/dist/server/access-keys/quota-checker.js +197 -0
- package/dist/server/access-keys/usage-tracker.js +279 -0
- package/dist/server/auth.js +16 -4
- package/dist/server/config-managed-fields.js +2 -0
- package/dist/server/fs-database.js +14 -1
- package/dist/server/main.js +1011 -13
- package/dist/server/proxy-server.js +605 -134
- package/dist/server/rules-status-service.js +16 -3
- package/dist/server/transformers/model-rewrite-transform.js +128 -0
- package/dist/ui/assets/index-Cws89pD2.js +828 -0
- package/dist/ui/assets/index-CzfKxImD.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-CMoQtBmK.css +0 -1
- package/dist/ui/assets/index-CXdNTFiX.js +0 -532
|
@@ -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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!
|
|
304
|
-
res.status(401).json({ error: {
|
|
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
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
//
|
|
419
|
+
// 确定路由来源
|
|
321
420
|
const allRoutes = this.dbManager.getRoutes();
|
|
322
|
-
let route
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
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
|
-
|
|
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 = ((
|
|
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
|
-
((
|
|
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: (
|
|
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) || ((
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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 = ((
|
|
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
|
-
((
|
|
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: (
|
|
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) || ((
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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 || ((
|
|
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: (
|
|
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 || ((
|
|
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: (
|
|
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 = (
|
|
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 || ((
|
|
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: (
|
|
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
|
-
((
|
|
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 || ((
|
|
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: (
|
|
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 (
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
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:
|
|
4239
|
+
statusCode: 511,
|
|
3876
4240
|
responseTime: Date.now() - requestStartAt,
|
|
3877
|
-
error: '
|
|
4241
|
+
error: 'Authentication required',
|
|
3878
4242
|
tags: this.buildRelayTags(false),
|
|
3879
4243
|
});
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
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 = ((
|
|
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
|
-
((
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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);
|