aicodeswitch 5.2.13 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2323,6 +2323,8 @@ class FileSystemDatabaseManager {
2323
2323
  lastRequestAt: now,
2324
2324
  requestCount: 1,
2325
2325
  totalTokens: 0,
2326
+ inputTokens: 0,
2327
+ outputTokens: 0,
2326
2328
  };
2327
2329
  this.sessions.push(session);
2328
2330
  yield this.saveSessions();
@@ -2478,6 +2480,8 @@ class FileSystemDatabaseManager {
2478
2480
  existing.lastRequestAt = now;
2479
2481
  existing.requestCount++;
2480
2482
  existing.totalTokens += session.totalTokens || 0;
2483
+ existing.inputTokens = (existing.inputTokens || 0) + (session.inputTokens || 0);
2484
+ existing.outputTokens = (existing.outputTokens || 0) + (session.outputTokens || 0);
2481
2485
  if (session.vendorId !== undefined)
2482
2486
  existing.vendorId = session.vendorId;
2483
2487
  if (session.vendorName !== undefined)
@@ -2510,6 +2514,8 @@ class FileSystemDatabaseManager {
2510
2514
  lastRequestAt: now,
2511
2515
  requestCount: 1,
2512
2516
  totalTokens: session.totalTokens || 0,
2517
+ inputTokens: session.inputTokens || 0,
2518
+ outputTokens: session.outputTokens || 0,
2513
2519
  vendorId: session.vendorId,
2514
2520
  vendorName: session.vendorName,
2515
2521
  serviceId: session.serviceId,
@@ -32,8 +32,6 @@ const os_1 = __importDefault(require("os"));
32
32
  const auth_1 = require("./auth");
33
33
  const version_check_1 = require("./version-check");
34
34
  const utils_1 = require("./utils");
35
- const tools_service_1 = require("./tools-service");
36
- const websocket_service_1 = require("./websocket-service");
37
35
  const rules_status_service_1 = require("./rules-status-service");
38
36
  const type_migration_1 = require("./type-migration");
39
37
  const config_metadata_1 = require("./config-metadata");
@@ -2514,19 +2512,6 @@ ${instruction}
2514
2512
  res.json({ success: false });
2515
2513
  }
2516
2514
  })));
2517
- // 工具安装检测相关路由
2518
- app.get('/api/tools/status', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
2519
- console.log('[API] GET /api/tools/status - 获取工具安装状态');
2520
- try {
2521
- const status = yield (0, tools_service_1.getToolsInstallationStatus)();
2522
- console.log('[API] 工具安装状态:', status);
2523
- res.json(status);
2524
- }
2525
- catch (error) {
2526
- console.error('[API] 获取工具状态失败:', error);
2527
- res.status(500).json({ error: '获取工具状态失败' });
2528
- }
2529
- })));
2530
2515
  // MCP 工具管理相关路由
2531
2516
  app.get('/api/mcps', (_req, res) => {
2532
2517
  res.json(dbManager.getMCPs());
@@ -3563,26 +3548,12 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3563
3548
  }
3564
3549
  setImmediate(() => process.exit(1));
3565
3550
  });
3566
- // 创建 WebSocket 服务器用于工具安装
3567
- const toolInstallWss = (0, websocket_service_1.createToolInstallationWSServer)();
3568
3551
  // 设置黑名单检查函数,用于在规则状态同步时检查黑名单是否已过期
3569
3552
  rules_status_service_1.rulesStatusBroadcaster.setBlacklistChecker((serviceId, routeId, contentType) => __awaiter(void 0, void 0, void 0, function* () {
3570
3553
  // 检查服务��否在黑名单中
3571
3554
  const isBlacklisted = yield dbManager.isServiceBlacklisted(serviceId, routeId, contentType);
3572
3555
  return isBlacklisted;
3573
3556
  }));
