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