@gamegeek-saikel/dsh-cost-meter 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -43,7 +43,7 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
43
43
  |---|---|
44
44
  | Cost anchoring | Append-only pricebook snapshots; step cost computed once at the event's own time |
45
45
  | Price sources | Manual override > official pricing page > built-in fallback > OpenRouter (fallback only, USD→CNY) > none |
46
- | Peak pricing | 2026-08-17 00:00 Beijing rollout; peak 09:00–12:00 / 14:00–18:00 Beijing, off-peak half price |
46
+ | Peak pricing | 2026-08-17 00:00 Beijing rollout; peak/off-peak windows parsed from each page's own schedule (the zh and en pages may differ; fallback 09:00–12:00 / 14:00–18:00 Beijing), off-peak half price |
47
47
  | Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
48
48
  | Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
49
49
  | Subagent support | BFS over the live agent tree; conversation totals = main session + descendants |
@@ -71,7 +71,7 @@ The pricebook (`src/pricebook.ts`) is the durable price source, persisted on the
71
71
 
72
72
  - **Priority chain** — per canonical model key (`provider/model`, bare model, or the `flash`/`pro` pricing key for DeepSeek-family models): manual override > official page > built-in fallback > OpenRouter (fallback only, USD→CNY, cache reads at the configured discount) > none.
73
73
  - **Snapshot selection** — `snapshotForTime` picks the newest snapshot with `effectiveAt <= event time` (pre-install sessions anchor to the first snapshot once).
74
- - **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by Beijing time (peak 09:00–12:00 / 14:00–18:00, everything else off-peak).
74
+ - **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows, falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
75
75
  - **Immutable ledger** — the `sessionCost` projection (`src/session-cost-projection.ts`) folds `request/header` (model) and usage-carrying events into per-step rows; a second usage sample for the same (turn, step) replaces the first (same-step finalization, not a re-price), with O(1) incremental totals.
76
76
 
77
77
  ## Project Structure
package/README.zh-CN.md CHANGED
@@ -43,7 +43,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
43
43
  |---|---|
44
44
  | 成本锚定 | 只追加价格簿快照;步成本在事件自身时刻只计算一次 |
45
45
  | 价格来源 | 手动覆盖 > 官方价格页 > 内置回退 > OpenRouter(仅回退,USD→CNY)> 无 |
46
- | 峰值定价 | 2026-08-17 00:00 北京生效;高峰 09:00–12:00 / 14:00–18:00(北京),闲时为半价 |
46
+ | 峰值定价 | 2026-08-17 00:00 北京生效;高峰/闲时窗口按各页面自己的时段表解析(中英文页面可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),闲时为半价 |
47
47
  | 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
48
48
  | 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
49
49
  | 子代理支持 | 沿活跃代理树 BFS;对话总花费 = 主会话 + 全部后代 |
@@ -71,7 +71,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
71
71
 
72
72
  - **优先级链**——按规范模型键(`provider/model`、裸模型名,或 DeepSeek 系模型的 `flash`/`pro` 定价键):手动覆盖 > 官方页面 > 内置回退 > OpenRouter(仅回退,USD→CNY,缓存读按配置折扣)> 无。
73
73
  - **快照选取**——`snapshotForTime` 取 `effectiveAt <= 事件时间` 的最新快照(安装前的会话一次性锚到首个快照)。
74
- - **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按北京时间选档(高峰 09:00–12:00 / 14:00–18:00,其余闲时)。
74
+ - **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00,其余闲时)。每步的档位在折叠时锚定一次,逐回复卡片与回复小标签永远显示该轮对话当时被计价的档位,而不是查看时刻的档位。
75
75
  - **不可变账本**——`sessionCost` 投影(`src/session-cost-projection.ts`)把 `request/header`(模型)与携带用量的事件折叠为逐步记录;同一 (turn, step) 的第二次用量样本**替换**第一条(同一步终结,而非重新计价),总计以 O(1) 增量维护。
76
76
 
77
77
  ## 项目结构
package/lib/client.js CHANGED
@@ -155,6 +155,14 @@ window.__ModuleLoader__.load({
155
155
  //#region src/pricing.ts
156
156
  /** Official peak-pricing rollout: 2026-08-17 00:00 Beijing time (UTC+8). */
157
157
  const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
158
+ /**
159
+ * Built-in fallback zh peak schedule: the official announcement's windows in
160
+ * Beijing time (peak 09:00-12:00 / 14:00-18:00, everything else off-peak).
161
+ */
162
+ const PEAK_SCHEDULE_ZH = {
163
+ timezone: "Asia/Shanghai",
164
+ ranges: [[9, 12], [14, 18]]
165
+ };
158
166
  /** Built-in fallback USD prices from the English pricing page. */
159
167
  const FALLBACK_CURRENT_USD = {
160
168
  flash: {
@@ -170,20 +178,22 @@ window.__ModuleLoader__.load({
170
178
  };
171
179
  ({ ...FALLBACK_CURRENT_USD.flash }), { ...FALLBACK_CURRENT_USD.pro };
172
180
  /**
173
- * Whether the current moment is a peak-pricing hour in Beijing time:
174
- * 09:00-12:00 and 14:00-18:00 (peak); everything else is off-peak.
181
+ * Whether the given instant is a peak-pricing hour under one schedule: the
182
+ * zh fallback is Beijing time 09:00-12:00 and 14:00-18:00; each pricebook
183
+ * passes the schedule parsed from its own (zh or en) pricing page.
175
184
  * @param now - the instant to classify.
185
+ * @param schedule - the peak-hour schedule to classify against.
176
186
  * @returns true during peak hours.
177
187
  */
178
- function isPeakHour(now = /* @__PURE__ */ new Date()) {
188
+ function isPeakHour(now = /* @__PURE__ */ new Date(), schedule = PEAK_SCHEDULE_ZH) {
179
189
  const parts = new Intl.DateTimeFormat("en-US", {
180
- timeZone: "Asia/Shanghai",
190
+ timeZone: schedule.timezone,
181
191
  hour: "numeric",
182
192
  hour12: false
183
193
  }).formatToParts(now);
184
194
  const hour = Number(parts.find((part) => part.type === "hour")?.value);
185
195
  if (Number.isNaN(hour)) return false;
186
- return hour >= 9 && hour < 12 || hour >= 14 && hour < 18;
196
+ return schedule.ranges.some(([start, end]) => hour >= start && hour < end);
187
197
  }
188
198
  //#endregion
189
199
  //#region src/client/cost-math.ts
@@ -238,10 +248,18 @@ window.__ModuleLoader__.load({
238
248
  if (subagents === void 0 || subagents.length === 0) return main;
239
249
  return sumTotals([main, ...subagents.map((entry) => entry.totals)]);
240
250
  }
241
- /** The price band in effect at one instant (mirror of the host helper). */
242
- function bandForTime(time) {
251
+ /**
252
+ * The price band in effect at one instant (mirror of the host helper). The
253
+ * schedule defaults to the zh fallback; live readouts pass the schedule of
254
+ * the pricebook they display (the zh and en pages may state different
255
+ * windows).
256
+ * @param time - epoch millis.
257
+ * @param schedule - the peak-hour schedule to classify against.
258
+ * @returns the band.
259
+ */
260
+ function bandForTime(time, schedule = PEAK_SCHEDULE_ZH) {
243
261
  if (time < PEAK_PRICING_START_MS) return "single";
244
- return isPeakHour(new Date(time)) ? "peak" : "offPeak";
262
+ return isPeakHour(new Date(time), schedule) ? "peak" : "offPeak";
245
263
  }
246
264
  /** The bucket of one model's entry for one band (mirror of the host helper). */
247
265
  function bucketOf(modelPrice, band) {
@@ -270,10 +288,10 @@ window.__ModuleLoader__.load({
270
288
  }
271
289
  }
272
290
  /** Resolve one model's bucket at one instant from a snapshot. */
273
- function bucketAt(snapshot, provider, model, time) {
291
+ function bucketAt(snapshot, provider, model, time, schedule = PEAK_SCHEDULE_ZH) {
274
292
  const entry = modelPriceOf(snapshot, provider, model);
275
293
  if (entry === void 0) return void 0;
276
- return bucketOf(entry, bandForTime(time));
294
+ return bucketOf(entry, bandForTime(time, schedule));
277
295
  }
278
296
  /**
279
297
  * The peak-to-off-peak price multiplier for one model's entry, when the
@@ -720,7 +738,7 @@ window.__ModuleLoader__.load({
720
738
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
721
739
  className: CostView_module_css_default.stepList,
722
740
  children: rows.map((step) => {
723
- const stepBand = bandForTime(Date.now());
741
+ const stepBand = step.band ?? bandForTime(step.time);
724
742
  const stepRatio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, step.provider, step.model);
725
743
  const stepBandLabel = stepBand === "peak" ? stepRatio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(stepRatio) }) : stepBand === "offPeak" ? stepRatio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / stepRatio) }) : null;
726
744
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
@@ -853,7 +871,7 @@ window.__ModuleLoader__.load({
853
871
  });
854
872
  }
855
873
  const amount = `${currencySymbol(currency)}${formatMoney(ledger.cost)}`;
856
- const band = bandForTime(Date.now());
874
+ const band = ledger.band ?? bandForTime(ledger.time);
857
875
  const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, ledger.provider, ledger.model);
858
876
  const bandLabel = band === "peak" ? ratio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(ratio) }) : band === "offPeak" ? ratio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / ratio) }) : null;
859
877
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -968,13 +986,14 @@ window.__ModuleLoader__.load({
968
986
  const projection = cost;
969
987
  const model = projection?.model ?? null;
970
988
  const snapshot = response?.pricebook.current ?? null;
989
+ const schedule = response?.pricebook.schedule;
971
990
  const subagents = response?.subagents;
972
991
  const combined = combineTotals(projection?.totals, subagents);
973
992
  const subSpend = subagentSpend(subagents);
974
993
  const symbol = currencySymbol(currency);
975
994
  const estimate = (0, react.useMemo)(() => {
976
995
  if (!running || projection === void 0) return null;
977
- const bucket = bucketAt(snapshot ?? void 0, model?.provider, model?.model ?? "unknown", Date.now());
996
+ const bucket = bucketAt(snapshot ?? void 0, model?.provider, model?.model ?? "unknown", Date.now(), schedule);
978
997
  if (bucket === void 0) return null;
979
998
  const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps), blocksOutputTokens(partial?.blocks));
980
999
  return (combined?.cost ?? 0) + stepEstimate;
@@ -983,6 +1002,7 @@ window.__ModuleLoader__.load({
983
1002
  projection,
984
1003
  combined,
985
1004
  snapshot,
1005
+ schedule,
986
1006
  model,
987
1007
  pressure,
988
1008
  partial,
@@ -994,7 +1014,7 @@ window.__ModuleLoader__.load({
994
1014
  const last = projection?.steps.length !== void 0 && projection.steps.length > 0 ? projection.steps[projection.steps.length - 1] : void 0;
995
1015
  const sourceKey = snapshot?.source ?? null;
996
1016
  const source = sourceKey === null ? t("source.none") : t(`source.${sourceKey}`);
997
- const band = bandForTime(Date.now());
1017
+ const band = bandForTime(Date.now(), schedule);
998
1018
  const peakRatio = peakOffPeakMultiplier(snapshot ?? void 0, model?.provider, model?.model ?? "unknown");
999
1019
  const bandLabel = band === "peak" ? peakRatio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(peakRatio) }) : band === "offPeak" ? peakRatio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / peakRatio) }) : null;
1000
1020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {