@kenz1117/dsh-ui-usage-billing 1.0.2 → 1.0.3

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
@@ -326,6 +326,23 @@ const MODEL_CATALOG = [
326
326
  output: 28
327
327
  }
328
328
  },
329
+ {
330
+ key: "glm-5.3-flash",
331
+ name: "GLM-5.3-Flash",
332
+ provider: "智谱 AI",
333
+ colorVar: "dsw-static-blue-300",
334
+ price: {
335
+ currency: "CNY",
336
+ input: .8,
337
+ cacheHit: .23,
338
+ output: 2.8
339
+ },
340
+ promo: {
341
+ factor: .5,
342
+ endsAtMs: Date.UTC(2026, 8, 8, 16, 0, 0),
343
+ note: "限时 5 折"
344
+ }
345
+ },
329
346
  {
330
347
  key: "glm-4.6",
331
348
  name: "GLM-4.6",
@@ -406,10 +423,66 @@ const MODEL_CATALOG = [
406
423
  colorVar: "dsw-static-blue-600",
407
424
  price: {
408
425
  currency: "CNY",
409
- input: 13.58,
410
- cacheHit: 1.36,
411
- output: 40.74
412
- }
426
+ input: 12,
427
+ cacheHit: 1.5,
428
+ output: 36
429
+ },
430
+ extraRows: [
431
+ {
432
+ label: "显式缓存创建",
433
+ input: 15
434
+ },
435
+ {
436
+ label: "显式缓存命中",
437
+ input: 1
438
+ },
439
+ {
440
+ label: "Batch File",
441
+ input: 6,
442
+ output: 18,
443
+ note: "长期半价"
444
+ },
445
+ {
446
+ label: "Batch Chat",
447
+ input: 12,
448
+ output: 36,
449
+ note: "与标准价一致"
450
+ }
451
+ ]
452
+ },
453
+ {
454
+ key: "qwen-3.8-flash",
455
+ name: "Qwen3.8 Flash",
456
+ provider: "阿里通义",
457
+ colorVar: "dsw-static-blue-400",
458
+ price: {
459
+ currency: "CNY",
460
+ input: 1,
461
+ cacheHit: .1,
462
+ output: 3
463
+ },
464
+ extraRows: [
465
+ {
466
+ label: "显式缓存创建",
467
+ input: 1.25
468
+ },
469
+ {
470
+ label: "显式缓存命中",
471
+ input: .1
472
+ },
473
+ {
474
+ label: "Batch File",
475
+ input: .5,
476
+ output: 1.5,
477
+ note: "长期半价"
478
+ },
479
+ {
480
+ label: "Batch Chat",
481
+ input: 1,
482
+ output: 3,
483
+ note: "与标准价一致"
484
+ }
485
+ ]
413
486
  },
414
487
  {
415
488
  key: "qwen-max",
@@ -418,10 +491,36 @@ const MODEL_CATALOG = [
418
491
  colorVar: "dsw-static-blue-300",
419
492
  price: {
420
493
  currency: "CNY",
421
- input: 6,
422
- cacheHit: .6,
423
- output: 18
424
- }
494
+ input: 12,
495
+ cacheHit: 1.2,
496
+ output: 36
497
+ },
498
+ promo: {
499
+ factor: .5,
500
+ note: "限时 5 折"
501
+ },
502
+ extraRows: [
503
+ {
504
+ label: "显式缓存创建",
505
+ input: 15
506
+ },
507
+ {
508
+ label: "显式缓存命中",
509
+ input: 1.2
510
+ },
511
+ {
512
+ label: "Batch File",
513
+ input: 6,
514
+ output: 18,
515
+ note: "长期半价"
516
+ },
517
+ {
518
+ label: "Batch Chat",
519
+ input: 12,
520
+ output: 36,
521
+ note: "与标准价一致"
522
+ }
523
+ ]
425
524
  },
426
525
  {
427
526
  key: "qwen-plus",
@@ -433,7 +532,29 @@ const MODEL_CATALOG = [
433
532
  input: .8,
434
533
  cacheHit: .08,
435
534
  output: 4.8
436
- }
535
+ },
536
+ extraRows: [
537
+ {
538
+ label: "显式缓存创建",
539
+ input: 1
540
+ },
541
+ {
542
+ label: "显式缓存命中",
543
+ input: .08
544
+ },
545
+ {
546
+ label: "Batch File",
547
+ input: .4,
548
+ output: 2.4,
549
+ note: "长期半价"
550
+ },
551
+ {
552
+ label: "Batch Chat",
553
+ input: .8,
554
+ output: 4.8,
555
+ note: "与标准价一致"
556
+ }
557
+ ]
437
558
  },
