@kenz1117/dsh-ui-usage-billing 1.2.5 → 1.2.7

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/lib/index.js CHANGED
@@ -243,24 +243,13 @@ const WEEKEND_OFFPEAK_START_MS = Date.parse("2026-08-22T16:00:00Z");
243
243
  * 1/0.02/4(峰 = 谷 × 2)。目录条目写现行(新)价;分界前的历史事件由
244
244
  * {@link computeCostAt} 按 {@link FLASH_REPRICED_OFFPEAK} 回算旧价,与
245
245
  * {@link PEAK_ERA_START_MS} 同一「按事件时刻分段适用规则」口径。
246
- * pro 例外:V4 Pro 的路由计费分界是 {@link PRO_OFFLINE_MS}(官方把原定
247
- * 09-10 的路由时点推迟到 09-14 12:00),不在本分界切换。
246
+ * V4 Pro 不在本分界内:官方取消了 09-14 的路由计划,pro 长期按刊例计费。
248
247
  */
249
248
  const FLASH_REPRICE_MS = Date.parse("2026-09-10T04:00:00Z");
250
249
  /**
251
- * V4 Pro 下线路由分界(UTC 2026-09-14T04:00:00Z,即北京时间 2026-09-14
252
- * 12:00):官方价目页注释明确,此后至 V4.1 Pro 上线前,`deepseek-v4-pro`
253
- * 的请求全部路由至 V4.1 Flash 并按其单价计费——此前的请求仍按 V4 Pro
254
- * 峰谷刊例计费(官方原定 09-10 路由,后推迟到 09-14)。
255
- */
256
- const PRO_OFFLINE_MS = Date.parse("2026-09-14T04:00:00Z");
257
- /**
258
250
  * flash 系调价前的官方谷档价(CNY / 1M tokens):{@link FLASH_REPRICE_MS}
259
- * 之前的 flash / flash-vision-exp 事件按此回算(峰档 = 谷档 × 2)。pro 的
260
- * 同表条目是 V4 Pro 峰谷时代刊例(谷 4.5/0.15/13.5):官方自
261
- * {@link PRO_OFFLINE_MS} 起(原定 09-10,后推迟)把 V4 Pro 请求全部路由至
262
- * V4.1 Flash 并按其单价计费(V4.1 Pro 上线前),故分界前回算 V4 Pro 刊例、
263
- * 分界后走目录的 Flash 价。用户价 = 实付价,与 legacy 口径相同地跳过本表。
251
+ * 之前的 flash / flash-vision-exp 事件按此回算(峰档 = 谷档 × 2)。
252
+ * 用户价 = 实付价,与 legacy 口径相同地跳过本表。
264
253
  */
