@kenz1117/dsh-ui-usage-billing 1.0.7 → 1.0.8

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
@@ -905,7 +905,8 @@ const MODEL_CATALOG = [
905
905
  output: 6
906
906
  }
907
907
  },
908
- peakHours: "Standard / Flex"
908
+ peakHours: "Standard / Flex",
909
+ tierSemantics: "latency"
909
910
  },
910
911
  {
911
912
  key: "gemini-flash",
@@ -923,7 +924,8 @@ const MODEL_CATALOG = [
923
924
  output: 3.75
924
925
  }
925
926
  },
926
- peakHours: "Standard / Flex"
927
+ peakHours: "Standard / Flex",
928
+ tierSemantics: "latency"
927
929
  },
928
930
  {
929
931
  key: "grok",
@@ -1746,6 +1748,7 @@ function foldUsage(acc, usage, key, subscription, timeMs, official = false) {
1746
1748
  acc.reasoning += usage.reasoningTokens ?? 0;
1747
1749
  acc.cacheHit += cacheHit;
1748
1750
  acc.cacheMiss += cacheMiss;
1751
+ if ((usage.cacheWriteTokens ?? 0) > 0) acc.cacheWrite = (acc.cacheWrite ?? 0) + (usage.cacheWriteTokens ?? 0);
1749
1752
  if (official) acc.officialCalls += 1;
1750
1753
  if (!subscription && isPriced(key)) {
1751
1754
  const thisCost = computeCostAt(modelOf(key), {
@@ -1797,6 +1800,8 @@ function workspaceNameOf(cwd) {
1797
1800
  if (cwd === void 0 || cwd === "") return "—";
1798
1801
  return cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? "—";
1799
1802
  }
1803
+ /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
1804
+ const PERF_SPIKE_MS = 1e4;
1800
1805
  /**
1801
1806
  * 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
1802
1807
  * header 归因算法);此后新写入的行总带当前 {@link FOLD_VERSION}。
@@ -1839,6 +1844,8 @@ function serializeFold(fold) {
1839
1844
  byModel: Object.fromEntries(fold.byModel),
1840
1845
  byDay: Object.fromEntries(fold.byDay),
1841
1846
  byDayModels: Object.fromEntries([...fold.byDayModels].map(([day, models]) => [day, Object.fromEntries(models)])),
1847
+ byTier: Object.fromEntries(fold.byTier),
1848
+ byTool: Object.fromEntries(fold.byTool),
1842
1849
  bySite: Object.fromEntries(fold.bySite),
1843
1850
  unpricedModels: [...fold.unpricedModels],
1844
1851
  planCalls: Object.fromEntries(fold.planCalls),
@@ -1855,6 +1862,8 @@ function deserializeFold(fold) {
1855
1862
  byModel: new Map(Object.entries(fold.byModel)),
1856
1863
  byDay: new Map(Object.entries(fold.byDay)),
1857
1864
  byDayModels: new Map(Object.entries(fold.byDayModels).map(([day, models]) => [day, new Map(Object.entries(models))])),
1865
+ byTier: new Map(Object.entries(fold.byTier ?? {})),
1866
+ byTool: new Map(Object.entries(fold.byTool ?? {})),
1858
1867
  bySite: new Map(Object.entries(fold.bySite)),
1859
1868
  unpricedModels: new Set(fold.unpricedModels),
1860
1869
  planCalls: new Map(Object.entries(fold.planCalls)),
@@ -1950,6 +1959,8 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
1950
1959
  byModel: /* @__PURE__ */ new Map(),
1951
1960
  byDay: /* @__PURE__ */ new Map(),
1952
1961
  byDayModels: /* @__PURE__ */ new Map(),
1962
+ byTier: /* @__PURE__ */ new Map(),
1963
+ byTool: /* @__PURE__ */ new Map(),
1953
1964
  bySite: /* @__PURE__ */ new Map(),
1954
1965
  unpricedModels: /* @__PURE__ */ new Set(),
1955
1966
  planCalls: /* @__PURE__ */ new Map(),
@@ -1970,6 +1981,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
1970
1981
  const turns = /* @__PURE__ */ new Map();
1971
1982
  const steps = /* @__PURE__ */ new Map();
1972
1983
  let lastOpenStepKey;
1984
+ const toolSeen = /* @__PURE__ */ new Set();
1973
1985
  for (const event of events) {
1974
1986
  if (seedBoundary >= 0 && typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq < seedBoundary) continue;
1975
1987
  fold.lastActive = Math.max(fold.lastActive, event.time);
@@ -2031,6 +2043,16 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2031
2043
  state.lastContentTime = event.time;
2032
2044
  }
2033
2045
  }
2046
+ if (chunk?.type === "tool-call-delta" && typeof turn === "number" && typeof step === "number") {
2047
+ const index = chunk.index;
2048
+ const name = chunk.name;
2049
+ const seenKey = `${turn}:${step}:${typeof index === "number" ? index : "-"}`;
2050
+ if (!toolSeen.has(seenKey)) {
2051
+ toolSeen.add(seenKey);
2052
+ const toolName = typeof name === "string" && name !== "" ? name : "unknown";
2053
+ fold.byTool.set(toolName, (fold.byTool.get(toolName) ?? 0) + 1);
2054
+ }
2055
+ }
2034
2056
  continue;
2035
2057
  }
2036
2058
  if (event.type !== "assistant/message") continue;
@@ -2051,6 +2073,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2051
2073
  foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official);
2052
2074
  foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official);
2053
2075
  foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official);
2076
+ foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official);
2054
2077
  if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
2055
2078
  const turn = event.data.turn ?? -1;
2056
2079
  const state = turnState(turns, turn);
@@ -2132,6 +2155,7 @@ function mergeUsageInto(acc, cell) {
2132
2155
  acc.reasoning += cell.reasoning;
2133
2156
  acc.cacheHit += cell.cacheHit;
2134
2157
  acc.cacheMiss += cell.cacheMiss;
2158
+ if (cell.cacheWrite !== void 0) acc.cacheWrite = (acc.cacheWrite ?? 0) + cell.cacheWrite;
2135
2159
  acc.cost += cell.cost;
2136
2160
  acc.officialCalls += cell.officialCalls;
2137
2161
  acc.officialCost += cell.officialCost;
@@ -2310,6 +2334,8 @@ function createUsageAggregator(persistence, options = {}) {
2310
2334
  const byModel = /* @__PURE__ */ new Map();
2311
2335
  const byDay = /* @__PURE__ */ new Map();
2312
2336
  const byDayModels = /* @__PURE__ */ new Map();
2337
+ const byTier = /* @__PURE__ */ new Map();
2338
+ const byTool = /* @__PURE__ */ new Map();
2313
2339
  const bySite = /* @__PURE__ */ new Map();
2314
2340
  const unpricedModels = /* @__PURE__ */ new Set();
2315
2341
  const planCalls = /* @__PURE__ */ new Map();
@@ -2334,6 +2360,8 @@ function createUsageAggregator(persistence, options = {}) {
2334
2360
  for (const [day, cell] of fold.byDay) mergeUsageInto(usageCell(byDay, day), cell);
2335
2361
  for (const [day, models] of fold.byDayModels) for (const [modelKey, cell] of models) mergeUsageInto(modelDayCell(byDayModels, day, modelKey), cell);
2336
2362
  for (const [siteKey, cell] of fold.bySite) mergeUsageInto(usageCell(bySite, siteKey), cell);
2363
+ for (const [tierKey, cell] of fold.byTier) mergeUsageInto(usageCell(byTier, tierKey), cell);
2364
+ for (const [toolName, count] of fold.byTool) byTool.set(toolName, (byTool.get(toolName) ?? 0) + count);
2337
2365
  for (const id of fold.unpricedModels) unpricedModels.add(id);
2338
2366
  for (const [modelKey, count] of fold.planCalls) planCalls.set(modelKey, (planCalls.get(modelKey) ?? 0) + count);
2339
2367
  for (const sample of fold.perf) {
@@ -2410,6 +2438,8 @@ function createUsageAggregator(persistence, options = {}) {
2410
2438
  ttftAvg: mean(acc.ttfts),
2411
2439
  ttftP50: percentile(acc.ttfts, .5),
2412
2440
  ttftP90: percentile(acc.ttfts, .9),
2441
+ ttftMax: Math.max(...acc.ttfts),
2442
+ ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2413
2443
  ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) },
2414
2444
  latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2415
2445
  estimatedSamples: acc.estimated
@@ -2432,6 +2462,11 @@ function createUsageAggregator(persistence, options = {}) {
2432
2462
  bySession: sessionRows.slice(0, 100),
2433
2463
  byTurn: turnRows.slice(0, 200),
2434
2464
  byWorkspace: workspaces.slice(0, 100),
2465
+ ...byTier.size === 0 ? {} : { byTier: {
2466
+ peak: byTier.get("peak") ?? emptyUsage(),
2467
+ offPeak: byTier.get("offPeak") ?? emptyUsage()
2468
+ } },
2469
+ ...byTool.size === 0 ? {} : { byTool: Object.fromEntries([...byTool].sort((a, b) => b[1] - a[1])) },
2435
2470
  ...bySite.size === 0 ? {} : { bySite: toRecord(bySite) },
2436
2471
  ...unpricedModels.size === 0 ? {} : { unpricedModels: [...unpricedModels].sort() },
2437
2472
  ...perf === void 0 ? {} : { perf },
@@ -84,6 +84,11 @@ export interface ModelUsage {
84
84
  output: number;
85
85
  cacheHit: number;
86
86
  cacheMiss: number;
87
+ /**
88
+ * 显式缓存写入 token(部分厂商单独计价的 cache creation)——已包含在
89
+ * `cacheMiss` 内,单列供结构展示;旧快照缺失。
90
+ */
91
+ cacheWrite?: number;
87
92
  cost: number;
88
93
  /** 输出中的 reasoning(思考)token;已包含在 `output` 内,单列用于结构展示。 */
89
94
  reasoning: number;
@@ -147,6 +152,16 @@ export interface UsageStatsDocument {
147
152
  byDay: Record<string, ModelUsage>;
148
153
  /** 模型 × 日期 二维统计:趋势图按模型堆叠的输入([date][modelKey])。 */
149
154
  byDayModels: Record<string, Record<string, ModelUsage>>;
155
+ /**
156
+ * 峰谷分桶(真实判档):折叠时逐调用按 `tierAt(event.time)` 归入高峰/低谷桶,
157
+ * 峰谷占比与「挪谷省钱」据此展示,不再按比例估算。旧快照可能缺失。
158
+ */
159
+ byTier?: {
160
+ peak: ModelUsage;
161
+ offPeak: ModelUsage;
162
+ };
163
+ /** 工具调用次数排行(键 = 工具名,按调用数倒序);token 无法按工具归因,仅计次。旧快照可能缺失。 */
164
+ byTool?: Record<string, number>;
150
165
  /** 会话明细:按费用倒序,封顶 {@link SESSION_ROW_LIMIT} 行;旧快照可能缺失。 */
151
166
  bySession: SessionUsageRow[];
152
167
  /** 每轮费用明细:按起始时间倒序,封顶 {@link TURN_ROW_LIMIT} 行;旧快照可能缺失。 */
@@ -201,6 +216,10 @@ export interface ModelPerf {
201
216
  ttftP50: number;
202
217
  /** 首字延时 P90(毫秒)。 */
203
218
  ttftP90: number;
219
+ /** 首字延时最大值(毫秒);定位偶发慢响应。 */
220
+ ttftMax: number;
221
+ /** 首字延时尖峰样本数(> 10s);定位服务端抖动。 */
222
+ ttftSpikes: number;
204
223
  /** 平均生成速度(tokens/s);生成了有效输出且时长可测时存在。 */
205
224
  tpsAvg?: number;
206
225
  /** 平均总延迟(首次请求 → 响应完成,毫秒)。 */
@@ -268,6 +287,8 @@ export declare const SESSION_ROW_LIMIT = 100;
268
287
  export declare const TURN_ROW_LIMIT = 200;
269
288
  /** 聚合文档的短 TTL(毫秒):合并密集轮询,TTL 内直接复用上次的合并结果。 */
270
289
  export declare const AGGREGATE_TTL_MS = 5000;
290
+ /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
291
+ export declare const PERF_SPIKE_MS = 10000;
271
292
  /** 单步性能样本(foldSession 的折叠产物;跨会话合并时按模型/小时再聚合)。 */
272
293
  export interface PerfSample {
273
294
  /** 计费目录键(模型;未收录模型原样保留)。 */
@@ -289,6 +310,10 @@ export interface SessionFold {
289
310
  byModel: Map<string, ModelUsage>;
290
311
  byDay: Map<string, ModelUsage>;
291
312
  byDayModels: Map<string, Map<string, ModelUsage>>;
313
+ /** 峰谷分桶:折叠时按调用时刻精确判档(tierAt),键 = 'peak' / 'offPeak'。 */
314
+ byTier: Map<string, ModelUsage>;
315
+ /** 工具调用次数(键 = 工具名;tool-call-delta 首见计数)。 */
316
+ byTool: Map<string, number>;
292
317
  /** 中转站归组:按 provider 路由归类到站点/直连/未知路由(key = {@link siteBucketKey})。 */
293
318
  bySite: Map<string, ModelUsage>;
294
319
  /** 不可计价模型 id(未收录/无价,且非订阅)集合;跨会话合并后输出给面板提示。 */
@@ -321,6 +346,9 @@ export interface SerializedSessionFold {
321
346
  byModel: Record<string, ModelUsage>;
322
347
  byDay: Record<string, ModelUsage>;
323
348
  byDayModels: Record<string, Record<string, ModelUsage>>;
349
+ /** 1.0.8 起新增;旧账本行缺失(合并时按空处理,不触发重折算)。 */
350
+ byTier?: Record<string, ModelUsage>;
351
+ byTool?: Record<string, number>;
324
352
  bySite: Record<string, ModelUsage>;
325
353
  unpricedModels: string[];
326
354
  planCalls: Record<string, number>;
@@ -16,6 +16,10 @@ export interface PerfModelData {
16
16
  ttftAvg: number;
17
17
  ttftP50: number;
18
18
  ttftP90: number;
19
+ /** 首字延时最大值(毫秒);1.0.8 起新增,旧快照缺失。 */
20
+ ttftMax?: number;
21
+ /** 首字延时尖峰样本数(> 10s);1.0.8 起新增,旧快照缺失。 */
22
+ ttftSpikes?: number;
19
23
  tpsAvg?: number;
20
24
  latencyAvg: number;
21
25
  estimatedSamples: number;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * TokenPanel: 「Token」分区——把 token 从费用里独立出来洞察。
3
- * 三个板块 + 导出,全部由 `UsageStats` 的 byDay/byModel/total 派生,服务端零改动:
3
+ * 四个板块 + 导出,全部由 `UsageStats` 派生,服务端零改动:
4
4
  * 1. 每日 Token 堆叠趋势(未命中输入 / 缓存命中 / 输出[含 reasoning]),7/30 天切换;
5
5
  * 2. 模型 Token 总量排行 + 占比;
6
- * 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)。
6
+ * 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)+ 显式缓存写入;
7
+ * 4. 工具调用排行(byTool 计次;token 无法按工具归因)。
7
8
  */
8
9
  import type { UsageBillingKey } from './locales.ts';
9
10
  import type { UsageStats } from './UsageBilling.tsx';
@@ -75,18 +75,15 @@ export declare function projectMonthCost(byDay: Record<string, {
75
75
  cost: number;
76
76
  }>, monthPrefix: string, today: string): number;
77
77
  /**
78
- * 峰谷时段费用分摊:按每轮的起始时刻(北京时间高峰 9-12 / 14-18)把费用
79
- * 归入高峰 / 空闲两档。导出供测试:纯函数。
80
- * @param turns - 每轮费用行(需带 startedAt cost)。
81
- * @returns 两档费用合计(人民币元)。
78
+ * 用户自定义单价显示重估:聚合发生在宿主进程(按内置目录计价),用户价只在
79
+ * 客户端显示层生效。对 byDayModels(day×model 完整二维)中命中用户价的模型
80
+ * 逐格平价重算 cost,并派生 byDay / byModel / total 的 cost。其余视图
81
+ * (bySite / byTier / bySession / byTurn)保持宿主原值——逐格时刻或行级归属
82
+ * 在客户端不可得,口径差异在设置面板注明。导出供测试:纯函数。
83
+ * @param stats - 服务端聚合文档。
84
+ * @returns 重估后的文档;无用户价或缺 byDayModels 时原样返回。
82
85
  */
83
- export declare function peakOffpeakCost(turns: readonly {
84
- startedAt: number;
85
- cost: number;
86
- }[]): {
87
- peak: number;
88
- offPeak: number;
89
- };
86
+ export declare function recostWithUserPrices(stats: UsageStats): UsageStats;
90
87
  /** 近 7 天费用序列(含今天,缺日补 0):触发卡 hover 速览的迷你柱数据源。
91
88
  * 导出供测试:纯函数(日期取本地时区)。 */
92
89
  export declare function activeDaysOf(byDay: Record<string, {
@@ -141,6 +138,8 @@ export interface UsageStats {
141
138
  output: number;
142
139
  cacheHit: number;
143
140
  cacheMiss: number;
141
+ /** 显式缓存写入(cacheMiss 子集,部分厂商单独报告);1.0.8 起新增,旧快照缺失。 */
142
+ cacheWrite?: number;
144
143
  cost: number;
145
144
  /** 输出中的 reasoning(思考)token;已含在 output 内。 */
146
145
  reasoning: number;
@@ -178,6 +177,22 @@ export interface UsageStats {
178
177
  cacheMiss: number;
179
178
  cost: number;
180
179
  }>>;
180
+ /**
181
+ * 峰谷分桶(全量逐调用真实判档):1.0.8 起服务端按调用时刻精确归桶,
182
+ * 峰谷占比条优先用它(覆盖全部历史调用);旧快照缺失时回退逐轮估算。
183
+ */
184
+ byTier?: {
185
+ peak: {
186
+ cost: number;
187
+ calls: number;
188
+ };
189
+ offPeak: {
190
+ cost: number;
191
+ calls: number;
192
+ };
193
+ };
194
+ /** 工具调用次数排行(键 = 工具名,按次数倒序);旧快照缺失。 */
195
+ byTool?: Record<string, number>;
181
196
  /** 每轮费用明细(服务端聚合路径恒带);旧快照可能缺失。 */
182
197
  byTurn?: readonly {
183
198
  sessionId: string;
@@ -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.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.cardDisplay' | 'billing.cardDisplayHint' | 'billing.cardMetric' | 'billing.cardMetricMoney' | 'billing.cardMetricTokens' | 'billing.triggerMonthTokens' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.staleLedgerNotice' | 'billing.sessionStaleBadge' | '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.ubStd' | '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.cardDisplay' | 'billing.cardDisplayHint' | 'billing.cardMetric' | 'billing.cardMetricMoney' | 'billing.cardMetricTokens' | 'billing.triggerMonthTokens' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.staleLedgerNotice' | 'billing.tokenCacheWrite' | 'billing.toolRank' | 'billing.toolName' | 'billing.userPrices' | 'billing.userPricesHint' | 'billing.userPriceSave' | 'billing.sessionStaleBadge' | '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.peakSharePerCall' | 'billing.offPeakSavings' | 'billing.perfMax' | '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>;
@@ -26,6 +26,32 @@ import type { LivePricing } from '../pricing-shared.ts';
26
26
  * live rate arrives the built-in value stays in force.
27
27
  */
28
28
  export declare const USD_TO_CNY = 6.79;
29
+ /**
30
+ * 用户自定义单价(设置面板录入,localStorage 持久化):覆盖内置/models.dev/
31
+ * dsh-spend 的全部价格来源,用于新模型上线目录未跟、或厂商未公布按量价的场景。
32
+ * 仅在客户端显示层生效——聚合发生在宿主进程,折叠时的成本仍按内置目录计算,
33
+ * 客户端检测到用户价后对受影响视图做显示重估(见 UsageBilling 的 recost)。
34
+ */
35
+ export interface UserPrice {
36
+ /** 未命中输入单价(元或美元 / 每百万 token)。 */
37
+ input: number;
38
+ /** 缓存命中输入单价。 */
39
+ cacheHit: number;
40
+ /** 输出单价。 */
41
+ output: number;
42
+ /** 计价币种;缺省 CNY。 */
43
+ currency?: 'CNY' | 'USD';
44
+ }
45
+ /**
46
+ * 注入用户自定义单价。键 = 计费目录键;查询侧用归一化两跳宽松命中。
47
+ * 空对象 = 清除全部自定义价,回退内置目录。
48
+ * @param prices - 目录键 → 自定义单价。
49
+ */
50
+ export declare function applyUserPrices(prices: Readonly<Record<string, UserPrice>>): void;
51
+ /** 当前生效的用户自定义单价(设置面板回显用);未设置时 undefined。 */
52
+ export declare function getUserPrices(): Readonly<Record<string, UserPrice>> | undefined;
53
+ /** 查一个计费键的用户自定义价(精确键 → 归一化键两跳)。 */
54
+ export declare function userPriceOf(key: string): UserPrice | undefined;
29
55
  /**
30
56
  * Apply the node half's live pricing snapshot. Absent fields keep the
31
57
  * built-in catalog and rate; callers never fabricate values.
@@ -158,6 +184,13 @@ export interface PriceRow {
158
184
  /** 补充说明(如与标准价的关系)。 */
159
185
  note?: string;
160
186
  }
187
+ /**
188
+ * 分档计价语义:厂商把「主档 / 低价档」的划分依据不同,界面需区分标注。
189
+ * - `timeOfDay`(缺省):按调用时刻分档(DeepSeek 峰谷时段),档位是客观的;
190
+ * - `latency`:按用户选择的延迟档分档(Gemini Standard/Flex,Flex 半价换 1-15
191
+ * 分钟延迟),与时刻无关;逐调用无法从日志判定实际档位,成本按比例估算。
192
+ */
193
+ export type TierSemantics = 'timeOfDay' | 'latency';
161
194
  /** One catalog entry: identity, brand color token, and price. */
162
195
  export interface ModelEntry {
163
196
  /** Model key used by `.dsh-usage-stats.json` `byModel`. */
@@ -172,6 +205,8 @@ export interface ModelEntry {
172
205
  price: ModelPrice;
173
206
  /** Peak-hour window label for time-of-day priced models. */
174
207
  peakHours?: string;
208
+ /** 分档语义;缺省 = 按时段(timeOfDay)。 */
209
+ tierSemantics?: TierSemantics;
175
210
  /**
176
211
  * 限时促销:生效期内 price 各档位按 factor 打折,过期自动恢复。
177
212
  * price 表本身永远保存刊例价,促销只在计价/显示出口处折算,不回写目录。
@@ -186,6 +221,8 @@ export interface ModelEntry {
186
221
  estimated?: boolean;
187
222
  /** 探活命中但无内置/models.dev 价:费率表标「未收录」,不参与计价。 */
188
223
  uncatalogued?: boolean;
224
+ /** 该条目当前按用户自定义单价计价(设置面板可维护);费率表标注「自定义」。 */
225
+ userPriced?: boolean;
189
226
  }
190
227
  /**
191
228
  * Built-in catalog of current mainstream models as of 2026-08-16, priced from
@@ -56,4 +56,23 @@ export declare const BILLING_CARD_STORAGE_KEY = "dsh.ui-usage-billing.card";
56
56
  export declare function loadBillingCardPrefs(): BillingCardPrefs;
57
57
  /** 写入计费卡偏好。失败静默(展示偏好非关键)。 */
58
58
  export declare function saveBillingCardPrefs(prefs: BillingCardPrefs): void;
59
+ /** 用户自定义单价(与 client/pricing.ts 的 `UserPrice` 同形;此处不 import 以保持 node 半区无 client 依赖)。 */
60
+ export interface StoredUserPrice {
61
+ /** 未命中输入单价(元或美元 / 每百万 token)。 */
62
+ input: number;
63
+ /** 缓存命中输入单价。 */
64
+ cacheHit: number;
65
+ /** 输出单价。 */
66
+ output: number;
67
+ /** 计价币种;缺省 CNY。 */
68
+ currency?: 'CNY' | 'USD';
69
+ }
70
+ /** 自定义价表:计费目录键 → 单价。 */
71
+ export type UserPriceMap = Record<string, StoredUserPrice>;
72
+ /** localStorage key(与其他 `dsh.ui-usage-billing.*` 偏好同命名空间)。 */
73
+ export declare const USER_PRICES_STORAGE_KEY = "dsh.ui-usage-billing.prices";
74
+ /** 读取用户自定义价(写入侧已校验,这里只挡住手工改坏的非数字行)。仅在浏览器半区调用。 */
75
+ export declare function loadUserPrices(): UserPriceMap;
76
+ /** 写入用户自定义价。失败静默(展示偏好非关键)。 */
77
+ export declare function saveUserPrices(prices: UserPriceMap): void;
59
78
  //# sourceMappingURL=usage-billing-settings.d.ts.map
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.7",
4
+ "version": "1.0.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },