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

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), {
@@ -1758,6 +1761,28 @@ function foldUsage(acc, usage, key, subscription, timeMs, official = false) {
1758
1761
  if (official) acc.officialCost += thisCost;
1759
1762
  }
1760
1763
  }
1764
+ /**
1765
+ * 联网搜索辅助请求的单次费用估算默认值(人民币元)。DeepSeek 官方对搜索请求
1766
+ * (web_search 服务端工具注入上下文)照常计费,实测每次约 0.01~0.03 元,取中值;
1767
+ * 部署可在插件配置 `searchCallEstimateCny` 覆盖(设 0 关闭估算)。
1768
+ */
1769
+ const DEFAULT_SEARCH_CALL_ESTIMATE_CNY = .02;
1770
+ /**
1771
+ * Fold one auxiliary web-search LLM request (issue #15) into an accumulator.
1772
+ * 这类调用绕过对话通道直连官方端点,日志只记请求(无响应/用量事件),token
1773
+ * 不可知:按「每次估值」计入费用并单独累计 `searchCalls`,不产生 token 维度。
1774
+ * @param acc - the accumulator to mutate.
1775
+ * @param estimateCny - per-call cost estimate in CNY; 0 disables the estimate.
1776
+ */
1777
+ function foldSearchCall(acc, estimateCny) {
1778
+ acc.calls += 1;
1779
+ acc.searchCalls = (acc.searchCalls ?? 0) + 1;
1780
+ acc.officialCalls += 1;
1781
+ if (estimateCny > 0) {
1782
+ acc.cost += estimateCny;
1783
+ acc.officialCost += estimateCny;
1784
+ }
1785
+ }
1761
1786
  /** Local-time date stamp (the host runs in the user's timezone). */
1762
1787
  function dayStamp(time) {
1763
1788
  const date = new Date(time);
@@ -1797,6 +1822,8 @@ function workspaceNameOf(cwd) {
1797
1822
  if (cwd === void 0 || cwd === "") return "—";
1798
1823
  return cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? "—";
1799
1824
  }
1825
+ /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
1826
+ const PERF_SPIKE_MS = 1e4;
1800
1827
  /**
1801
1828
  * 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
1802
1829
  * header 归因算法);此后新写入的行总带当前 {@link FOLD_VERSION}。
@@ -1839,6 +1866,8 @@ function serializeFold(fold) {
1839
1866
  byModel: Object.fromEntries(fold.byModel),
1840
1867
  byDay: Object.fromEntries(fold.byDay),
1841
1868
  byDayModels: Object.fromEntries([...fold.byDayModels].map(([day, models]) => [day, Object.fromEntries(models)])),
1869
+ byTier: Object.fromEntries(fold.byTier),
1870
+ byTool: Object.fromEntries(fold.byTool),
1842
1871
  bySite: Object.fromEntries(fold.bySite),
1843
1872
  unpricedModels: [...fold.unpricedModels],
1844
1873
  planCalls: Object.fromEntries(fold.planCalls),
@@ -1855,6 +1884,8 @@ function deserializeFold(fold) {
1855
1884
  byModel: new Map(Object.entries(fold.byModel)),
1856
1885
  byDay: new Map(Object.entries(fold.byDay)),
1857
1886
  byDayModels: new Map(Object.entries(fold.byDayModels).map(([day, models]) => [day, new Map(Object.entries(models))])),
1887
+ byTier: new Map(Object.entries(fold.byTier ?? {})),
1888
+ byTool: new Map(Object.entries(fold.byTool ?? {})),
1858
1889
  bySite: new Map(Object.entries(fold.bySite)),
1859
1890
  unpricedModels: new Set(fold.unpricedModels),
1860
1891
  planCalls: new Map(Object.entries(fold.planCalls)),
@@ -1937,9 +1968,11 @@ function turnState(turns, turn) {
1937
1968
  * @param subscriptionProviders - provider ids billed through subscription plans.
1938
1969
  * @param officialProviderIds - provider ids treated as the official DeepSeek channel
1939
1970
  * (default: any `deepseek`-prefixed id). Others count as third-party.
1971
+ * @param routes - 当前 provider 路由视图(中转站归组)。
1972
+ * @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
1940
1973
  * @returns the per-session fold (cached by the incremental aggregator).
1941
1974
  */
1942
- function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}) {
1975
+ function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}, searchCallEstimateCny = DEFAULT_SEARCH_CALL_ESTIMATE_CNY) {
1943
1976
  let seedBoundary = -1;
1944
1977
  for (const event of events) if (event.type === "session/end-seed" && typeof event.seq === "number" && Number.isFinite(event.seq)) seedBoundary = Math.max(seedBoundary, event.seq);
1945
1978
  if (seedBoundary >= 0) {
@@ -1950,6 +1983,8 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
1950
1983
  byModel: /* @__PURE__ */ new Map(),
1951
1984
  byDay: /* @__PURE__ */ new Map(),
1952
1985
  byDayModels: /* @__PURE__ */ new Map(),
1986
+ byTier: /* @__PURE__ */ new Map(),
1987
+ byTool: /* @__PURE__ */ new Map(),
1953
1988
  bySite: /* @__PURE__ */ new Map(),
1954
1989
  unpricedModels: /* @__PURE__ */ new Set(),
1955
1990
  planCalls: /* @__PURE__ */ new Map(),
@@ -1970,6 +2005,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
1970
2005
  const turns = /* @__PURE__ */ new Map();
1971
2006
  const steps = /* @__PURE__ */ new Map();
1972
2007
  let lastOpenStepKey;
2008
+ const toolSeen = /* @__PURE__ */ new Set();
1973
2009
  for (const event of events) {
1974
2010
  if (seedBoundary >= 0 && typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq < seedBoundary) continue;
1975
2011
  fold.lastActive = Math.max(fold.lastActive, event.time);
@@ -2019,6 +2055,21 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2019
2055
  }
2020
2056
  continue;
2021
2057
  }
2058
+ if (event.type === "web/deepseek-search-llm-request") {
2059
+ const model = event.data.body?.model;
2060
+ const modelKey = typeof model === "string" && model !== "" ? resolveCatalogKey(model) : "other";
2061
+ const day = dayStamp(event.time);
2062
+ foldSearchCall(fold.total, searchCallEstimateCny);
2063
+ foldSearchCall(usageCell(fold.byModel, modelKey), searchCallEstimateCny);
2064
+ foldSearchCall(usageCell(fold.byDay, day), searchCallEstimateCny);
2065
+ foldSearchCall(modelDayCell(fold.byDayModels, day, modelKey), searchCallEstimateCny);
2066
+ foldSearchCall(usageCell(fold.bySite, siteBucketKey({
2067
+ kind: "direct",
2068
+ provider: "deepseek"
2069
+ })), searchCallEstimateCny);
2070
+ foldSearchCall(usageCell(fold.byTier, tierAt(event.time)), searchCallEstimateCny);
2071
+ continue;
2072
+ }
2022
2073
  if (event.type === "assistant/chunk") {
2023
2074
  const data = event.data;
2024
2075
  const turn = data.turn;
@@ -2031,6 +2082,16 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2031
2082
  state.lastContentTime = event.time;
2032
2083
  }
2033
2084
  }
2085
+ if (chunk?.type === "tool-call-delta" && typeof turn === "number" && typeof step === "number") {
2086
+ const index = chunk.index;
2087
+ const name = chunk.name;
2088
+ const seenKey = `${turn}:${step}:${typeof index === "number" ? index : "-"}`;
2089
+ if (!toolSeen.has(seenKey)) {
2090
+ toolSeen.add(seenKey);
2091
+ const toolName = typeof name === "string" && name !== "" ? name : "unknown";
2092
+ fold.byTool.set(toolName, (fold.byTool.get(toolName) ?? 0) + 1);
2093
+ }
2094
+ }
2034
2095
  continue;
2035
2096
  }
2036
2097
  if (event.type !== "assistant/message") continue;
@@ -2051,6 +2112,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2051
2112
  foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official);
2052
2113
  foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official);
2053
2114
  foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official);
2115
+ foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official);
2054
2116
  if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
2055
2117
  const turn = event.data.turn ?? -1;
2056
2118
  const state = turnState(turns, turn);
@@ -2132,9 +2194,11 @@ function mergeUsageInto(acc, cell) {
2132
2194
  acc.reasoning += cell.reasoning;
2133
2195
  acc.cacheHit += cell.cacheHit;
2134
2196
  acc.cacheMiss += cell.cacheMiss;
2197
+ if (cell.cacheWrite !== void 0) acc.cacheWrite = (acc.cacheWrite ?? 0) + cell.cacheWrite;
2135
2198
  acc.cost += cell.cost;
2136
2199
  acc.officialCalls += cell.officialCalls;
2137
2200
  acc.officialCost += cell.officialCost;
2201
+ if (cell.searchCalls !== void 0) acc.searchCalls = (acc.searchCalls ?? 0) + cell.searchCalls;
2138
2202
  }