265
254
  const FLASH_PRE_REPRICE_BAND = {
266
255
  input: 1.5,
@@ -269,12 +258,7 @@ const FLASH_PRE_REPRICE_BAND = {
269
258
  };
270
259
  const FLASH_REPRICED_OFFPEAK = {
271
260
  flash: FLASH_PRE_REPRICE_BAND,
272
- "flash-vision-exp": FLASH_PRE_REPRICE_BAND,
273
- pro: {
274
- input: 4.5,
275
- cacheHit: .15,
276
- output: 13.5
277
- }
261
+ "flash-vision-exp": FLASH_PRE_REPRICE_BAND
278
262
  };
279
263
  /**
280
264
  * DeepSeek V4 峰谷时代之前的官方基础价(CNY / 1M tokens):官方中文定价页
@@ -347,7 +331,7 @@ const MODEL_CATALOG = [
347
331
  key: "flash",
348
332
  name: "DeepSeek V4.1 Flash",
349
333
  provider: "DeepSeek",
350
- colorVar: "dsw-static-blue-500",
334
+ colorVar: "ds-blue",
351
335
  price: {
352
336
  currency: "CNY",
353
337
  input: 2,
@@ -365,7 +349,7 @@ const MODEL_CATALOG = [
365
349
  key: "flash-vision-exp",
366
350
  name: "DeepSeek V4 Flash Vision (Exp)",
367
351
  provider: "DeepSeek",
368
- colorVar: "dsw-static-blue-500",
352
+ colorVar: "ds-blue",
369
353
  retired: true,
370
354
  price: {
371
355
  currency: "CNY",
@@ -387,13 +371,13 @@ const MODEL_CATALOG = [
387
371
  colorVar: "dsw-static-deepseek-500",
388
372
  price: {
389
373
  currency: "CNY",
390
- input: 2,
391
- cacheHit: .04,
392
- output: 8,
374
+ input: 9,
375
+ cacheHit: .3,
376
+ output: 27,
393
377
  offPeak: {
394
- input: 1,
395
- cacheHit: .02,
396
- output: 4
378
+ input: 4.5,
379
+ cacheHit: .15,
380
+ output: 13.5
397
381
  }
398
382
  },
399
383
  peakHours: DEEPSEEK_PEAK_HOURS
@@ -414,7 +398,7 @@ const MODEL_CATALOG = [
414
398
  key: "glm-5.3",
415
399
  name: "GLM-5.3",
416
400
  provider: "智谱 AI",
417
- colorVar: "dsw-static-blue-500",
401
+ colorVar: "ds-blue",
418
402
  price: {
419
403
  currency: "CNY",
420
404
  input: 8,
@@ -480,7 +464,7 @@ const MODEL_CATALOG = [
480
464
  key: "glm-5-turbo",
481
465
  name: "GLM-5-Turbo",
482
466
  provider: "智谱 AI",
483
- colorVar: "dsw-static-blue-500",
467
+ colorVar: "ds-blue",
484
468
  price: {
485
469
  currency: "CNY",
486
470
  input: 5,
@@ -622,7 +606,7 @@ const MODEL_CATALOG = [
622
606
  key: "qwen-plus",
623
607
  name: "Qwen3.5-Plus",
624
608
  provider: "阿里通义",
625
- colorVar: "dsw-static-blue-500",
609
+ colorVar: "ds-blue",
626
610
  price: {
627
611
  currency: "CNY",
628
612
  input: .8,
@@ -675,7 +659,7 @@ const MODEL_CATALOG = [
675
659
  key: "doubao",
676
660
  name: "Doubao Seed-2.0 Pro",
677
661
  provider: "字节豆包",
678
- colorVar: "dsw-static-red-500",
662
+ colorVar: "ds-red",
679
663
  price: {
680
664
  currency: "CNY",
681
665
  input: 3.2,
@@ -711,7 +695,7 @@ const MODEL_CATALOG = [
711
695
  key: "doubao-seed-evolving",
712
696
  name: "Doubao-Seed-Evolving",
713
697
  provider: "字节豆包",
714
- colorVar: "dsw-static-red-500",
698
+ colorVar: "ds-red",
715
699
  price: {
716
700
  currency: "CNY",
717
701
  input: 6,
@@ -819,7 +803,7 @@ const MODEL_CATALOG = [
819
803
  key: "minimax",
820
804
  name: "MiniMax-M3",
821
805
  provider: "MiniMax",
822
- colorVar: "dsw-static-amber-500",
806
+ colorVar: "ds-amber",
823
807
  price: {
824
808
  currency: "CNY",
825
809
  input: 2.1,
@@ -843,7 +827,7 @@ const MODEL_CATALOG = [
843
827
  key: "minimax-m2.7-highspeed",
844
828
  name: "MiniMax-M2.7-highspeed",
845
829
  provider: "MiniMax",
846
- colorVar: "dsw-static-amber-500",
830
+ colorVar: "ds-amber",
847
831
  price: {
848
832
  currency: "CNY",
849
833
  input: 4.2,
@@ -891,7 +875,7 @@ const MODEL_CATALOG = [
891
875
  key: "yi",
892
876
  name: "Yi-Lightning",
893
877
  provider: "零一万物",
894
- colorVar: "dsw-static-green-500",
878
+ colorVar: "ds-green",
895
879
  price: {
896
880
  currency: "CNY",
897
881
  input: .99,
@@ -949,16 +933,34 @@ const MODEL_CATALOG = [
949
933
  output: 9
950
934
  }
951
935
  },
936
+ {
937
+ key: "gpt-6-astra",
938
+ name: "GPT-6 Astra",
939
+ provider: "OpenAI",
940
+ colorVar: "ds-green",
941
+ price: {
942
+ currency: "USD",
943
+ input: 10,
944
+ cacheHit: 1,
945
+ output: 50
946
+ }
947
+ },
952
948
  {
953
949
  key: "gpt-5.6-sol",
954
950
  name: "GPT-5.6 Sol",
955
951
  provider: "OpenAI",
956
- colorVar: "dsw-static-green-500",
952
+ colorVar: "ds-green",
957
953
  price: {
958
954
  currency: "USD",
959
955
  input: 5,
960
956
  cacheHit: .5,
961
957
  output: 30
958
+ },
959
+ promo: {
960
+ factor: .8,
961
+ factors: { output: 2 / 3 },
962
+ endsAtMs: Date.UTC(2026, 10, 21, 16, 0, 0),
963
+ note: "限时促销至 2026-11-21"
962
964
  }
963
965
  },
964
966
  {
@@ -977,7 +979,7 @@ const MODEL_CATALOG = [
977
979
  key: "gpt-5.6-luna",
978
980
  name: "GPT-5.6 Luna",
979
981
  provider: "OpenAI",
980
- colorVar: "dsw-static-green-500",
982
+ colorVar: "ds-green",
981
983
  price: {
982
984
  currency: "USD",
983
985
  input: .2,
@@ -1051,7 +1053,7 @@ const MODEL_CATALOG = [
1051
1053
  key: "llama",
1052
1054
  name: "Llama 4 Maverick",
1053
1055
  provider: "Meta",
1054
- colorVar: "dsw-static-red-500",
1056
+ colorVar: "ds-red",
1055
1057
  price: {
1056
1058
  currency: "USD",
1057
1059
  input: .2,
@@ -1075,7 +1077,7 @@ const MODEL_CATALOG = [
1075
1077
  key: "claude-opus-4-6",
1076
1078
  name: "Claude Opus 4.6",
1077
1079
  provider: "Anthropic",
1078
- colorVar: "dsw-static-red-500",
1080
+ colorVar: "ds-red",
1079
1081
  price: {
1080
1082
  currency: "USD",
1081
1083
  input: 5,
@@ -1111,7 +1113,7 @@ const MODEL_CATALOG = [
1111
1113
  key: "claude-opus-5",
1112
1114
  name: "Claude Opus 5",
1113
1115
  provider: "Anthropic",
1114
- colorVar: "dsw-static-red-500",
1116
+ colorVar: "ds-red",
1115
1117
  price: {
1116
1118
  currency: "USD",
1117
1119
  input: 5,
@@ -1135,7 +1137,7 @@ const MODEL_CATALOG = [
1135
1137
  key: "mistral-large-2512",
1136
1138
  name: "Mistral Large 3",
1137
1139
  provider: "Mistral AI",
1138
- colorVar: "dsw-static-violet-500",
1140
+ colorVar: "ds-violet",
1139
1141
  price: {
1140
1142
  currency: "USD",
1141
1143
  input: .5,
@@ -1171,7 +1173,7 @@ const MODEL_CATALOG = [
1171
1173
  key: "command-a-03-2025",
1172
1174
  name: "Command A",
1173
1175
  provider: "Cohere",
1174
- colorVar: "dsw-static-cyan-500",
1176
+ colorVar: "ds-cyan",
1175
1177
  price: {
1176
1178
  currency: "USD",
1177
1179
  input: 2.5,
@@ -1195,7 +1197,7 @@ const MODEL_CATALOG = [
1195
1197
  key: "longcat-2.0",
1196
1198
  name: "LongCat 2.0",
1197
1199
  provider: "美团",
1198
- colorVar: "dsw-static-amber-500",
1200
+ colorVar: "ds-amber",
1199
1201
  price: {
1200
1202
  currency: "CNY",
1201
1203
  input: 4,
@@ -1208,7 +1210,7 @@ const MODEL_CATALOG = [
1208
1210
  key: "minicpm-v-4.5",
1209
1211
  name: "MiniCPM-V 4.5",
1210
1212
  provider: "面壁智能",
1211
- colorVar: "dsw-static-green-500",
1213
+ colorVar: "ds-green",
1212
1214
  price: {
1213
1215
  currency: "CNY",
1214
1216
  input: 1,
@@ -1234,7 +1236,7 @@ const MODEL_CATALOG = [
1234
1236
  key: "dots-3-note-preview",
1235
1237
  name: "Dots3-Note Preview",
1236
1238
  provider: "小红书",
1237
- colorVar: "dsw-static-red-500",
1239
+ colorVar: "ds-red",
1238
1240
  price: {
1239
1241
  currency: "CNY",
1240
1242
  input: 2,
@@ -1346,6 +1348,19 @@ const MODEL_CATALOG = [
1346
1348
  },
1347
1349
  estimated: true
1348
1350
  },
1351
+ {
1352
+ key: "kimi-k2.8-preview",
1353
+ name: "Kimi K2.8 Preview",
1354
+ provider: "月之暗面",
1355
+ colorVar: "dsw-static-cyan-400",
1356
+ price: {
1357
+ currency: "CNY",
1358
+ input: 6.5,
1359
+ cacheHit: 1.3,
1360
+ output: 27
1361
+ },
1362
+ estimated: true
1363
+ },
1349
1364
  {
1350
1365
  key: "kimi-k2.6-fast",
1351
1366
  name: "Kimi K2.6 Fast",
@@ -1388,7 +1403,7 @@ const MODEL_CATALOG = [
1388
1403
  key: "doubao-seed-2.0-code",
1389
1404
  name: "Doubao Seed-2.0 Code",
1390
1405
  provider: "字节豆包",
1391
- colorVar: "dsw-static-red-500",
1406
+ colorVar: "ds-red",
1392
1407
  price: {
1393
1408
  currency: "CNY",
1394
1409
  input: 3.2,
@@ -1508,6 +1523,12 @@ const MODEL_KEY_ALIASES = {
1508
1523
  "seed-2.0-mini": "doubao-mini",
1509
1524
  "k3": "kimi-k3",
1510
1525
  "kimi-k3": "kimi-k3",
1526
+ "kimi-k2.8-preview": "kimi-k2.8-preview",
1527
+ "kimi-k2.8": "kimi-k2.8-preview",
1528
+ "kimi-k2-8-preview": "kimi-k2.8-preview",
1529
+ "kimi-k2-8": "kimi-k2.8-preview",
1530
+ "k2.8-preview": "kimi-k2.8-preview",
1531
+ "k2.8": "kimi-k2.8-preview",
1511
1532
  "minimax-m1": "minimax",
1512
1533
  "minimax-m2": "minimax",
1513
1534
  "minimax-m3": "minimax",
@@ -1516,7 +1537,9 @@ const MODEL_KEY_ALIASES = {
1516
1537
  "minimax-m2.7-high-speed": "minimax-m2.7-highspeed",
1517
1538
  "minimax-m2-7": "minimax-m2.7",
1518
1539
  "minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
1519
- "minimax-m2-7-high-speed": "minimax-m2.7-highspeed"
1540
+ "minimax-m2-7-high-speed": "minimax-m2.7-highspeed",
1541
+ "gpt-6": "gpt-6-astra",
1542
+ "gpt-6-astra": "gpt-6-astra"
1520
1543
  };
1521
1544
  /**
1522
1545
  * 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
@@ -1681,8 +1704,9 @@ function isPromoActive(promo, nowMs) {
1681
1704
  return Number.isFinite(nowMs) && !expired && promo.factor > 0 && promo.factor < 1;
1682
1705
  }
1683
1706
  /**
1684
- * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 全部乘 factor 的
1685
- * 副本,其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
1707
+ * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 逐档乘折扣系数的
1708
+ * 副本(某档在 promo.factors 有合法覆盖时用覆盖值,否则用 promo.factor),
1709
+ * 其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
1686
1710
  * 幂等由调用方保证——计价与费率表显示各自只折一次,勿对已折价副本重复应用。
1687
1711
  * @param entry - 目录条目(price 保持刊例价口径)。
1688
1712
  * @param nowMs - 判定时刻(epoch ms)。
@@ -1690,11 +1714,12 @@ function isPromoActive(promo, nowMs) {
1690
1714
  function applyPromo(entry, nowMs) {
1691
1715
  const { promo } = entry;
1692
1716
  if (promo === void 0 || !isPromoActive(promo, nowMs)) return entry;
1717
+ const factorOf = (field) => promo.factors?.[field] ?? promo.factor;
1693
1718
  const scaled = (band) => ({
1694
- input: band.input * promo.factor,
1695
- cacheHit: band.cacheHit * promo.factor,
1696
- ...band.cacheMiss !== void 0 ? { cacheMiss: band.cacheMiss * promo.factor } : {},
1697
- output: band.output * promo.factor
1719
+ input: band.input * factorOf("input"),
1720
+ cacheHit: band.cacheHit * factorOf("cacheHit"),
1721
+ ...band.cacheMiss !== void 0 ? { cacheMiss: band.cacheMiss * factorOf("cacheMiss") } : {},
1722
+ output: band.output * factorOf("output")
1698
1723
  });
1699
1724
  return {
1700
1725
  ...entry,
@@ -1771,7 +1796,7 @@ function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
1771
1796
  const priced = applyPromo(entry, timeMs);
1772
1797
  const legacy = timeMs < PEAK_ERA_START_MS && entry.userPriced !== true ? LEGACY_DEEPSEEK_BANDS[entry.key] : void 0;
1773
1798
  if (legacy !== void 0) return priceBandCost(legacy, buckets, "CNY");
1774
- const repriced = timeMs < (entry.key === "pro" ? PRO_OFFLINE_MS : FLASH_REPRICE_MS) && entry.userPriced !== true ? FLASH_REPRICED_OFFPEAK[entry.key] : void 0;
1799
+ const repriced = timeMs < FLASH_REPRICE_MS && entry.userPriced !== true ? FLASH_REPRICED_OFFPEAK[entry.key] : void 0;
1775
1800
  if (repriced !== void 0) return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? {
1776
1801
  input: repriced.input * 2,
1777
1802
  cacheHit: repriced.cacheHit * 2,
@@ -2022,9 +2047,151 @@ function workspaceNameOf(cwd) {
2022
2047
  }
2023
2048
  /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
2024
2049
  const PERF_SPIKE_MS = 1e4;
2050
+ /** 全零摘要(合并的恒等元)。 */
2051
+ const EMPTY_MODEL_DIGEST = {
2052
+ samples: 0,
2053
+ ttftSum: 0,
2054
+ ttftMax: 0,
2055
+ ttftSpikes: 0,
2056
+ ttftP50: 0,
2057
+ ttftP90: 0,
2058
+ tpsCount: 0,
2059
+ tpsSum: 0,
2060
+ latencyCount: 0,
2061
+ latencySum: 0,
2062
+ estimated: 0
2063
+ };
2064
+ /** 从样本集构建模型级摘要(样本侧精确:P50/P90 为精确插值)。 */
2065
+ function modelDigestOf(samples) {
2066
+ const ttfts = [];
2067
+ let ttftSum = 0;
2068
+ let ttftMax = 0;
2069
+ let ttftSpikes = 0;
2070
+ let tpsCount = 0;
2071
+ let tpsSum = 0;
2072
+ let latencyCount = 0;
2073
+ let latencySum = 0;
2074
+ let estimated = 0;
2075
+ for (const sample of samples) {
2076
+ ttfts.push(sample.ttftMs);
2077
+ ttftSum += sample.ttftMs;
2078
+ if (sample.ttftMs > ttftMax) ttftMax = sample.ttftMs;
2079
+ if (sample.ttftMs > 1e4) ttftSpikes += 1;
2080
+ if (sample.tps !== void 0) {
2081
+ tpsCount += 1;
2082
+ tpsSum += sample.tps;
2083
+ }
2084
+ if (sample.latencyMs !== void 0) {
2085
+ latencyCount += 1;
2086
+ latencySum += sample.latencyMs;
2087
+ }
2088
+ if (sample.estimated) estimated += 1;
2089
+ }
2090
+ return {
2091
+ samples: samples.length,
2092
+ ttftSum,
2093
+ ttftMax,
2094
+ ttftSpikes,
2095
+ ttftP50: percentile(ttfts, .5),
2096
+ ttftP90: percentile(ttfts, .9),
2097
+ tpsCount,
2098
+ tpsSum,
2099
+ latencyCount,
2100
+ latencySum,
2101
+ estimated
2102
+ };
2103
+ }
2104
+ /** 合并两个模型级摘要:计数/求和/极值精确;分位数按样本数加权。 */
2105
+ function mergeModelDigest(a, b) {
2106
+ const n = a.samples + b.samples;
2107
+ const weighted = (x, y) => n === 0 ? 0 : (x * a.samples + y * b.samples) / n;
2108
+ return {
2109
+ samples: n,
2110
+ ttftSum: a.ttftSum + b.ttftSum,
2111
+ ttftMax: Math.max(a.ttftMax, b.ttftMax),
2112
+ ttftSpikes: a.ttftSpikes + b.ttftSpikes,
2113
+ ttftP50: weighted(a.ttftP50, b.ttftP50),
2114
+ ttftP90: weighted(a.ttftP90, b.ttftP90),
2115
+ tpsCount: a.tpsCount + b.tpsCount,
2116
+ tpsSum: a.tpsSum + b.tpsSum,
2117
+ latencyCount: a.latencyCount + b.latencyCount,
2118
+ latencySum: a.latencySum + b.latencySum,
2119
+ estimated: a.estimated + b.estimated
2120
+ };
2121
+ }
2122
+ /** 从样本集构建小时级摘要(均值可无损重建)。 */
2123
+ function hourDigestOf(samples) {
2124
+ let ttftSum = 0;
2125
+ let tpsCount = 0;
2126
+ let tpsSum = 0;
2127
+ for (const sample of samples) {
2128
+ ttftSum += sample.ttftMs;
2129
+ if (sample.tps !== void 0) {
2130
+ tpsCount += 1;
2131
+ tpsSum += sample.tps;
2132
+ }
2133
+ }
2134
+ return {
2135
+ samples: samples.length,
2136
+ ttftSum,
2137
+ tpsCount,
2138
+ tpsSum
2139
+ };
2140
+ }
2141
+ /** 全零小时摘要(合并的恒等元)。 */
2142
+ const EMPTY_HOUR_DIGEST = {
2143
+ samples: 0,
2144
+ ttftSum: 0,
2145
+ tpsCount: 0,
2146
+ tpsSum: 0
2147
+ };
2148
+ /** 合并两个小时级摘要(纯字段加法,无损)。 */
2149
+ function mergeHourDigest(a, b) {
2150
+ return {
2151
+ samples: a.samples + b.samples,
2152
+ ttftSum: a.ttftSum + b.ttftSum,
2153
+ tpsCount: a.tpsCount + b.tpsCount,
2154
+ tpsSum: a.tpsSum + b.tpsSum
2155
+ };
2156
+ }
2157
+ /**
2158
+ * 样本集 → 账本摘要形态(模型 → 摘要;小时 → 模型 → 摘要)。serializeFold
2159
+ * 与账本迁移共用同一转换,两处口径不会漂移。
2160
+ * @param samples - 一个会话的全部性能样本。
2161
+ * @returns `{ byModel, byHourModel }` 的 JSON-safe 记录。
2162
+ */
2163
+ function perfDigestsOf(samples) {
2164
+ const byModelSamples = /* @__PURE__ */ new Map();
2165
+ const byHourModelSamples = /* @__PURE__ */ new Map();
2166
+ for (const sample of samples) {
2167
+ let modelSamples = byModelSamples.get(sample.model);
2168
+ if (modelSamples === void 0) {
2169
+ modelSamples = [];
2170
+ byModelSamples.set(sample.model, modelSamples);
2171
+ }
2172
+ modelSamples.push(sample);
2173
+ let hourModels = byHourModelSamples.get(sample.hour);
2174
+ if (hourModels === void 0) {
2175
+ hourModels = /* @__PURE__ */ new Map();
2176
+ byHourModelSamples.set(sample.hour, hourModels);
2177
+ }
2178
+ let hourModelSamples = hourModels.get(sample.model);
2179
+ if (hourModelSamples === void 0) {
2180
+ hourModelSamples = [];
2181
+ hourModels.set(sample.model, hourModelSamples);
2182
+ }
2183
+ hourModelSamples.push(sample);
2184
+ }
2185
+ return {
2186
+ byModel: Object.fromEntries([...byModelSamples].map(([model, list]) => [model, modelDigestOf(list)])),
2187
+ byHourModel: Object.fromEntries([...byHourModelSamples].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, list]) => [model, hourDigestOf(list)]))]))
2188
+ };
2189
+ }
2025
2190
  /**
2026
2191
  * 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
2027
- * header 归因算法);此后新写入的行总带当前 {@link FOLD_VERSION}。
2192
+ * header 归因算法);perf 摘要化迁移把 v15 及更早行的逐样本 `perf` 数组转换为
2193
+ * `perfDigest`/`perfHourDigest` 摘要(日志已删的行不再被重折,迁移是它们唯一的
2194
+ * 收缩机会);此后新写入的行总带当前 {@link FOLD_VERSION} 与摘要形态。
2028
2195
  */
2029
2196
  const LEDGER_MIGRATIONS = [{
2030
2197
  id: "fold-version-backfill",
@@ -2036,6 +2203,26 @@ const LEDGER_MIGRATIONS = [{
2036
2203
  }
2037
2204
  return changed;
2038
2205
  }
2206
+ }, {
2207
+ id: "perf-sample-to-digest",
2208
+ apply(document) {
2209
+ let changed = false;
2210
+ for (const session of document.sessions) {
2211
+ const fold = session.fold;
2212
+ if (Array.isArray(fold.perf)) {
2213
+ const { byModel, byHourModel } = perfDigestsOf(fold.perf);
2214
+ fold.perfDigest = byModel;
2215
+ fold.perfHourDigest = byHourModel;
2216
+ delete fold.perf;
2217
+ changed = true;
2218
+ } else if (fold.perfDigest === void 0 || fold.perfHourDigest === void 0) {
2219
+ fold.perfDigest ??= {};
2220
+ fold.perfHourDigest ??= {};
2221
+ changed = true;
2222
+ }
2223
+ }
2224
+ return changed;
2225
+ }
2039
2226
  }];
2040
2227
  /**
2041
2228
  * 在加载边界对账本文档应用未执行的迁移,并记录已应用 id 供写回。
@@ -2059,6 +2246,7 @@ function runLedgerMigrations(document, migrations = LEDGER_MIGRATIONS) {
2059
2246
  }
2060
2247
  /** Serialize Map/Set-heavy fold state into a JSON-safe ledger entry. */
2061
2248
  function serializeFold(fold) {
2249
+ const { byModel: perfDigest, byHourModel: perfHourDigest } = perfDigestsOf(fold.perf);
2062
2250
  return {
2063
2251
  total: fold.total,
2064
2252
  byModel: Object.fromEntries(fold.byModel),
@@ -2071,7 +2259,8 @@ function serializeFold(fold) {
2071
2259
  unpricedModels: [...fold.unpricedModels],
2072
2260
  planCalls: Object.fromEntries(fold.planCalls),
2073
2261
  turns: fold.turns,
2074
- perf: fold.perf,
2262
+ perfDigest,
2263
+ perfHourDigest,
2075
2264
  roles: fold.roles,
2076
2265
  lastActive: fold.lastActive,
2077
2266
  ...fold.title !== void 0 ? { title: fold.title } : {}
@@ -2091,7 +2280,9 @@ function deserializeFold(fold) {
2091
2280
  unpricedModels: new Set(fold.unpricedModels),
2092
2281
  planCalls: new Map(Object.entries(fold.planCalls)),
2093
2282
  turns: fold.turns,
2094
- perf: fold.perf,
2283
+ perf: [],
2284
+ perfDigest: new Map(Object.entries(fold.perfDigest)),
2285
+ perfHourDigest: new Map(Object.entries(fold.perfHourDigest).map(([hour, models]) => [hour, new Map(Object.entries(models))])),
2095
2286
  roles: fold.roles,
2096
2287
  lastActive: fold.lastActive,
2097
2288
  ...fold.title !== void 0 ? { title: fold.title } : {}
@@ -2107,7 +2298,7 @@ function ledgerSessionsOf(value) {
2107
2298
  const row = entry;
2108
2299
  if (typeof row.id !== "string" || row.id === "" || row.fold === null || typeof row.fold !== "object") return false;
2109
2300
  const fold = row.fold;
2110
- return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) && Array.isArray(fold.perf) && fold.roles !== void 0 && typeof fold.lastActive === "number";
2301
+ return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) && fold.perfDigest !== void 0 && fold.perfHourDigest !== void 0 && fold.roles !== void 0 && typeof fold.lastActive === "number";
2111
2302
  });
2112
2303
  }
2113
2304
  /**
@@ -2202,6 +2393,8 @@ function freshFold() {
2202
2393
  planCalls: /* @__PURE__ */ new Map(),
2203
2394
  turns: [],
2204
2395
  perf: [],
2396
+ perfDigest: /* @__PURE__ */ new Map(),
2397
+ perfHourDigest: /* @__PURE__ */ new Map(),
2205
2398
  roles: {
2206
2399
  userChars: 0,
2207
2400
  toolChars: 0,
@@ -2443,11 +2636,6 @@ function mergeUsageInto(acc, cell) {
2443
2636
  acc.officialCost += cell.officialCost;
2444
2637
  if (cell.searchCalls !== void 0) acc.searchCalls = (acc.searchCalls ?? 0) + cell.searchCalls;
2445
2638
  }
2446
- /** 均值(数组非空时调用;空数组按 0 兜底)。 */
2447
- function mean(values) {
2448
- if (values.length === 0) return 0;
2449
- return values.reduce((sum, value) => sum + value, 0) / values.length;
2450
- }
2451
2639
  /** 分位数(0..1):先拷贝排序,再线性插值;空数组返回 0。 */
2452
2640
  function percentile(values, p) {
2453
2641
  if (values.length === 0) return 0;
@@ -2464,6 +2652,53 @@ function percentile(values, p) {
2464
2652
  if (a === void 0 || b === void 0) return 0;
2465
2653
  return a + (b - a) * (idx - lo);
2466
2654
  }
2655
+ /** 活样本累加器 → 模型级摘要(跨会话合并前先归一到摘要形态)。 */
2656
+ function accumToModelDigest(acc) {
2657
+ return {
2658
+ samples: acc.ttfts.length,
2659
+ ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
2660
+ ttftMax: acc.ttfts.length === 0 ? 0 : Math.max(...acc.ttfts),
2661
+ ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2662
+ ttftP50: percentile(acc.ttfts, .5),
2663
+ ttftP90: percentile(acc.ttfts, .9),
2664
+ tpsCount: acc.tps.length,
2665
+ tpsSum: acc.tps.reduce((sum, value) => sum + value, 0),
2666
+ latencyCount: acc.latencies.length,
2667
+ latencySum: acc.latencies.reduce((sum, value) => sum + value, 0),
2668
+ estimated: acc.estimated
2669
+ };
2670
+ }
2671
+ /** 活样本小时累加器 → 小时级摘要。 */
2672
+ function accumToHourDigest(acc) {
2673
+ return {
2674
+ samples: acc.ttfts.length,
2675
+ ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
2676
+ tpsCount: acc.tps.length,
2677
+ tpsSum: acc.tps.reduce((sum, value) => sum + value, 0)
2678
+ };
2679
+ }
2680
+ /** 模型级摘要 → 面板行(均值由求和/计数派生)。 */
2681
+ function modelPerfOf(digest) {
2682
+ return {
2683
+ samples: digest.samples,
2684
+ ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
2685
+ ttftP50: digest.ttftP50,
2686
+ ttftP90: digest.ttftP90,
2687
+ ttftMax: digest.ttftMax,
2688
+ ttftSpikes: digest.ttftSpikes,
2689
+ ...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount },
2690
+ latencyAvg: digest.latencyCount === 0 ? 0 : digest.latencySum / digest.latencyCount,
2691
+ estimatedSamples: digest.estimated
2692
+ };
2693
+ }
2694
+ /** 小时级摘要 → 面板行(均值由求和/计数派生)。 */
2695
+ function hourPerfOf(digest) {
2696
+ return {
2697
+ samples: digest.samples,
2698
+ ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
2699
+ ...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount }
2700
+ };
2701
+ }
2467
2702
  /**
2468
2703
  * 聚合配置指纹:影响折叠语义的全部配置(订阅豁免、官方名单、路由别名、搜索估值)
2469
2704
  * 的稳定序列化。账本行的复用判定携带该指纹——用户改配置后(如为 grok build 加
@@ -2511,7 +2746,7 @@ function createUsageAggregator(persistence, options = {}) {
2511
2746
  id,
2512
2747
  ...cwd === void 0 ? {} : { cwd },
2513
2748
  ...stamp === null ? {} : { stamp },
2514
- foldVersion: 14,
2749
+ foldVersion: 16,
2515
2750
  fingerprint: foldFingerprint,
2516
2751
  fold: serializeFold(fold)
2517
2752
  };
@@ -2608,7 +2843,7 @@ function createUsageAggregator(persistence, options = {}) {
2608
2843
  try {
2609
2844
  const cwd = meta.cwd;
2610
2845
  const ledgerRow = ledger.get(id);
2611
- if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 14 && ledgerRow.fingerprint === foldFingerprint) {
2846
+ if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 16 && ledgerRow.fingerprint === foldFingerprint) {
2612
2847
  const fold = deserializeFold(ledgerRow.fold);
2613
2848
  folds.push({
2614
2849
  id,
@@ -2690,7 +2925,7 @@ function createUsageAggregator(persistence, options = {}) {
2690
2925
  for (const entry of ledger.values()) {
2691
2926
  if (included.has(entry.id)) continue;
2692
2927
  try {
2693
- const stale = (entry.foldVersion ?? 1) < 14 || entry.fingerprint !== foldFingerprint;
2928
+ const stale = (entry.foldVersion ?? 1) < 16 || entry.fingerprint !== foldFingerprint;
2694
2929
  folds.push({
2695
2930
  id: entry.id,
2696
2931
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -2727,6 +2962,8 @@ function createUsageAggregator(persistence, options = {}) {
2727
2962
  };
2728
2963
  const perfModel = /* @__PURE__ */ new Map();
2729
2964
  const perfHourModel = /* @__PURE__ */ new Map();
2965
+ const perfModelDigest = /* @__PURE__ */ new Map();
2966
+ const perfHourDigest = /* @__PURE__ */ new Map();
2730
2967
  for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
2731
2968
  mergeUsageInto(total, fold.total);
2732
2969
  roles.userChars += fold.roles.userChars;
@@ -2773,6 +3010,15 @@ function createUsageAggregator(persistence, options = {}) {
2773
3010
  hourAccum.ttfts.push(sample.ttftMs);
2774
3011
  if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
2775
3012
  }
3013
+ for (const [model, digest] of fold.perfDigest) perfModelDigest.set(model, mergeModelDigest(perfModelDigest.get(model) ?? EMPTY_MODEL_DIGEST, digest));
3014
+ for (const [hour, models] of fold.perfHourDigest) {
3015
+ let hourModels = perfHourDigest.get(hour);
3016
+ if (hourModels === void 0) {
3017
+ hourModels = /* @__PURE__ */ new Map();
3018
+ perfHourDigest.set(hour, hourModels);
3019
+ }
3020
+ for (const [model, digest] of models) hourModels.set(model, mergeHourDigest(hourModels.get(model) ?? EMPTY_HOUR_DIGEST, digest));
3021
+ }
2776
3022
  for (const row of fold.turns) turnRows.push({
2777
3023
  sessionId,
2778
3024
  ...row
@@ -2816,23 +3062,26 @@ function createUsageAggregator(persistence, options = {}) {
2816
3062
  };
2817
3063
  const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
2818
3064
  const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
2819
- const perf = perfModel.size === 0 ? void 0 : {
2820
- byModel: Object.fromEntries([...perfModel].map(([model, acc]) => [model, {
2821
- samples: acc.ttfts.length,
2822
- ttftAvg: mean(acc.ttfts),
2823
- ttftP50: percentile(acc.ttfts, .5),
2824
- ttftP90: percentile(acc.ttfts, .9),
2825
- ttftMax: Math.max(...acc.ttfts),
2826
- ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2827
- ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) },
2828
- latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2829
- estimatedSamples: acc.estimated
2830
- }])),
2831
- byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
2832
- samples: acc.ttfts.length,
2833
- ttftAvg: mean(acc.ttfts),
2834
- ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
2835
- }]))]))
3065
+ const modelKeys = /* @__PURE__ */ new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3066
+ const hourKeys = /* @__PURE__ */ new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3067
+ const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
3068
+ byModel: Object.fromEntries([...modelKeys].map((model) => {
3069
+ const live = perfModel.get(model);
3070
+ const restored = perfModelDigest.get(model);
3071
+ const digest = live === void 0 ? restored : mergeModelDigest(accumToModelDigest(live), restored ?? EMPTY_MODEL_DIGEST);
3072
+ return digest === void 0 ? null : [model, modelPerfOf(digest)];
3073
+ }).filter((entry) => entry !== null)),
3074
+ byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
3075
+ const liveModels = perfHourModel.get(hour);
3076
+ const digestModels = perfHourDigest.get(hour);
3077
+ const modelKeysInHour = /* @__PURE__ */ new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3078
+ return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
3079
+ const live = liveModels?.get(model);
3080
+ const restored = digestModels?.get(model);
3081
+ const digest = live === void 0 ? restored : mergeHourDigest(accumToHourDigest(live), restored ?? EMPTY_HOUR_DIGEST);
3082
+ return digest === void 0 ? null : [model, hourPerfOf(digest)];
3083
+ }).filter((entry) => entry !== null))];
3084
+ }))
2836
3085
  };
2837
3086
  lastDoc = {
2838
3087
  version: 4,
@@ -2878,6 +3127,29 @@ function createUsageAggregator(persistence, options = {}) {
2878
3127
  });
2879
3128
  return inflight;
2880
3129
  },
3130
+ async sessionUsageOf(sessionId) {
3131
+ await this.aggregate();
3132
+ const hit = cache.get(sessionId);
3133
+ if (hit !== void 0) return {
3134
+ calls: hit.fold.total.calls,
3135
+ cost: hit.fold.total.cost,
3136
+ input: hit.fold.total.input,
3137
+ output: hit.fold.total.output
3138
+ };
3139
+ const row = ledger.get(sessionId);
3140
+ if (row === void 0) return void 0;
3141
+ try {
3142
+ const fold = deserializeFold(row.fold);
3143
+ return {
3144
+ calls: fold.total.calls,
3145
+ cost: fold.total.cost,
3146
+ input: fold.total.input,
3147
+ output: fold.total.output
3148
+ };
3149
+ } catch {
3150
+ return;
3151
+ }
3152
+ },
2881
3153
  async flush() {
2882
3154
  if (inflight !== void 0) await inflight.catch(() => {});
2883
3155
  await saveLedger(true);
@@ -5443,8 +5715,10 @@ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5443
5715
  * 下发,供「设置 → 插件信息卡」展示。发布版 lib/index.js 相对包根解析。
5444
5716
  */
5445
5717
  const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
5446
- /** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
5447
- const SNAPSHOT_INTERVAL_MS = 3e4;
5718
+ /** 统计快照的落盘节流(毫秒):快照只服务聚合失败回退与重启首屏(展示级
5719
+ * 新鲜度即可),降到 5 分钟一次——原子写含上一版备份是两次全量 JSON 落盘,
5720
+ * 重度用户快照上百 KB,跟随前端轮询 30 秒写一次纯属写放大。 */
5721
+ const SNAPSHOT_INTERVAL_MS = 3e5;
5448
5722
  /** usage-stats 响应的等待预算(毫秒):聚合在此预算内完成就回实时数据,
5449
5723
  * 超预算立即回最近快照(stale-while-revalidate),后台折叠继续跑。
5450
5724
  * 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
@@ -5977,6 +6251,11 @@ function apply(ctx, config = {}) {
5977
6251
  }
5978
6252
  const sessionId = exec.agent?.id;
5979
6253
  if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
6254
+ const summary = await aggregator.sessionUsageOf(String(sessionId));
6255
+ if (summary !== void 0) return {
6256
+ range: args.range,
6257
+ ...summary
6258
+ };
5980
6259
  let cost = 0;
5981
6260
  let calls = 0;
5982
6261
  let input = 0;