3574
- // 将 WebSocket 服务器附加到 HTTP 服务器(仅用于工具安装)
3575
- server.on('upgrade', (request, socket, head) => {
3576
- if (request.url === '/api/tools/install') {
3577
- toolInstallWss.handleUpgrade(request, socket, head, (ws) => {
3578
- toolInstallWss.emit('connection', ws, request);
3579
- });
3580
- }
3581
- else {
3582
- socket.destroy();
3583
- }
3584
- });
3585
- console.log(`WebSocket server for tool installation attached to ws://${clientHost}:${port}/api/tools/install`);
3586
3557
  let isShuttingDown = false;
3587
3558
  let shutdownPromise = null;
3588
3559
  const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
@@ -3594,10 +3565,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3594
3565
  console.log(`[Server] Received ${signal}, shutting down...`);
3595
3566
  // 立即停止监听以释放端口(同步关闭监听句柄,端口马上可用),
3596
3567
  // 避免在漫长的清理流程期间端口仍被占用,导致此时重启产生 EADDRINUSE 冲突。
3597
- // 现有连接会继续处理直到完成或超时;其回调与下面的清理流程并行等待。
3598
3568
  const serverClosedPromise = new Promise((resolve) => {
3599
3569
  server.close(() => resolve());
3600
3570
  });
3571
+ // 强制断开所有现存连接(含浏览器长连的 SSE:agent-map stream、rules-status
3572
+ // 广播等)。否则这些连接永不自行关闭,server.close 的回调要等满下面 5s 超时
3573
+ // 才触发,表现为 Ctrl+C 后「卡很久才打印 Server stopped.」。closeAllConnections
3574
+ // 立即销毁所有 socket,让 close 回调几乎瞬时完成。
3575
+ if (typeof server.closeAllConnections === 'function') {
3576
+ server.closeAllConnections();
3577
+ }
3601
3578
  // 服务终止前恢复配置文件(适用于 aicos stop 与 Ctrl+C)
3602
3579
  try {
3603
3580
  const claudeRestored = yield restoreClaudeConfig();
@@ -68,8 +68,12 @@ class ServicePerformanceTracker {
68
68
  try {
69
69
  const raw = yield promises_1.default.readFile(this.filePath, 'utf-8');
70
70
  const parsed = JSON.parse(raw);
71
- if (parsed && parsed.vendors)
71
+ if (parsed && parsed.vendors) {
72
72
  this.file = parsed;
73
+ // 旧数据文件可能缺新增的 token 字段(sumInputTokens/sumTotalTokens),
74
+ // 直接累加会得到 undefined+N=NaN,污染派生值。这里补齐为 0。
75
+ this.migrateBuckets(this.file);
76
+ }
73
77
  }
74
78
  catch (_a) {
75
79
  // 首次启动或文件损坏,使用空桶
@@ -77,6 +81,42 @@ class ServicePerformanceTracker {
77
81
  }
78
82
  });
79
83
  }
84
+ /** 加载后补齐旧数据缺失的桶字段,避免 undefined 累加成 NaN */
85
+ migrateBuckets(file) {
86
+ for (const v of Object.values(file.vendors)) {
87
+ if (v.vendorRollup)
88
+ this.normalizeAggregate(v.vendorRollup);
89
+ for (const s of Object.values(v.services || {})) {
90
+ if (s.serviceRollup)
91
+ this.normalizeAggregate(s.serviceRollup);
92
+ for (const agg of Object.values(s.models || {})) {
93
+ this.normalizeAggregate(agg);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ normalizeAggregate(agg) {
99
+ this.normalizeBucket(agg.precise);
100
+ this.normalizeBucket(agg.estimated);
101
+ for (const b of Object.values(agg.hourly || {}))
102
+ this.normalizeBucket(b);
103
+ }
104
+ normalizeBucket(b) {
105
+ if (!b)
106
+ return;
107
+ if (typeof b.count !== 'number')
108
+ b.count = 0;
109
+ if (typeof b.sumTtftMs !== 'number')
110
+ b.sumTtftMs = 0;
111
+ if (typeof b.sumTps !== 'number')
112
+ b.sumTps = 0;
113
+ if (typeof b.totalOutputTokens !== 'number')
114
+ b.totalOutputTokens = 0;
115
+ if (typeof b.sumInputTokens !== 'number')
116
+ b.sumInputTokens = 0;
117
+ if (typeof b.sumTotalTokens !== 'number')
118
+ b.sumTotalTokens = 0;
119
+ }
80
120
  /**
81
121
  * 记录一次请求的性能数据点(三级同步聚合)。
82
122
  * 纯内存操作,不阻塞调用方。
@@ -257,7 +297,7 @@ class ServicePerformanceTracker {
257
297
  };
258
298
  }
259
299
  emptyBucket() {
260
- return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0 };
300
+ return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0, sumInputTokens: 0, sumTotalTokens: 0 };
261
301
  }
262
302
  accumulate(agg, m, hour, withExtremes) {
263
303
  var _a;
@@ -268,6 +308,7 @@ class ServicePerformanceTracker {
268
308
  const bucket = m.timingAccuracy === 'precise' ? agg.precise : agg.estimated;
269
309
  const hasTtft = m.timingAccuracy === 'precise' && typeof m.ttftMs === 'number';
270
310
  const hasTps = typeof m.tokensPerSecond === 'number';
311
+ // token 量与计时精度无关,precise/estimated 桶都累加
271
312
  bucket.count += 1;
272
313
  if (hasTtft)
273
314
  bucket.sumTtftMs += m.ttftMs;
@@ -275,18 +316,37 @@ class ServicePerformanceTracker {
275
316
  bucket.sumTps += m.tokensPerSecond;
276
317
  if (m.outputTokens)
277
318
  bucket.totalOutputTokens += m.outputTokens;
278
- // 小时桶(仅精确样本计入走势,避免估算样本污染)
319
+ if (m.inputTokens)
320
+ bucket.sumInputTokens += m.inputTokens;
321
+ if (m.totalTokens)
322
+ bucket.sumTotalTokens += m.totalTokens;
323
+ // 小时走势桶:
324
+ // - count/sumTtftMs/sumTps 仅精确样本计入(避免估算样本污染 TTFT/TPM 均值)
325
+ // - token 三项与精度无关,所有样本都计入走势(含非流式)
326
+ const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
327
+ let touchedHourly = false;
279
328
  if (m.timingAccuracy === 'precise') {
280
- const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
281
329
  hb.count += 1;
282
330
  if (hasTtft)
283
331
  hb.sumTtftMs += m.ttftMs;
284
332
  if (hasTps)
285
333
  hb.sumTps += m.tokensPerSecond;
286
- if (m.outputTokens)
287
- hb.totalOutputTokens += m.outputTokens;
288
- this.trimHourly(agg.hourly);
334
+ touchedHourly = true;
335
+ }
336
+ if (m.outputTokens) {
337
+ hb.totalOutputTokens += m.outputTokens;
338
+ touchedHourly = true;
339
+ }
340
+ if (m.inputTokens) {
341
+ hb.sumInputTokens += m.inputTokens;
342
+ touchedHourly = true;
289
343
  }
344
+ if (m.totalTokens) {
345
+ hb.sumTotalTokens += m.totalTokens;
346
+ touchedHourly = true;
347
+ }
348
+ if (touchedHourly)
349
+ this.trimHourly(agg.hourly);
290
350
  // 极值(仅模型级、仅精确样本)
291
351
  if (withExtremes && m.timingAccuracy === 'precise') {
292
352
  if (hasTtft) {
@@ -315,9 +375,14 @@ class ServicePerformanceTracker {
315
375
  // ---------------- 内部:派生 ----------------
316
376
  derive(agg) {
317
377
  const p = agg.precise;
378
+ const e = agg.estimated;
318
379
  const count = p.count;
319
380
  const avgTtftMs = count > 0 ? p.sumTtftMs / count : 0;
320
381
  const avgTps = count > 0 ? p.sumTps / count : 0;
382
+ // token 量跨 precise+estimated 求和(含非流式样本)
383
+ const totalOutputTokens = p.totalOutputTokens + e.totalOutputTokens;
384
+ const totalInputTokens = p.sumInputTokens + e.sumInputTokens;
385
+ const totalTokens = p.sumTotalTokens + e.sumTotalTokens;
321
386
  return {
322
387
  count,
323
388
  avgTtftMs,
@@ -327,7 +392,9 @@ class ServicePerformanceTracker {
327
392
  minTps: agg.minTps,
328
393
  maxTps: agg.maxTps,
329
394
  errorCount: agg.errorCount,
330
- totalOutputTokens: p.totalOutputTokens,
395
+ totalOutputTokens,
396
+ totalInputTokens,
397
+ totalTokens,
331
398
  successRate: count + agg.errorCount > 0 ? count / (count + agg.errorCount) : 0,
332
399
  };
333
400
  }
@@ -355,6 +422,9 @@ class ServicePerformanceTracker {
355
422
  count: b.count,
356
423
  avgTtftMs: b.count > 0 ? b.sumTtftMs / b.count : 0,
357
424
  avgTpm: b.count > 0 ? (b.sumTps / b.count) * 60 : 0,
425
+ inputTokens: b.sumInputTokens,
426
+ outputTokens: b.totalOutputTokens,
427
+ totalTokens: b.sumTotalTokens,
358
428
  }));
359
429
  }
360
430
  locateService(serviceId) {
@@ -67,6 +67,13 @@ const coding_plan_1 = require("./coding-plan");
67
67
  const coding_plan_headers_1 = require("./coding-plan-headers");
68
68
  const auth_1 = require("./auth");
69
69
  const SUPPORTED_TARGETS = ['claude-code', 'codex'];
70
+ /**
71
+ * Fallback(回退原始配置)路径的虚拟供应商归属。
72
+ * 该路径转发的请求不属于任何用户配置的供应商/服务,故用一组固定 ID + 名称
73
+ * 把它纳入服务性能统计(service-performance.json),便于在测速面板单独呈现。
74
+ */
75
+ const FALLBACK_VENDOR_ID = 'fallback-vendor';
76
+ const FALLBACK_VENDOR_NAME = '原始配置 / 直连';
70
77
  /** 默认模型列表 */
71
78
  const DEFAULT_MODELS = [
72
79
  { id: 'claude-sonnet-4-20250514', owned_by: 'anthropic' },
@@ -275,12 +282,16 @@ class ProxyServer {
275
282
  * 流式:依据 streamTiming 精确计算 TTFT 与生成阶段吞吐;非流式:端到端估算(estimated)。
276
283
  */
277
284
  emitPerformance(params) {
285
+ var _a;
278
286
  const tracker = this.performanceTracker;
279
287
  if (!tracker)
280
288
  return;
281
289
  const { statusCode, startTime, usage, streamTiming, service, vendorId, vendorName, model } = params;
282
290
  const isError = statusCode >= 400;
291
+ const inputTokens = usage === null || usage === void 0 ? void 0 : usage.inputTokens;
283
292
  const outputTokens = usage === null || usage === void 0 ? void 0 : usage.outputTokens;
293
+ const computedTotal = (inputTokens || 0) + (outputTokens || 0);
294
+ const totalTokens = (_a = usage === null || usage === void 0 ? void 0 : usage.totalTokens) !== null && _a !== void 0 ? _a : (computedTotal > 0 ? computedTotal : undefined);
284
295
  const responseMs = Date.now() - startTime;
285
296
  let ttftMs;
286
297
  let tokensPerSecond;
@@ -296,7 +307,11 @@ class ProxyServer {
296
307
  else if (outputTokens && responseMs > 0) {
297
308
  tokensPerSecond = outputTokens / (responseMs / 1000);
298
309
  }
299
- tracker.recordPerformance(vendorId !== null && vendorId !== void 0 ? vendorId : service.vendorId, vendorName, service.id, service.name, model, { ttftMs, tokensPerSecond, outputTokens, timingAccuracy, isError });
310
+ // 解析最终归属:Fallback 临时服务没有真实 vendor,service.vendorId 兜底为虚拟供应商;
311
+ // 此时 vendorName 也为空,补上虚拟供应商名,确保不被 recordPerformance 的三元组校验丢弃。
312
+ const effectiveVendorId = vendorId !== null && vendorId !== void 0 ? vendorId : service.vendorId;
313
+ const effectiveVendorName = vendorName !== null && vendorName !== void 0 ? vendorName : (effectiveVendorId === FALLBACK_VENDOR_ID ? FALLBACK_VENDOR_NAME : undefined);
314
+ tracker.recordPerformance(effectiveVendorId, effectiveVendorName, service.id, service.name, model, { ttftMs, tokensPerSecond, outputTokens, inputTokens, totalTokens, timingAccuracy, isError });
300
315
  }
301
316
  /**
302
317
  * 从请求中提取 API Key(支持三种 Header,按优先级依次尝试)
@@ -1260,6 +1275,7 @@ class ProxyServer {
1260
1275
  const tempService = {
1261
1276
  id: 'fallback-service',
1262
1277
  name: 'Original Config',
1278
+ vendorId: FALLBACK_VENDOR_ID,
1263
1279
  apiUrl: originalConfig.apiUrl,
1264
1280
  apiKey: originalConfig.apiKey,
1265
1281
  authType: originalConfig.authType,
@@ -3347,8 +3363,28 @@ class ProxyServer {
3347
3363
  });
3348
3364
  return { converter: adapter, extractUsage };
3349
3365
  }
3366
+ /**
3367
+ * 将 SSEEventCollector 归一化后的 usage({input_tokens, output_tokens, total_tokens, cache_read_input_tokens})
3368
+ * 映射为 TokenUsage。collector 已遍历全部事件并合并 Anthropic message_start/message_delta + OpenAI/Gemini 字段,
3369
+ * 因此这里无需再区分上游格式。
3370
+ */
3371
+ tokenUsageFromCollected(extracted) {
3372
+ var _a;
3373
+ if (!extracted)
3374
+ return undefined;
3375
+ const inputTokens = extracted.input_tokens || 0;
3376
+ const outputTokens = extracted.output_tokens || 0;
3377
+ const computedTotal = inputTokens + outputTokens;
3378
+ const totalTokens = (_a = extracted.total_tokens) !== null && _a !== void 0 ? _a : (computedTotal > 0 ? computedTotal : undefined);
3379
+ return {
3380
+ inputTokens,
3381
+ outputTokens,
3382
+ totalTokens,
3383
+ cacheReadInputTokens: extracted.cache_read_input_tokens || 0,
3384
+ };
3385
+ }
3350
3386
  extractTokenUsageFromResponse(responseData, sourceType) {
3351
- var _a, _b, _c, _d, _e, _f;
3387
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3352
3388
  if (!responseData)
3353
3389
  return undefined;
3354
3390
  const format = (0, source_type_mapping_1.sourceTypeToFormat)(sourceType);
@@ -3364,14 +3400,15 @@ class ProxyServer {
3364
3400
  };
3365
3401
  }
3366
3402
  if (format === 'completions' || format === 'responses') {
3367
- const usage = responseData === null || responseData === void 0 ? void 0 : responseData.usage;
3403
+ // 标准 Responses 非流式 usage 在顶层;个别上游/中转会嵌在 response.usage 下,做兜底
3404
+ const usage = (_a = responseData === null || responseData === void 0 ? void 0 : responseData.usage) !== null && _a !== void 0 ? _a : (_b = responseData === null || responseData === void 0 ? void 0 : responseData.response) === null || _b === void 0 ? void 0 : _b.usage;
3368
3405
  if (!usage)
3369
3406
  return undefined;
3370
3407
  return {
3371
3408
  inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) || 0,
3372
3409
  outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) || 0,
3373
3410
  totalTokens: (usage === null || usage === void 0 ? void 0 : usage.total_tokens) || 0,
3374
- cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cached_tokens) || (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
3411
+ cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cached_tokens) || (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || ((_c = usage === null || usage === void 0 ? void 0 : usage.input_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens) || 0,
3375
3412
  };
3376
3413
  }
3377
3414
  if (format === 'claude') {
@@ -3379,7 +3416,7 @@ class ProxyServer {
3379
3416
  return {
3380
3417
  inputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) || 0,
3381
3418
  outputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) || 0,
3382
- totalTokens: ((_a = responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) !== null && _a !== void 0 ? _a : 0) + ((_b = responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) !== null && _b !== void 0 ? _b : 0) || undefined,
3419
+ totalTokens: ((_d = responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) !== null && _d !== void 0 ? _d : 0) + ((_e = responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) !== null && _e !== void 0 ? _e : 0) || undefined,
3383
3420
  cacheReadInputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.cache_read_input_tokens) || 0,
3384
3421
  };
3385
3422
  }
@@ -3389,7 +3426,7 @@ class ProxyServer {
3389
3426
  return {
3390
3427
  inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
3391
3428
  outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
3392
- totalTokens: ((_c = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _c !== void 0 ? _c : 0) + ((_d = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _d !== void 0 ? _d : 0) || undefined,
3429
+ totalTokens: ((_f = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _f !== void 0 ? _f : 0) + ((_g = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _g !== void 0 ? _g : 0) || undefined,
3393
3430
  cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
3394
3431
  };
3395
3432
  }
@@ -3409,7 +3446,7 @@ class ProxyServer {
3409
3446
  return {
3410
3447
  inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
3411
3448
  outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
3412
- totalTokens: ((_e = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _e !== void 0 ? _e : 0) + ((_f = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _f !== void 0 ? _f : 0) || undefined,
3449
+ totalTokens: ((_h = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _h !== void 0 ? _h : 0) + ((_j = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _j !== void 0 ? _j : 0) || undefined,
3413
3450
  cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
3414
3451
  };
3415
3452
  }
@@ -3427,10 +3464,37 @@ class ProxyServer {
3427
3464
  const sessionId = this.defaultExtractSessionId(req, targetType) || '-';
3428
3465
  // Agent Map:在途请求注册(active 状态判定依据)
3429
3466
  const _accessKeyCtxAtStart = req._accessKeyCtx;
3467
+ // 后台类请求(count_tokens / compact / background 规则)不重置「本轮通知标记」,避免主任务结束后的
3468
+ // 续发请求重复触发结束通知
3469
+ const _isBgRequest = rule.contentType === 'background'
3470
+ || rule.contentType === 'compact'
3471
+ || this.isCountTokensPath(req.path)
3472
+ || this.isCountTokensPath(req.originalUrl);
3473
+ // 思考类请求:复用路由同款识别(rule.contentType==='thinking' 或请求体带 thinking/reasoning 信号)。
3474
+ // 思考期间用更宽的静默上限,避免思考静默被误判为「上游停滞」而弹通知。
3475
+ const _isThinking = rule.contentType === 'thinking' || this.hasThinkingSignal(req.body);
3476
+ res.locals._agentMapThinking = _isThinking;
3430
3477
  agent_map_1.agentMapService.startRequest(sessionId, targetType, {
3431
3478
  source: _accessKeyCtxAtStart ? 'access-key' : 'global',
3432
3479
  keyId: (_a = _accessKeyCtxAtStart === null || _accessKeyCtxAtStart === void 0 ? void 0 : _accessKeyCtxAtStart.accessKey) === null || _a === void 0 ? void 0 : _a.id,
3433
3480
  keyName: (_b = _accessKeyCtxAtStart === null || _accessKeyCtxAtStart === void 0 ? void 0 : _accessKeyCtxAtStart.accessKey) === null || _b === void 0 ? void 0 : _b.name,
3481
+ background: _isBgRequest,
3482
+ thinking: _isThinking,
3483
+ });
3484
+ // 安全网:无论走哪条分支(含编程套餐拒绝、配额拦截、异常等早退,导致 finalizeLog 未被调用),
3485
+ // 都在响应关闭时确保 endRequest 配对执行一次,杜绝在途计数泄漏(「永远卡在进行中」)。
3486
+ res.on('close', () => {
3487
+ // 仅保证 endRequest 配对执行一次(防在途计数泄漏)。用独立标记 _agentMapEnded,
3488
+ // 不再与 finalizeLog 的 onFinalized 共用标记 —— 否则 res 'close' 先于 finalizeLog 触发时
3489
+ // 会把标记置位,导致 finalizeLog 跳过 onFinalized(requestCount / model / token 永不更新)。
3490
+ if (res.locals._agentMapEnded)
3491
+ return;
3492
+ res.locals._agentMapEnded = true;
3493
+ agent_map_1.agentMapService.endRequest(sessionId, {
3494
+ isStream: !!res.locals._agentMapStream,
3495
+ thinking: !!res.locals._agentMapThinking,
3496
+ });
3497
+ agent_map_1.agentMapService.reevaluate(sessionId);
3434
3498
  });
3435
3499
  const vendor = this.dbManager.getVendorByServiceId(service.id);
3436
3500
  console.log(`\x1b[32m[Request Start]\x1b[0m client=${targetType}, session=${sessionId}, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}, model=${rule.targetModel || ((_c = req.body) === null || _c === void 0 ? void 0 : _c.model) || '-'}`);
@@ -3611,10 +3675,19 @@ class ProxyServer {
3611
3675
  service, vendorId: vendor === null || vendor === void 0 ? void 0 : vendor.id, vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
3612
3676
  model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
3613
3677
  });
3614
- // Agent Map:在途请求注销 + 活动/状态采集广播(独立于 enableLogging,仅执行一次)
3615
- if (!res.locals._agentMapRecorded) {
3616
- res.locals._agentMapRecorded = true;
3617
- agent_map_1.agentMapService.endRequest(sessionId);
3678
+ // Agent Map:在途请求注销 + 活动/状态采集广播(独立于 enableLogging
3679
+ // 注意:endRequest 与 onFinalized 用各自独立的标记,二者解耦。
3680
+ // 这样即便 res 'close' 安全网已先执行 endRequest(_agentMapEnded=true),onFinalized 仍会在此执行 ——
3681
+ // 修复「requestCount / model / token 不更新」:旧实现二者共用 _agentMapRecorded,'close' 抢先置位会让此处整块跳过。
3682
+ if (!res.locals._agentMapEnded) {
3683
+ res.locals._agentMapEnded = true;
3684
+ agent_map_1.agentMapService.endRequest(sessionId, {
3685
+ isStream: !!res.locals._agentMapStream,
3686
+ thinking: !!res.locals._agentMapThinking,
3687
+ });
3688
+ }
3689
+ if (!res.locals._agentMapFinalized) {
3690
+ res.locals._agentMapFinalized = true;
3618
3691
  const _akCtx = req._accessKeyCtx;
3619
3692
  const _tokensDelta = (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) ||
3620
3693
  (((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0));
@@ -3631,6 +3704,9 @@ class ProxyServer {
3631
3704
  // 避免规则未配置 targetModel 时回退成编程工具提交的 req.body.model
3632
3705
  model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
3633
3706
  tokensDelta: _tokensDelta,
3707
+ // 输入/输出拆分(3D 连线两段用);usageForLog 已规范化为驼峰字段
3708
+ inputTokensDelta: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0,
3709
+ outputTokensDelta: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0,
3634
3710
  body: req.body,
3635
3711
  downstreamResponseBody: downstreamResponseBodyForLog !== null && downstreamResponseBodyForLog !== void 0 ? downstreamResponseBodyForLog : responseBodyForLog,
3636
3712
  responseBody: responseBodyForLog,
@@ -3806,6 +3882,9 @@ class ProxyServer {
3806
3882
  serviceName: service.name,
3807
3883
  model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_o = req.body) === null || _o === void 0 ? void 0 : _o.model),
3808
3884
  totalTokens,
3885
+ // 输入/输出拆分持久化(与 totalTokens 同口径),供 Agent Map 重启后恢复
3886
+ inputTokens: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0,
3887
+ outputTokens: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0,
3809
3888
  highIqMode: rule.contentType === 'high-iq' ? true : existingSession === null || existingSession === void 0 ? void 0 : existingSession.highIqMode,
3810
3889
  highIqRuleId: rule.contentType === 'high-iq' ? rule.id : existingSession === null || existingSession === void 0 ? void 0 : existingSession.highIqRuleId,
3811
3890
  highIqEnabledAt: rule.contentType === 'high-iq'
@@ -4149,9 +4228,19 @@ class ProxyServer {
4149
4228
  const serializer = new streaming_1.SSESerializerTransform();
4150
4229
  const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
4151
4230
  rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
4231
+ // Agent Map 心跳:每个流经的下游 chunk 都视为「会话仍活跃」,刷新活动时钟(节流由 ChunkCollector 内部 5s 上限控制)
4232
+ // 同时把当前流式请求的实时累计 usage(input/output)一并推送,使前端节点在流式过程中随 token 增长实时上移
4233
+ const u = eventCollector.extractUsage();
4234
+ agent_map_1.agentMapService.heartbeat(sessionId, u ? {
4235
+ inputTokens: u.input_tokens || 0,
4236
+ outputTokens: u.output_tokens || 0,
4237
+ } : undefined);
4152
4238
  });
4153
4239
  // 服务性能打点:记录首/末 SSE 事件时间,用于 TTFT 与生成阶段吞吐
4154
4240
  streamTiming = new stream_timing_transform_1.StreamTimingTransform(startTime);
4241
+ // Agent Map:SSE 流已建立 —— 记一个在途流,状态机据此区分「同步/流式」活跃判定
4242
+ res.locals._agentMapStream = true;
4243
+ agent_map_1.agentMapService.markStreaming(sessionId);
4155
4244
  const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
4156
4245
  ? new ClaudeCompactResponseSanitizer()
4157
4246
  : null;
@@ -4160,7 +4249,7 @@ class ProxyServer {
4160
4249
  const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
4161
4250
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4162
4251
  // 使用 transformSSEToTool 方法选择转换器
4163
- const { converter, extractUsage } = this.transformSSEToTool(targetType, sourceType);
4252
+ const { converter } = this.transformSSEToTool(targetType, sourceType);
4164
4253
  this.copyResponseHeaders(responseHeaders, res);
4165
4254
  // 收集日志:responseBody/streamChunks 记录上游原始响应;downstreamResponseBody 记录实际下发内容
4166
4255
  const finalizeChunks = () => {
@@ -4177,13 +4266,7 @@ class ProxyServer {
4177
4266
  extractedUsage = converterUsage || extractedUsage;
4178
4267
  }
4179
4268
  }
4180
- // 如果有自定义的 extractUsage 函数,使用它
4181
- if (extractUsage && extractedUsage) {
4182
- usageForLog = extractUsage(extractedUsage);
4183
- }
4184
- else if (extractedUsage) {
4185
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
4186
- }
4269
+ usageForLog = this.tokenUsageFromCollected(extractedUsage);
4187
4270
  };
4188
4271
  // 监听 res 的错误事件
4189
4272
  res.on('error', (err) => {
@@ -4984,7 +5067,7 @@ class ProxyServer {
4984
5067
  // 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
4985
5068
  const originalModel = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
4986
5069
  const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
4987
- const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
5070
+ const { converter } = this.transformSSEByFormat(clientFormat, sourceType);
4988
5071
  this.copyResponseHeaders(responseHeaders, res);
4989
5072
  res.status(response.status);
4990
5073
  const finalizeStreamChunks = () => {
@@ -4997,12 +5080,7 @@ class ProxyServer {
4997
5080
  if (converterUsage)
4998
5081
  extractedUsage = converterUsage;
4999
5082
  }
5000
- if (extractUsage && extractedUsage) {
5001
- usageForLog = extractUsage(extractedUsage);
5002
- }
5003
- else if (extractedUsage) {
5004
- usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
5005
- }
5083
+ usageForLog = this.tokenUsageFromCollected(extractedUsage);
5006
5084
  };
5007
5085
  try {
5008
5086
  yield new Promise((resolve, reject) => {