2139
2203
  /** 均值(数组非空时调用;空数组按 0 兜底)。 */
2140
2204
  function mean(values) {
@@ -2176,6 +2240,7 @@ function createUsageAggregator(persistence, options = {}) {
2176
2240
  let lastAt = 0;
2177
2241
  /** 每次聚合取最新的 provider 路由视图(中转站零配置发现);缺省按空处理(全部未知路由)。 */
2178
2242
  const routesOf = () => options.resolveRoutes?.() ?? {};
2243
+ const searchEstimate = options.searchCallEstimateCny ?? .02;
2179
2244
  const ensureLedgerLoaded = async () => {
2180
2245
  if (ledgerLoaded || options.ledger === void 0) return;
2181
2246
  ledgerLoaded = true;
@@ -2240,7 +2305,7 @@ function createUsageAggregator(persistence, options = {}) {
2240
2305
  const { events } = await persistence.readFrom(meta.id, 0);
2241
2306
  const after = await stampOf(meta);
2242
2307
  if (stamp !== null && after !== stamp) continue;
2243
- const fold = foldSession(events, subscriptionProviders, officialProviderIds, routesOf());
2308
+ const fold = foldSession(events, subscriptionProviders, officialProviderIds, routesOf(), searchEstimate);
2244
2309
  cache.set(id, {
2245
2310
  stamp,
2246
2311
  fold
@@ -2256,7 +2321,7 @@ function createUsageAggregator(persistence, options = {}) {
2256
2321
  id,
2257
2322
  ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
2258
2323
  ...stamp === null ? {} : { stamp },
2259
- foldVersion: 2,
2324
+ foldVersion: 3,
2260
2325
  fold: serializeFold(fold)
2261
2326
  };
2262
2327
  const previous = ledger.get(id);
@@ -2280,7 +2345,7 @@ function createUsageAggregator(persistence, options = {}) {
2280
2345
  for (const entry of ledger.values()) {
2281
2346
  if (included.has(entry.id)) continue;
2282
2347
  try {
2283
- const stale = (entry.foldVersion ?? 1) < 2;
2348
+ const stale = (entry.foldVersion ?? 1) < 3;
2284
2349
  folds.push({
2285
2350
  id: entry.id,
2286
2351
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -2310,6 +2375,8 @@ function createUsageAggregator(persistence, options = {}) {
2310
2375
  const byModel = /* @__PURE__ */ new Map();
2311
2376
  const byDay = /* @__PURE__ */ new Map();
2312
2377
  const byDayModels = /* @__PURE__ */ new Map();
2378
+ const byTier = /* @__PURE__ */ new Map();
2379
+ const byTool = /* @__PURE__ */ new Map();
2313
2380
  const bySite = /* @__PURE__ */ new Map();
2314
2381
  const unpricedModels = /* @__PURE__ */ new Set();
2315
2382
  const planCalls = /* @__PURE__ */ new Map();
@@ -2334,6 +2401,8 @@ function createUsageAggregator(persistence, options = {}) {
2334
2401
  for (const [day, cell] of fold.byDay) mergeUsageInto(usageCell(byDay, day), cell);
2335
2402
  for (const [day, models] of fold.byDayModels) for (const [modelKey, cell] of models) mergeUsageInto(modelDayCell(byDayModels, day, modelKey), cell);
2336
2403
  for (const [siteKey, cell] of fold.bySite) mergeUsageInto(usageCell(bySite, siteKey), cell);
2404
+ for (const [tierKey, cell] of fold.byTier) mergeUsageInto(usageCell(byTier, tierKey), cell);
2405
+ for (const [toolName, count] of fold.byTool) byTool.set(toolName, (byTool.get(toolName) ?? 0) + count);
2337
2406
  for (const id of fold.unpricedModels) unpricedModels.add(id);
2338
2407
  for (const [modelKey, count] of fold.planCalls) planCalls.set(modelKey, (planCalls.get(modelKey) ?? 0) + count);
2339
2408
  for (const sample of fold.perf) {
@@ -2410,6 +2479,8 @@ function createUsageAggregator(persistence, options = {}) {
2410
2479
  ttftAvg: mean(acc.ttfts),
2411
2480
  ttftP50: percentile(acc.ttfts, .5),
2412
2481
  ttftP90: percentile(acc.ttfts, .9),
2482
+ ttftMax: Math.max(...acc.ttfts),
2483
+ ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2413
2484
  ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) },
2414
2485
  latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2415
2486
  estimatedSamples: acc.estimated
@@ -2432,8 +2503,14 @@ function createUsageAggregator(persistence, options = {}) {
2432
2503
  bySession: sessionRows.slice(0, 100),
2433
2504
  byTurn: turnRows.slice(0, 200),
2434
2505
  byWorkspace: workspaces.slice(0, 100),
2506
+ ...byTier.size === 0 ? {} : { byTier: {
2507
+ peak: byTier.get("peak") ?? emptyUsage(),
2508
+ offPeak: byTier.get("offPeak") ?? emptyUsage()
2509
+ } },
2510
+ ...byTool.size === 0 ? {} : { byTool: Object.fromEntries([...byTool].sort((a, b) => b[1] - a[1])) },
2435
2511
  ...bySite.size === 0 ? {} : { bySite: toRecord(bySite) },
2436
2512
  ...unpricedModels.size === 0 ? {} : { unpricedModels: [...unpricedModels].sort() },
2513
+ ...searchEstimate > 0 ? { searchCallEstimateCny: searchEstimate } : {},
2437
2514
  ...perf === void 0 ? {} : { perf },
2438
2515
  ...staleLedgerSessions > 0 ? { staleLedgerSessions } : {},
2439
2516
  byRole: (() => {
@@ -4886,6 +4963,7 @@ function apply(ctx, config = {}) {
4886
4963
  ...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
4887
4964
  resolveRoutes: () => readPiAiProviderRoutes(ctx.settings),
4888
4965
  ...workspaceTitleResolver === void 0 ? {} : { resolveWorkspaceTitle: workspaceTitleResolver },
4966
+ ...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
4889
4967
  ledger: ledgerStore
4890
4968
  });
4891
4969
  const candidates = [
@@ -74,6 +74,9 @@ export interface AggregateOptions {
74
74
  /** 独立的持久用量账本。启用后,已经成功折叠过的会话即使随后从
75
75
  * sessionPersistence 中永久删除,也会继续计入累计用量。 */
76
76
  ledger?: UsageLedgerStore;
77
+ /** 联网搜索请求(`web/deepseek-search-llm-request`,无用量事件)的单次费用
78
+ * 估算(人民币元);默认 {@link DEFAULT_SEARCH_CALL_ESTIMATE_CNY},设 0 关闭。 */
79
+ searchCallEstimateCny?: number;
77
80
  }
78
81
  /** 每会话折叠缓存默认上限:超过则按 LRU 淘汰(P1-6 峰值内存治理)。 */
79
82
  export declare const DEFAULT_MAX_CACHE_SESSIONS = 400;
@@ -84,6 +87,11 @@ export interface ModelUsage {
84
87
  output: number;
85
88
  cacheHit: number;
86
89
  cacheMiss: number;
90
+ /**
91
+ * 显式缓存写入 token(部分厂商单独计价的 cache creation)——已包含在
92
+ * `cacheMiss` 内,单列供结构展示;旧快照缺失。
93
+ */
94
+ cacheWrite?: number;
87
95
  cost: number;
88
96
  /** 输出中的 reasoning(思考)token;已包含在 `output` 内,单列用于结构展示。 */
89
97
  reasoning: number;
@@ -93,6 +101,12 @@ export interface ModelUsage {
93
101
  officialCalls: number;
94
102
  /** 走官方渠道的费用(CNY);三方费用 = cost - officialCost。 */
95
103
  officialCost: number;
104
+ /**
105
+ * 联网搜索辅助请求的估算调用数(`web/deepseek-search-llm-request`,日志只有
106
+ * 请求无用量事件);已按每次 `searchCallEstimateCny` 估算计入 `cost`,不计
107
+ * token。旧快照缺失。
108
+ */
109
+ searchCalls?: number;
96
110
  }
97
111
  /** Zeroed usage accumulator. */
98
112
  export declare function emptyUsage(): ModelUsage;
@@ -108,6 +122,20 @@ export declare function emptyUsage(): ModelUsage;
108
122
  * @param official - whether the call went through the official DeepSeek channel (vs a third-party relay).
109
123
  */
110
124
  export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean): void;
125
+ /**
126
+ * 联网搜索辅助请求的单次费用估算默认值(人民币元)。DeepSeek 官方对搜索请求
127
+ * (web_search 服务端工具注入上下文)照常计费,实测每次约 0.01~0.03 元,取中值;
128
+ * 部署可在插件配置 `searchCallEstimateCny` 覆盖(设 0 关闭估算)。
129
+ */
130
+ export declare const DEFAULT_SEARCH_CALL_ESTIMATE_CNY = 0.02;
131
+ /**
132
+ * Fold one auxiliary web-search LLM request (issue #15) into an accumulator.
133
+ * 这类调用绕过对话通道直连官方端点,日志只记请求(无响应/用量事件),token
134
+ * 不可知:按「每次估值」计入费用并单独累计 `searchCalls`,不产生 token 维度。
135
+ * @param acc - the accumulator to mutate.
136
+ * @param estimateCny - per-call cost estimate in CNY; 0 disables the estimate.
137
+ */
138
+ export declare function foldSearchCall(acc: ModelUsage, estimateCny: number): void;
111
139
  /** Local-time date stamp (the host runs in the user's timezone). */
112
140
  export declare function dayStamp(time: number): string;
113
141
  /** Local-time hour stamp `YYYY-MM-DDTHH` — the performance series bucket key. */
@@ -147,6 +175,16 @@ export interface UsageStatsDocument {
147
175
  byDay: Record<string, ModelUsage>;
148
176
  /** 模型 × 日期 二维统计:趋势图按模型堆叠的输入([date][modelKey])。 */
149
177
  byDayModels: Record<string, Record<string, ModelUsage>>;
178
+ /**
179
+ * 峰谷分桶(真实判档):折叠时逐调用按 `tierAt(event.time)` 归入高峰/低谷桶,
180
+ * 峰谷占比与「挪谷省钱」据此展示,不再按比例估算。旧快照可能缺失。
181
+ */
182
+ byTier?: {
183
+ peak: ModelUsage;
184
+ offPeak: ModelUsage;
185
+ };
186
+ /** 工具调用次数排行(键 = 工具名,按调用数倒序);token 无法按工具归因,仅计次。旧快照可能缺失。 */
187
+ byTool?: Record<string, number>;
150
188
  /** 会话明细:按费用倒序,封顶 {@link SESSION_ROW_LIMIT} 行;旧快照可能缺失。 */
151
189
  bySession: SessionUsageRow[];
152
190
  /** 每轮费用明细:按起始时间倒序,封顶 {@link TURN_ROW_LIMIT} 行;旧快照可能缺失。 */
@@ -167,6 +205,8 @@ export interface UsageStatsDocument {
167
205
  byRole?: RoleCost;
168
206
  /** 不可计价的模型 id(未收录 / 无价,费用按 0 计);供面板提示用户自查与反馈。 */
169
207
  unpricedModels?: readonly string[];
208
+ /** 联网搜索请求的单次费用估算(人民币元,配置回显);0 或缺省 = 未启用估算。 */
209
+ searchCallEstimateCny?: number;
170
210
  /**
171
211
  * 性能指标(TTFT / 生成速度 / 总延迟)按模型与按小时聚合;旧快照可能缺失。
172
212
  * 口径:TTFT = request/header → 首个内容 chunk;生成速度 = 输出 token ÷ 生成时长;
@@ -201,6 +241,10 @@ export interface ModelPerf {
201
241
  ttftP50: number;
202
242
  /** 首字延时 P90(毫秒)。 */
203
243
  ttftP90: number;
244
+ /** 首字延时最大值(毫秒);定位偶发慢响应。 */
245
+ ttftMax: number;
246
+ /** 首字延时尖峰样本数(> 10s);定位服务端抖动。 */
247
+ ttftSpikes: number;
204
248
  /** 平均生成速度(tokens/s);生成了有效输出且时长可测时存在。 */
205
249
  tpsAvg?: number;
206
250
  /** 平均总延迟(首次请求 → 响应完成,毫秒)。 */
@@ -268,6 +312,8 @@ export declare const SESSION_ROW_LIMIT = 100;
268
312
  export declare const TURN_ROW_LIMIT = 200;
269
313
  /** 聚合文档的短 TTL(毫秒):合并密集轮询,TTL 内直接复用上次的合并结果。 */
270
314
  export declare const AGGREGATE_TTL_MS = 5000;
315
+ /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
316
+ export declare const PERF_SPIKE_MS = 10000;
271
317
  /** 单步性能样本(foldSession 的折叠产物;跨会话合并时按模型/小时再聚合)。 */
272
318
  export interface PerfSample {
273
319
  /** 计费目录键(模型;未收录模型原样保留)。 */
@@ -289,6 +335,10 @@ export interface SessionFold {
289
335
  byModel: Map<string, ModelUsage>;
290
336
  byDay: Map<string, ModelUsage>;
291
337
  byDayModels: Map<string, Map<string, ModelUsage>>;
338
+ /** 峰谷分桶:折叠时按调用时刻精确判档(tierAt),键 = 'peak' / 'offPeak'。 */
339
+ byTier: Map<string, ModelUsage>;
340
+ /** 工具调用次数(键 = 工具名;tool-call-delta 首见计数)。 */
341
+ byTool: Map<string, number>;
292
342
  /** 中转站归组:按 provider 路由归类到站点/直连/未知路由(key = {@link siteBucketKey})。 */
293
343
  bySite: Map<string, ModelUsage>;
294
344
  /** 不可计价模型 id(未收录/无价,且非订阅)集合;跨会话合并后输出给面板提示。 */
@@ -321,6 +371,9 @@ export interface SerializedSessionFold {
321
371
  byModel: Record<string, ModelUsage>;
322
372
  byDay: Record<string, ModelUsage>;
323
373
  byDayModels: Record<string, Record<string, ModelUsage>>;
374
+ /** 1.0.8 起新增;旧账本行缺失(合并时按空处理,不触发重折算)。 */
375
+ byTier?: Record<string, ModelUsage>;
376
+ byTool?: Record<string, number>;
324
377
  bySite: Record<string, ModelUsage>;
325
378
  unpricedModels: string[];
326
379
  planCalls: Record<string, number>;
@@ -353,10 +406,11 @@ export interface UsageLedgerDocument {
353
406
  /**
354
407
  * 折叠算法版本:归账语义变化时递增。v1 = 按 request/header 归账(稀疏 header 把
355
408
  * 两次 header 之间的用量串到上一个模型,订阅模型首当其冲,issue #14);v2 =
356
- * `assistant/message` 自带 source 归账(1.0.7 起)。持久账本行据此区分新旧算法:
357
- * 日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
409
+ * `assistant/message` 自带 source 归账(1.0.7 起);v3 = 联网搜索请求按次估算
410
+ * 计费(issue #15,1.0.9 起)——旧行缺 `searchCalls` 维度与搜索估算费用。
411
+ * 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
358
412
  */
359
- export declare const FOLD_VERSION = 2;
413
+ export declare const FOLD_VERSION = 3;
360
414
  /**
361
415
  * 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
362
416
  * 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
@@ -400,6 +454,8 @@ export declare function messageTextLength(message: unknown): number;
400
454
  * @param subscriptionProviders - provider ids billed through subscription plans.
401
455
  * @param officialProviderIds - provider ids treated as the official DeepSeek channel
402
456
  * (default: any `deepseek`-prefixed id). Others count as third-party.
457
+ * @param routes - 当前 provider 路由视图(中转站归组)。
458
+ * @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
403
459
  * @returns the per-session fold (cached by the incremental aggregator).
404
460
  */
405
461
  export declare function foldSession(events: readonly {
@@ -407,7 +463,7 @@ export declare function foldSession(events: readonly {
407
463
  time: number;
408
464
  data: never;
409
465
  seq?: number;
410
- }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>): SessionFold;
466
+ }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number): SessionFold;
411
467
  /**
412
468
  * 增量聚合器:按会话缓存折叠结果,用日志文件的 mtime+size 作失效键——
413
469
  * 日志没动的会话直接复用,只有写过的会话重新折叠;整份文档另有短 TTL
@@ -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,9 +138,13 @@ 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;
146
+ /** 联网搜索请求的估算调用数(已按次估值计入 cost);1.0.9 起新增,旧快照缺失。 */
147
+ searchCalls?: number;
147
148
  };
148
149
  byModel: Record<string, {
149
150
  calls: number;
@@ -178,6 +179,22 @@ export interface UsageStats {
178
179
  cacheMiss: number;
179
180
  cost: number;
180
181
  }>>;
182
+ /**
183
+ * 峰谷分桶(全量逐调用真实判档):1.0.8 起服务端按调用时刻精确归桶,
184
+ * 峰谷占比条优先用它(覆盖全部历史调用);旧快照缺失时回退逐轮估算。
185
+ */
186
+ byTier?: {
187
+ peak: {
188
+ cost: number;
189
+ calls: number;
190
+ };
191
+ offPeak: {
192
+ cost: number;
193
+ calls: number;
194
+ };
195
+ };
196
+ /** 工具调用次数排行(键 = 工具名,按次数倒序);旧快照缺失。 */
197
+ byTool?: Record<string, number>;
181
198
  /** 每轮费用明细(服务端聚合路径恒带);旧快照可能缺失。 */
182
199
  byTurn?: readonly {
183
200
  sessionId: string;
@@ -212,6 +229,8 @@ export interface UsageStats {
212
229
  }>;
213
230
  /** 不可计价模型 id(未收录/无价,费用按 0 计);旧快照可能缺失。 */
214
231
  unpricedModels?: readonly string[];
232
+ /** 联网搜索请求的单次费用估算(人民币元,配置回显);0 或缺省 = 未启用估算。 */
233
+ searchCallEstimateCny?: number;
215
234
  /** 按角色费用归因(估算口径:输出实测,输入按消息长度摊分);旧快照可能缺失。 */
216
235
  byRole?: {
217
236
  user: number;
@@ -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.searchEstimateHint' | 'billing.siteListDisplay' | 'billing.siteListDisplayHint' | '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,39 @@ 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
+ /**
60
+ * 中转站列表(中转站分布 / 中转站额度)的展示偏好(issue #17)。
61
+ * 纯 client 偏好,存 localStorage(不依赖 node 半区接口/设置 schema)。
62
+ */
63
+ export interface SiteListPrefs {
64
+ /** 隐藏「未知路由」(bySite 的 unknown 桶)与「未识别」类型的中转站占位条目;默认隐藏。 */
65
+ hideUnidentified: boolean;
66
+ }
67
+ /** 默认站点列表偏好:隐藏无参考价值的占位条目,净化账单列表。 */
68
+ export declare const DEFAULT_SITE_LIST_PREFS: SiteListPrefs;
69
+ /** localStorage key(与其他 `dsh.ui-usage-billing.*` 偏好同命名空间)。 */
70
+ export declare const SITE_LIST_STORAGE_KEY = "dsh.ui-usage-billing.sites";
71
+ /** 读取站点列表偏好(含损坏/缺失回退到默认)。仅在浏览器半区调用。 */
72
+ export declare function loadSiteListPrefs(): SiteListPrefs;
73
+ /** 写入站点列表偏好。失败静默(展示偏好非关键)。 */
74
+ export declare function saveSiteListPrefs(prefs: SiteListPrefs): void;
75
+ /** 用户自定义单价(与 client/pricing.ts 的 `UserPrice` 同形;此处不 import 以保持 node 半区无 client 依赖)。 */
76
+ export interface StoredUserPrice {
77
+ /** 未命中输入单价(元或美元 / 每百万 token)。 */
78
+ input: number;
79
+ /** 缓存命中输入单价。 */
80
+ cacheHit: number;
81
+ /** 输出单价。 */
82
+ output: number;
83
+ /** 计价币种;缺省 CNY。 */
84
+ currency?: 'CNY' | 'USD';
85
+ }
86
+ /** 自定义价表:计费目录键 → 单价。 */
87
+ export type UserPriceMap = Record<string, StoredUserPrice>;
88
+ /** localStorage key(与其他 `dsh.ui-usage-billing.*` 偏好同命名空间)。 */
89
+ export declare const USER_PRICES_STORAGE_KEY = "dsh.ui-usage-billing.prices";
90
+ /** 读取用户自定义价(写入侧已校验,这里只挡住手工改坏的非数字行)。仅在浏览器半区调用。 */
91
+ export declare function loadUserPrices(): UserPriceMap;
92
+ /** 写入用户自定义价。失败静默(展示偏好非关键)。 */
93
+ export declare function saveUserPrices(prices: UserPriceMap): void;
59
94
  //# sourceMappingURL=usage-billing-settings.d.ts.map