438
559
  {
439
560
  key: "qwen-flash",
@@ -445,7 +566,14 @@ const MODEL_CATALOG = [
445
566
  input: .2,
446
567
  cacheHit: .02,
447
568
  output: 2
448
- }
569
+ },
570
+ extraRows: [{
571
+ label: "显式缓存创建",
572
+ input: .25
573
+ }, {
574
+ label: "显式缓存命中",
575
+ input: .02
576
+ }]
449
577
  },
450
578
  {
451
579
  key: "doubao",
@@ -1027,7 +1155,14 @@ const MODEL_CATALOG = [
1027
1155
  input: 9,
1028
1156
  cacheHit: .9,
1029
1157
  output: 54
1030
- }
1158
+ },
1159
+ extraRows: [{
1160
+ label: "显式缓存创建",
1161
+ input: 11.25
1162
+ }, {
1163
+ label: "显式缓存命中",
1164
+ input: .9
1165
+ }]
1031
1166
  },
1032
1167
  {
1033
1168
  key: "qwen3-coder-plus",
@@ -1039,7 +1174,14 @@ const MODEL_CATALOG = [
1039
1174
  input: 4,
1040
1175
  cacheHit: .8,
1041
1176
  output: 16
1042
- }
1177
+ },
1178
+ extraRows: [{
1179
+ label: "显式缓存创建",
1180
+ input: 5
1181
+ }, {
1182
+ label: "显式缓存命中",
1183
+ input: .4
1184
+ }]
1043
1185
  },
1044
1186
  {
1045
1187
  key: "qwen3-coder",
@@ -1198,6 +1340,7 @@ const MODEL_KEY_ALIASES = {
1198
1340
  "glm-4.7": "glm-4.7",
1199
1341
  "glm-5-turbo": "glm-5-turbo",
1200
1342
  "glm-5.1": "glm-5.1",
1343
+ "glm-5.3-flash": "glm-5.3-flash",
1201
1344
  "glm-5v-turbo": "glm-5v-turbo",
1202
1345
  "glm-5v.1": "glm-5v-turbo",
1203
1346
  "claude-opus-4-6": "claude-opus-4-6",
@@ -1235,6 +1378,7 @@ const MODEL_KEY_ALIASES = {
1235
1378
  "doubao-seed-2.1-turbo": "doubao-seed-2.1-turbo",
1236
1379
  "doubao-seed-2-1-turbo": "doubao-seed-2.1-turbo",
1237
1380
  "qwen3.8-max": "qwen-3.8-max",
1381
+ "qwen3.8-flash": "qwen-3.8-flash",
1238
1382
  "qwen3.7-max": "qwen-max",
1239
1383
  "qwen3.6-max": "qwen3.6-max",
1240
1384
  "qwen3.6-max-preview": "qwen3.6-max",
@@ -1379,6 +1523,42 @@ function isPriced(key) {
1379
1523
  return FALLBACK_RATES.some((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
1380
1524
  }
1381
1525
  /**
1526
+ * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
1527
+ * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
1528
+ * 导出供测试:纯函数。
1529
+ * @param promo - 待判定的促销窗口。
1530
+ * @param nowMs - 判定时刻(epoch ms)。
1531
+ */
1532
+ function isPromoActive(promo, nowMs) {
1533
+ const expired = promo.endsAtMs !== void 0 && nowMs >= promo.endsAtMs;
1534
+ return Number.isFinite(nowMs) && !expired && promo.factor > 0 && promo.factor < 1;
1535
+ }
1536
+ /**
1537
+ * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 全部乘 factor 的
1538
+ * 副本,其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
1539
+ * 幂等由调用方保证——计价与费率表显示各自只折一次,勿对已折价副本重复应用。
1540
+ * @param entry - 目录条目(price 保持刊例价口径)。
1541
+ * @param nowMs - 判定时刻(epoch ms)。
1542
+ */
1543
+ function applyPromo(entry, nowMs) {
1544
+ const { promo } = entry;
1545
+ if (promo === void 0 || !isPromoActive(promo, nowMs)) return entry;
1546
+ const scaled = (band) => ({
1547
+ input: band.input * promo.factor,
1548
+ cacheHit: band.cacheHit * promo.factor,
1549
+ ...band.cacheMiss !== void 0 ? { cacheMiss: band.cacheMiss * promo.factor } : {},
1550
+ output: band.output * promo.factor
1551
+ });
1552
+ return {
1553
+ ...entry,
1554
+ price: {
1555
+ ...scaled(entry.price),
1556
+ currency: entry.price.currency,
1557
+ ...entry.price.offPeak !== void 0 ? { offPeak: scaled(entry.price.offPeak) } : {}
1558
+ }
1559
+ };
1560
+ }
1561
+ /**
1382
1562
  * Price one band's token usage in CNY. The stats `input` field is the TOTAL
1383
1563
  * prompt tokens (cacheHit + cacheMiss), so billing splits it: the cache-hit
1384
1564
  * share prices at the hit rate and the remaining share at the miss rate.
@@ -1407,15 +1587,17 @@ function priceBandCost(band, buckets, currency) {
1407
1587
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
1408
1588
  * @returns the estimated cost in CNY.
1409
1589
  */
1410
- function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE) {
1411
- const peak = priceBandCost(entry.price, buckets, entry.price.currency);
1412
- const off = entry.price.offPeak === void 0 ? peak : priceBandCost(entry.price.offPeak, buckets, entry.price.currency);
1590
+ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Date.now()) {
1591
+ const priced = applyPromo(entry, nowMs);
1592
+ const peak = priceBandCost(priced.price, buckets, priced.price.currency);
1593
+ const off = priced.price.offPeak === void 0 ? peak : priceBandCost(priced.price.offPeak, buckets, priced.price.currency);
1413
1594
  return peak * peakShare + off * (1 - peakShare);
1414
1595
  }
1415
1596
  /**
1416
1597
  * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
1417
1598
  * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
1418
- * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。
1599
+ * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
1600
+ * 同口径:按事件时刻判定该笔流量当时享受的单价。
1419
1601
  * @param entry - the catalog entry whose prices apply.
1420
1602
  * @param buckets - token usage counts.
1421
1603
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
@@ -1423,9 +1605,10 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE) {
1423
1605
  * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
1424
1606
  */
1425
1607
  function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
1426
- if (entry.price.offPeak === void 0) return priceBandCost(entry.price, buckets, entry.price.currency);
1427
1608
  if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
1428
- return priceBandCost(tierAt(timeMs) === "peak" ? entry.price : entry.price.offPeak, buckets, entry.price.currency);
1609
+ const priced = applyPromo(entry, timeMs);
1610
+ if (priced.price.offPeak === void 0) return priceBandCost(priced.price, buckets, priced.price.currency);
1611
+ return priceBandCost(tierAt(timeMs) === "peak" ? priced.price : priced.price.offPeak, buckets, priced.price.currency);
1429
1612
  }
1430
1613
  /**
1431
1614
  * Format an amount with adaptive precision and the given currency symbol.
@@ -4407,6 +4590,7 @@ const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z
4407
4590
  const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
4408
4591
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
4409
4592
  const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
4593
+ const BALANCE_CACHE_MS = 300 * 1e3;
4410
4594
  /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
4411
4595
  const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
4412
4596
  /**
@@ -4890,6 +5074,10 @@ function apply(ctx, config = {}) {
4890
5074
  res.end(JSON.stringify(live));
4891
5075
  }
4892
5076
  }), "usage-billing: pricing route");
5077
+ let balanceCache = {
5078
+ at: 0,
5079
+ doc: { balances: [] }
5080
+ };
4893
5081
  ctx.effect(() => ctx.webServer.register({
4894
5082
  kind: "exact",
4895
5083
  path: "/api/billing/balance",
@@ -4903,39 +5091,53 @@ function apply(ctx, config = {}) {
4903
5091
  ...providers["deepseek"],
4904
5092
  apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
4905
5093
  };
4906
- const balances = await queryBalances(ctx, providers);
4907
- const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
4908
- const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
4909
- for (const row of [
4910
- ...balances,
4911
- ...custom,
4912
- ...declared
4913
- ]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
4914
- const official = balances.find((row) => row.provider === "deepseek");
4915
- let reconcile;
4916
- if (official !== void 0 && official.totalBalance !== void 0) {
4917
- const now = Date.now();
4918
- const today = dayStamp(now);
4919
- let todayOfficialCost = 0;
4920
- try {
4921
- todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
4922
- } catch {}
4923
- const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
4924
- reconcileRef = result.ref;
4925
- if (result.event !== null && result.event.kind !== "flat") reconcile = {
4926
- ...result.event,
4927
- provider: official.displayName
4928
- };
4929
- if (result.ref !== null) persistReconcileRef();
5094
+ if (Date.now() - balanceCache.at < BALANCE_CACHE_MS) {
5095
+ res.end(JSON.stringify(balanceCache.doc));
5096
+ return;
4930
5097
  }
4931
- res.end(JSON.stringify({
4932
- balances: [
5098
+ try {
5099
+ const balances = await queryBalances(ctx, providers);
5100
+ const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
5101
+ const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
5102
+ for (const row of [
4933
5103
  ...balances,
4934
5104
  ...custom,
4935
5105
  ...declared
4936
- ],
4937
- ...reconcile === void 0 ? {} : { reconcile }
4938
- }));
5106
+ ]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
5107
+ const official = balances.find((row) => row.provider === "deepseek");
5108
+ let reconcile;
5109
+ if (official !== void 0 && official.totalBalance !== void 0) {
5110
+ const now = Date.now();
5111
+ const today = dayStamp(now);
5112
+ let todayOfficialCost = 0;
5113
+ try {
5114
+ todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
5115
+ } catch {}
5116
+ const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
5117
+ reconcileRef = result.ref;
5118
+ if (result.event !== null && result.event.kind !== "flat") reconcile = {
5119
+ ...result.event,
5120
+ provider: official.displayName
5121
+ };
5122
+ if (result.ref !== null) persistReconcileRef();
5123
+ }
5124
+ const doc = {
5125
+ balances: [
5126
+ ...balances,
5127
+ ...custom,
5128
+ ...declared
5129
+ ],
5130
+ ...reconcile === void 0 ? {} : { reconcile }
5131
+ };
5132
+ balanceCache = {
5133
+ at: Date.now(),
5134
+ doc
5135
+ };
5136
+ res.end(JSON.stringify(doc));
5137
+ } catch (error) {
5138
+ console.warn("[usage-billing] refreshBalances failed; serving cached balances:", error);
5139
+ res.end(JSON.stringify(balanceCache.doc));
5140
+ }
4939
5141
  }
4940
5142
  }), "usage-billing: balance route");
4941
5143
  ctx.effect(() => ctx.webServer.register({
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.budgetAmount' | 'billing.budgetSummary' | 'billing.sessions' | 'billing.sessionTitle' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.footer' | 'billing.footerCredit' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | 'billing.peakBand' | 'billing.pricingSource' | 'billing.noteCache' | 'billing.noteBand' | 'billing.noteSource' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatMode' | 'billing.floatTargets' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.roundsHint' | 'billing.anomaly' | 'billing.workspaces' | 'billing.workspacesHint' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.thModel' | 'billing.thInputMiss' | 'billing.thInputHit' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPos' | 'billing.peakAlertMode' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.budgetAmount' | 'billing.budgetSummary' | 'billing.sessions' | 'billing.sessionTitle' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.footer' | 'billing.footerCredit' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.promoBadge' | 'billing.promoUntil' | 'billing.promoOpenEnded' | 'billing.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | 'billing.peakBand' | 'billing.pricingSource' | 'billing.noteCache' | 'billing.noteBand' | 'billing.noteSource' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatMode' | 'billing.floatTargets' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.roundsHint' | 'billing.anomaly' | 'billing.workspaces' | 'billing.workspacesHint' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.thModel' | 'billing.thInputMiss' | 'billing.thInputHit' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPos' | 'billing.peakAlertMode' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -12,6 +12,10 @@
12
12
  * (weekdays 09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate —
13
13
  * weekends (Sat/Sun, Beijing) are charged at the off-peak rate all day.
14
14
  * The estimator mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
15
+ *
16
+ * Time-limited launch promos ({@link PricePromo}) never mutate the catalog:
17
+ * entries keep list price and a promo window; the estimator and the rate
18
+ * table apply the discount factor until the deadline, then auto-revert.
15
19
  */
16
20
  import type { LivePricing } from '../pricing-shared.ts';
17
21
  /**
@@ -124,6 +128,36 @@ export interface ModelPrice extends PriceBand {
124
128
  /** Off-peak band (Gemini Flex / DeepSeek 低谷档); absent = flat pricing. */
125
129
  offPeak?: PriceBand;
126
130
  }
131
+ /**
132
+ * 限时促销窗口(新模型上线折扣等厂商营销活动):生效期内该条目所有档位
133
+ * (主档与 offPeak)单价按 factor 折扣计价与显示,截止时刻起自动恢复刊例价。
134
+ */
135
+ export interface PricePromo {
136
+ /** 折扣系数(0.5 = 五折);仅 (0,1) 区间有效,非法值视为无促销。 */
137
+ factor: number;
138
+ /**
139
+ * 截止时刻(epoch ms):该时刻及之后恢复刊例价。缺省表示厂商未公布截止日
140
+ * 的长期活动(如「限时 5 折直至另行通知」),持续生效直至收到公告后补填。
141
+ */
142
+ endsAtMs?: number;
143
+ /** 展示备注(如「限时 5 折至 …」),供界面提示活动性质。 */
144
+ note?: string;
145
+ }
146
+ /**
147
+ * 附加计价行(纯展示参考价):承载主三桶之外的厂商计价维度,如 Batch
148
+ * 半价档、显式缓存创建/命中等。不参与估算计费——用量统计源只有
149
+ * input/cacheHit/cacheMiss/output 四桶,无 batch 与显式缓存维度可区分。
150
+ */
151
+ export interface PriceRow {
152
+ /** 行标签(沿用目录单语风格,直接中文)。 */
153
+ label: string;
154
+ /** 输入侧单价(元或美元 / 每百万 token);缺省显示 —。 */
155
+ input?: number;
156
+ /** 输出侧单价;缺省显示 —。 */
157
+ output?: number;
158
+ /** 补充说明(如与标准价的关系)。 */
159
+ note?: string;
160
+ }
127
161
  /** One catalog entry: identity, brand color token, and price. */
128
162
  export interface ModelEntry {
129
163
  /** Model key used by `.dsh-usage-stats.json` `byModel`. */
@@ -138,6 +172,13 @@ export interface ModelEntry {
138
172
  price: ModelPrice;
139
173
  /** Peak-hour window label for time-of-day priced models. */
140
174
  peakHours?: string;
175
+ /**
176
+ * 限时促销:生效期内 price 各档位按 factor 打折,过期自动恢复。
177
+ * price 表本身永远保存刊例价,促销只在计价/显示出口处折算,不回写目录。
178
+ */
179
+ promo?: PricePromo;
180
+ /** 附加计价行(Batch / 显式缓存等展示性参考价),费率表在该模型行下方列出。 */
181
+ extraRows?: readonly PriceRow[];
141
182
  /**
142
183
  * 单价为估算价:厂商未公布按量官方单价(公测 / 套餐制),表内价格为估算,
143
184
  * 展示时标注以免误当正式定价;正式定价公布后移除。
@@ -193,12 +234,30 @@ export declare function modelOf(key: string): ModelEntry;
193
234
  * 聚合层的计价闸门(目录外模型不产生费用,避免兜底档误估)。
194
235
  */
195
236
  export declare function isPriced(key: string): boolean;
237
+ /**
238
+ * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
239
+ * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
240
+ * 导出供测试:纯函数。
241
+ * @param promo - 待判定的促销窗口。
242
+ * @param nowMs - 判定时刻(epoch ms)。
243
+ */
244
+ export declare function isPromoActive(promo: PricePromo, nowMs: number): boolean;
245
+ /**
246
+ * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 全部乘 factor 的
247
+ * 副本,其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
248
+ * 幂等由调用方保证——计价与费率表显示各自只折一次,勿对已折价副本重复应用。
249
+ * @param entry - 目录条目(price 保持刊例价口径)。
250
+ * @param nowMs - 判定时刻(epoch ms)。
251
+ */
252
+ export declare function applyPromo(entry: ModelEntry, nowMs: number): ModelEntry;
196
253
  /**
197
254
  * 费率表渲染的完整目录:内置 + models.dev 补充条目 + 探活模型(无价标记)。
198
- * 探活模型去重(按归一化 id):内置/补充已有的不再重复;无价的保留并标记
199
- * `uncatalogued`,费率表据此显示「未收录」。
255
+ * 内置条目按 nowMs 折算限时促销(生效中的条目显示折后单价,过期自动恢复
256
+ * 刊例价)。探活模型去重(按归一化 id):内置/补充已有的不再重复;无价的
257
+ * 保留并标记 `uncatalogued`,费率表据此显示「未收录」。
258
+ * @param nowMs - 促销判定时刻;缺省当前时刻。
200
259
  */
201
- export declare function catalogEntries(): readonly ModelEntry[];
260
+ export declare function catalogEntries(nowMs?: number): readonly ModelEntry[];
202
261
  /** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
203
262
  export declare function resolveToken(name: string): string;
204
263
  /**
@@ -216,11 +275,12 @@ export declare function resolveToken(name: string): string;
216
275
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
217
276
  * @returns the estimated cost in CNY.
218
277
  */
219
- export declare function computeCost(entry: ModelEntry, buckets: TokenUsageBuckets, peakShare?: number): number;
278
+ export declare function computeCost(entry: ModelEntry, buckets: TokenUsageBuckets, peakShare?: number, nowMs?: number): number;
220
279
  /**
221
280
  * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
222
281
  * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
223
- * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。
282
+ * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
283
+ * 同口径:按事件时刻判定该笔流量当时享受的单价。
224
284
  * @param entry - the catalog entry whose prices apply.
225
285
  * @param buckets - token usage counts.
226
286
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },