dsh-usage-spend 0.1.0 → 0.2.0

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
@@ -1,14 +1,15 @@
1
1
  # Usage and spend (dsh-usage-spend)
2
2
 
3
- Shows the session's token usage and an **estimated spend** under the DSH composer, and appends the amount to the built-in per-turn usage action. Currency and exchange rate are configured on the **Plugins** page.
3
+ Shows the session's token usage and an **estimated spend** under the DSH composer, priced at DeepSeek's **peak and off-peak rates**, and appends the amount to the built-in per-turn usage action. Currency and exchange rate are configured on the **Plugins** page.
4
4
 
5
5
  One package is both the bundle (it declares the `usage-cost` row) and the feature itself, so installing it gives you a card on the Plugins page.
6
6
 
7
7
  ## What you get
8
8
 
9
- - **Composer usage pill**: `85.3M tok · ≈¥3.09 · cache hit 99.4%`, replacing the built-in `usage` row.
10
- - **Click the card**: the header shows the exact token count and amount, followed by buckets for cache hits, uncached input, cache reads, cache writes and output.
11
- - **Per-turn action**: the built-in `用量 85.3M tok` becomes `85.3M tok · ≈¥3.09`.
9
+ - **Composer usage pill**: `85.3M tok · ≈¥3.09 · cache hit 99.4%`, replacing the built-in `usage` row; a `峰` / `谷` badge after the amount says which window it was priced in.
10
+ - **Click the card**: the header shows the exact token count and amount, followed by buckets for cache hits, uncached input, cache reads, cache writes, output and the billing window.
11
+ - **Per-turn action**: the built-in `用量 85.3M tok` becomes `85.3M tok · ≈¥3.09`, priced at that turn's end time.
12
+ - **Peak pricing**: Beijing time weekdays 09:00–12:00 and 14:00–18:00 cost double; everything else, weekends and public holidays included, is off-peak.
12
13
  - **Plugins page settings**: currency (CNY / USD / both), exchange rate, and whether to show the sidebar balance, with a read-only price table below the form.
13
14
  - **Sidebar balance**: `余额 ¥12.34` at the bottom of the sidebar, refreshed every 60 seconds or on click; the grant is in the tooltip; collapsed the sidebar shows a compact amount. Can be turned off.
14
15
 
@@ -39,31 +40,47 @@ Turning the row off removes the pill, the per-turn amount and the sidebar balanc
39
40
 
40
41
  ## How the estimate works
41
42
 
42
- The price table is a models.dev snapshot of DeepSeek pricing (USD / 1M tokens):
43
+ Rates come from DeepSeek's official card (`api-docs.deepseek.com/zh-cn/quick_start/pricing`, checked 2026-11-07), in **CNY per 1M tokens**, and the table below is the **off-peak** rate:
43
44
 
44
- | Model | Input | Cache read | Cache write | Output |
45
- |---|---:|---:|---:|---:|
46
- | `deepseek-flash` / `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` | 0.15 | 0.003 | 0.15 | 0.60 |
47
- | `deepseek-v4-pro` | 0.66 | 0.022 | 0.66 | 1.98 |
48
- | `deepseek-chat` / `deepseek-reasoner` | 0.28 | 0.028 | 0.28 | 0.42 |
45
+ | Model | Input (cache miss) | Input (cache hit) | Output |
46
+ |---|---:|---:|---:|
47
+ | `deepseek-flash` (DeepSeek-V4.1-Flash-1017) | 1 | 0.02 | 4 |
48
+ | `deepseek-v4-pro` (DeepSeek-V4-Pro-0813) | 4.5 | 0.15 | 13.5 |
49
49
 
50
- - Cost = uncached input × input price + cache reads × cache-read price + cache writes × cache-write price + output × output price.
51
- - The CNY amount is converted with the configured rate, `7.2` by default.
52
- - Peak/off-peak multipliers, discounts, granted credit and the platform's actual bill are **not** included. This is an estimate only.
53
- - Per-turn amounts prefer the exact usage recorded in the session; when that is unavailable they sum the turn's assistant messages and show `≈`.
50
+ `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are retired names that still work: requests are served by V4.1-Flash and billed at the Flash price, so they share its rates. `deepseek-chat` and `deepseek-reasoner` were discontinued on 2026-07-24 and are no longer priced.
51
+
52
+ ### Peak and off-peak
53
+
54
+ DeepSeek charges half the peak price off-peak, so **peak = twice the table**.
55
+
56
+ - Peak: Beijing time, **Monday to Friday**, `09:00–12:00` and `14:00–18:00`
57
+ - Everything else is off-peak: nights, the lunch gap, **weekends** and **whole public holidays**
58
+ - Holidays follow the State Council schedule; the plugin ships the 2026 dates and needs one update a year
59
+
60
+ How the numbers are produced:
61
+
62
+ - Cost = (uncached input × input + cache reads × cache-read + cache writes × cache-write + output × output) / 1e6 × window multiplier
63
+ - Cache writes bill at the cache-miss input price, matching DeepSeek
64
+ - **Per-turn amounts use that turn's end time**, so they are exact
65
+ - **The session total uses the window in effect now**: the whole-log token projection carries no time dimension, so a session cannot be replayed turn by turn. Reopening an old session prices the total at the window you open it in while the per-turn amounts stay put
66
+ - The `峰` / `谷` badge on the pill shows which window the total used
67
+ - CNY is the base currency; USD is converted with the configured rate and only affects that column
68
+ - Discounts, granted credit, Search/Fetch fees and the platform's actual bill are **not** included. This is an estimate only.
54
69
 
55
70
  ## Custom prices
56
71
 
57
- Write `localStorage["dsh.cost.prices"]` from the browser console, keyed by model name (USD / 1M tokens):
72
+ Write `localStorage["dsh.cost.prices"]` from the browser console, keyed by model name (**CNY per 1M tokens, off-peak**; peak doubles it):
58
73
 
59
74
  ```json
60
75
  {
61
- "deepseek-flash": { "input": 0.15, "cacheRead": 0.003, "cacheWrite": 0.15, "output": 0.6 }
76
+ "deepseek-flash": { "input": 1, "cacheRead": 0.02, "cacheWrite": 1, "output": 4 }
62
77
  }
63
78
  ```
64
79
 
65
80
  Keys may be the full model name, `provider/model`, or the bare model name. Reload to apply.
66
81
 
82
+ > 0.1.0 overrides were USD; 0.2.0 switched to CNY and added the peak multiplier, so old overrides need re-entering in the new unit.
83
+
67
84
  ## Compatibility and limits
68
85
 
69
86
  - Targets the client slot / projection API of DSH `0.2.2-rc.1`.
package/README.zh.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # 用量与花费(dsh-usage-spend)
2
2
 
3
- 在 DSH 的输入框下方显示本次会话的 token 用量与**估算花费**,并把金额补到内置的每轮用量按钮上。币种和汇率在 **插件** 页配置。
3
+ 在 DSH 的输入框下方显示本次会话的 token 用量与**估算花费**,按 DeepSeek 官方的**峰谷价**计价,并把金额补到内置的每轮用量按钮上。币种和汇率在 **插件** 页配置。
4
4
 
5
5
  一个包同时是 bundle(声明 `usage-cost` 这一行)和功能本体,装上即出现插件页卡片。
6
6
 
7
7
  ## 功能
8
8
 
9
- - **底部用量 pill**:`85.3M tok · ≈¥3.09 · 缓存命中 99.4%`,替换内置的 `usage` 行。
10
- - **点击卡片**:标题行显示精确 token 数与金额,下面是缓存命中、未缓存输入、缓存读取、缓存写入、输出的分桶。
11
- - **每轮按钮**:把内置的 `用量 85.3M tok` 改成 `85.3M tok · ≈¥3.09`。
9
+ - **底部用量 pill**:`85.3M tok · ≈¥3.09 · 缓存命中 99.4%`,替换内置的 `usage` 行;金额后面带 `峰` / `谷` 小标签,标明这一笔按哪档价格算。
10
+ - **点击卡片**:标题行显示精确 token 数与金额,下面是缓存命中、未缓存输入、缓存读取、缓存写入、输出、计费时段。
11
+ - **每轮按钮**:把内置的 `用量 85.3M tok` 改成 `85.3M tok · ≈¥3.09`,金额按该轮结束时间判断峰谷。
12
+ - **峰谷计价**:北京时间工作日 9:00–12:00、14:00–18:00 为高峰(价目 ×2),其余时段含周末与法定节假日为空闲。
12
13
  - **插件页配置**:`币种`(人民币 / 美元 / 同时显示)、`汇率(1 USD = ? CNY)`、是否显示左侧栏余额,并在表单下方以只读表格展示当前单价表。
13
14
  - **左侧栏余额**:在侧栏底部显示 `余额 ¥12.34`,每 60 秒或点击时刷新;赠金放在 tooltip 里;折叠栏显示紧凑金额。可关闭。
14
15
 
@@ -39,31 +40,47 @@ dsh plugin --profile <profile> add dsh-usage-spend
39
40
 
40
41
  ## 估算口径
41
42
 
42
- 价格表是 models.dev 的 DeepSeek 快照(USD / 1M token):
43
+ 单价取自 DeepSeek 官方价目(`api-docs.deepseek.com/zh-cn/quick_start/pricing`,2026-11-07 核对),单位是**元 / 百万 token**,下表为**空闲时段**价格:
43
44
 
44
- | 模型 | 输入 | 缓存读 | 缓存写 | 输出 |
45
- |---|---:|---:|---:|---:|
46
- | `deepseek-flash` / `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` | 0.15 | 0.003 | 0.15 | 0.60 |
47
- | `deepseek-v4-pro` | 0.66 | 0.022 | 0.66 | 1.98 |
48
- | `deepseek-chat` / `deepseek-reasoner` | 0.28 | 0.028 | 0.28 | 0.42 |
45
+ | 模型 | 输入(缓存未命中) | 输入(缓存命中) | 输出 |
46
+ |---|---:|---:|---:|
47
+ | `deepseek-flash`(DeepSeek-V4.1-Flash-1017) | 1 | 0.02 | 4 |
48
+ | `deepseek-v4-pro`(DeepSeek-V4-Pro-0813) | 4.5 | 0.15 | 13.5 |
49
49
 
50
- - 费用 = 未缓存输入 × 输入价 + 缓存读取 × 缓存读价 + 缓存写入 × 缓存写价 + 输出 × 输出价。
51
- - 人民币金额按配置的汇率换算,默认 `7.2`。
52
- - **不包含**峰谷倍率、折扣、赠送额度或平台实际账单,只用于估算。
53
- - 每轮金额优先使用会话里的精确用量;精确用量不可得时按该轮 assistant 消息求和并显示 `≈`。
50
+ `deepseek-v4-flash` 与 `deepseek-v4-flash-vision-exp` 是**已下线但仍可调用**的旧模型名,请求由 V4.1-Flash 提供并按 Flash 价格计费,因此与 `deepseek-flash` 同价。`deepseek-chat` / `deepseek-reasoner` 已于 2026-07-24 停止服务,不再有价格。
51
+
52
+ ### 峰谷计价
53
+
54
+ 官方规则:**空闲时段价格是高峰时段的一半**,即高峰 = 表中价格 ×2。
55
+
56
+ - 高峰时段:北京时间**周一至周五** `9:00–12:00`、`14:00–18:00`
57
+ - 其余时段全部为空闲:夜间、午休、**周末**、**中国法定节假日全天**
58
+ - 节假日按国务院办公厅的放假安排,插件内置 2026 年日期(元旦、春节、清明、劳动节、端午、中秋、国庆);次年安排公布后需要更新一次
59
+
60
+ 计算方式:
61
+
62
+ - 费用 = (未缓存输入 × 输入价 + 缓存读取 × 缓存读价 + 缓存写入 × 缓存写价 + 输出 × 输出价)/ 1e6 × 时段倍率
63
+ - 缓存写入按缓存未命中的输入价计费,与官方一致
64
+ - **单轮金额按该轮结束时间**判断峰谷,是精确的
65
+ - **会话总额按当前时段**估算:整份 token 统计没有时间维度,无法回溯每一轮分别落在哪个时段。所以隔天回看老会话时,总额按你打开它时的时段算,而每轮按钮上的金额不变
66
+ - 底部 pill 上的 `峰` / `谷` 小标签就是当前用的哪档
67
+ - 币种默认人民币;选美元时按配置的汇率把人民币换算过去,汇率只影响美元那一栏
68
+ - **不包含**折扣、赠送额度、Search/Fetch 费用或平台实际账单,只用于估算
54
69
 
55
70
  ## 自定义价格
56
71
 
57
- 在浏览器控制台写入 `localStorage["dsh.cost.prices"]`,格式为「模型名 → USD / 1M token」:
72
+ 价格表可用浏览器 `localStorage` 覆盖,格式为「模型名 → **元** / 1M token」,填**空闲时段**价格(高峰会再乘 2):
58
73
 
59
74
  ```json
60
75
  {
61
- "deepseek-flash": { "input": 0.15, "cacheRead": 0.003, "cacheWrite": 0.15, "output": 0.6 }
76
+ "deepseek-flash": { "input": 1, "cacheRead": 0.02, "cacheWrite": 1, "output": 4 }
62
77
  }
63
78
  ```
64
79
 
65
80
  键可以是完整模型名、`provider/model` 或裸模型名;刷新页面生效。
66
81
 
82
+ > 0.1.0 的覆盖值是美元,0.2.0 起改为人民币并新增峰谷倍率,旧覆盖值需要按新单位重填。
83
+
67
84
  ## 兼容性与限制
68
85
 
69
86
  - 面向 DSH `0.2.2-rc.1` 的客户端 slot / projection API。
package/lib/client.js CHANGED
@@ -12,8 +12,9 @@
12
12
  * - `plugins.row.config` for this row, so the Plugins page exposes the
13
13
  * currency and exchange-rate settings.
14
14
  *
15
- * Pricing is a local USD / 1M token table (models.dev snapshot) with an
16
- * optional localStorage override. No network request is made for pricing.
15
+ * Pricing is a local CNY / 1M token table taken from DeepSeek's official card,
16
+ * with the peak-hours multiplier (Beijing time, weekdays, holiday calendar) and
17
+ * an optional localStorage override. No network request is made for pricing.
17
18
  */
18
19
  window.__ModuleLoader__.load({
19
20
  id: "dsh-usage-cost",
@@ -39,12 +40,17 @@ window.__ModuleLoader__.load({
39
40
  const zh = {
40
41
  "pill.tokens": "{count} tok",
41
42
  "pill.cacheHit": "缓存命中 {percent}%",
43
+ "pill.peak": "峰",
44
+ "pill.offPeak": "谷",
42
45
  "dialog.title": "Token 用量",
43
46
  "dialog.cacheHit": "缓存命中",
44
47
  "dialog.input": "未缓存输入",
45
48
  "dialog.cacheRead": "缓存读取",
46
49
  "dialog.cacheWrite": "缓存写入",
47
50
  "dialog.output": "输出",
51
+ "dialog.window": "计费时段",
52
+ "dialog.windowPeak": "高峰(×2)",
53
+ "dialog.windowOffPeak": "空闲",
48
54
  "currency.cny": "¥ 人民币",
49
55
  "currency.usd": "$ 美元",
50
56
  "currency.both": "¥+$",
@@ -52,9 +58,9 @@ window.__ModuleLoader__.load({
52
58
  "config.title": "用量与花费",
53
59
  "config.currency": "显示币种",
54
60
  "config.rate": "汇率:1 USD = ? CNY",
55
- "config.rateHint": "只影响人民币估算,默认 7.2。",
56
- "config.ratesTitle": "单价(USD / 1M tok)",
57
- "config.ratesNote": "价格表是 models.dev 的 DeepSeek 快照,费用仅用于估算。",
61
+ "config.rateHint": "只用于换算美元金额;人民币按官方价目直接计价。",
62
+ "config.ratesTitle": "单价(元 / 1M tok,空闲时段)",
63
+ "config.ratesNote": "官方价目,仅供估算。高峰时段为表中价格的两倍:北京时间周一至周五 9:00–12:00、14:00–18:00,法定节假日除外;其余时段(含周末与节假日全天)为空闲时段。",
58
64
  "config.model": "模型",
59
65
  "config.showBalance": "显示左侧栏余额",
60
66
  "config.showBalanceHint": "关闭后不显示余额 chip,也不会请求账号余额。",
@@ -77,12 +83,17 @@ window.__ModuleLoader__.load({
77
83
  const en = {
78
84
  "pill.tokens": "{count} tok",
79
85
  "pill.cacheHit": "cache hit {percent}%",
86
+ "pill.peak": "peak",
87
+ "pill.offPeak": "off-peak",
80
88
  "dialog.title": "Token usage",
81
89
  "dialog.cacheHit": "Cache hit",
82
90
  "dialog.input": "Uncached input",
83
91
  "dialog.cacheRead": "Cache read",
84
92
  "dialog.cacheWrite": "Cache write",
85
93
  "dialog.output": "Output",
94
+ "dialog.window": "Billing window",
95
+ "dialog.windowPeak": "Peak (×2)",
96
+ "dialog.windowOffPeak": "Off-peak",
86
97
  "currency.cny": "¥ CNY",
87
98
  "currency.usd": "$ USD",
88
99
  "currency.both": "¥+$",
@@ -90,9 +101,9 @@ window.__ModuleLoader__.load({
90
101
  "config.title": "Usage and spend",
91
102
  "config.currency": "Display currency",
92
103
  "config.rate": "Exchange rate: 1 USD = ? CNY",
93
- "config.rateHint": "Only affects the CNY estimate; default 7.2.",
94
- "config.ratesTitle": "Rates (USD / 1M tok)",
95
- "config.ratesNote": "Rates are the models.dev DeepSeek snapshot; spend is an estimate.",
104
+ "config.rateHint": "Only converts the USD figure; CNY comes straight from the official card.",
105
+ "config.ratesTitle": "Rates (CNY / 1M tok, off-peak)",
106
+ "config.ratesNote": "Official card, estimates only. Peak hours cost twice these rates: Beijing time Mon–Fri 09:00–12:00 and 14:00–18:00, excluding Chinese public holidays; everything else, weekends and holidays included, is off-peak.",
96
107
  "config.model": "Model",
97
108
  "config.showBalance": "Show the sidebar balance",
98
109
  "config.showBalanceHint": "When off, the balance chip is hidden and no balance request is made.",
@@ -124,6 +135,7 @@ window.__ModuleLoader__.load({
124
135
  ".dshuc_label{text-overflow:ellipsis;min-width:0;overflow:hidden}",
125
136
  ".dshuc_sep{color:var(--dsw-alias-separator-primary);margin:0 6px}",
126
137
  ".dshuc_money{color:var(--dsw-alias-label-secondary)}",
138
+ ".dshuc_window{color:var(--dsw-alias-label-tertiary);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xs,4px);padding:0 3px;margin-left:4px;font-size:10px;line-height:14px}",
127
139
  ".dshuc_panel{z-index:1100;box-sizing:border-box;width:max-content;min-width:min(300px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-width:min(440px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;max-height:var(--dsh-anchored-max-height,none);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;padding:16px;font-size:12px;line-height:18px;position:fixed;overflow:hidden auto}",
128
140
  ".dshuc_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
129
141
  ".dshuc_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}",
@@ -339,16 +351,83 @@ window.__ModuleLoader__.load({
339
351
  }
340
352
 
341
353
  // ----------------------------------------------------------------- prices
342
- /** USD per 1M tokens, models.dev snapshot (2026-11-07). */
354
+ /**
355
+ * CNY per 1M tokens, off-peak rates from the official card
356
+ * (api-docs.deepseek.com/zh-cn/quick_start/pricing, checked 2026-11-07).
357
+ * Peak hours cost twice these rates. Cache writes bill at the cache-miss
358
+ * input price, which is what DeepSeek charges for them.
359
+ */
343
360
  const MODEL_PRICES = {
344
- "deepseek-flash": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
345
- "deepseek-v4-flash": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
346
- "deepseek-v4-flash-vision-exp": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
347
- "deepseek-v4-pro": { input: 0.66, cacheRead: 0.022, cacheWrite: 0.66, output: 1.98 },
348
- "deepseek-chat": { input: 0.28, cacheRead: 0.028, cacheWrite: 0.28, output: 0.42 },
349
- "deepseek-reasoner": { input: 0.28, cacheRead: 0.028, cacheWrite: 0.28, output: 0.42 },
361
+ "deepseek-flash": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
362
+ // Retired names: still accepted by the API, served and billed as Flash.
363
+ "deepseek-v4-flash": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
364
+ "deepseek-v4-flash-vision-exp": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
365
+ "deepseek-v4-pro": { input: 4.5, cacheRead: 0.15, cacheWrite: 4.5, output: 13.5 },
350
366
  };
351
367
 
368
+ // ------------------------------------------------------------ peak hours
369
+ /**
370
+ * Peak = Beijing time, Monday to Friday, 09:00–12:00 and 14:00–18:00,
371
+ * excluding Chinese public holidays. Peak costs twice the off-peak rate.
372
+ * Everything else — nights, weekends, whole holiday days — is off-peak.
373
+ */
374
+ const PEAK_FACTOR = 2;
375
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
376
+ const PEAK_HOURS = [
377
+ [9, 12],
378
+ [14, 18],
379
+ ];
380
+ /** 国务院办公厅 2026 年放假安排;次年公布后追加即可。 */
381
+ const PUBLIC_HOLIDAY_RANGES = [
382
+ ["2026-01-01", "2026-01-03"], // 元旦
383
+ ["2026-02-15", "2026-02-23"], // 春节
384
+ ["2026-04-04", "2026-04-06"], // 清明
385
+ ["2026-05-01", "2026-05-05"], // 劳动节
386
+ ["2026-06-19", "2026-06-21"], // 端午
387
+ ["2026-09-25", "2026-09-27"], // 中秋
388
+ ["2026-10-01", "2026-10-07"], // 国庆
389
+ ];
390
+ const PUBLIC_HOLIDAYS = (() => {
391
+ const days = new Set();
392
+ for (const [from, to] of PUBLIC_HOLIDAY_RANGES) {
393
+ const last = Date.parse(to + "T00:00:00Z");
394
+ for (let at = Date.parse(from + "T00:00:00Z"); at <= last; at += 86_400_000) {
395
+ days.add(new Date(at).toISOString().slice(0, 10));
396
+ }
397
+ }
398
+ return days;
399
+ })();
400
+
401
+ /** Beijing wall-clock parts for an instant, independent of the host timezone. */
402
+ function beijingParts(at) {
403
+ const shifted = new Date(Number(at) + BEIJING_OFFSET_MS);
404
+ return {
405
+ date: shifted.toISOString().slice(0, 10),
406
+ weekday: shifted.getUTCDay(),
407
+ hour: shifted.getUTCHours() + shifted.getUTCMinutes() / 60,
408
+ };
409
+ }
410
+ /** True while DeepSeek bills this instant at the peak rate. */
411
+ function isPeak(at) {
412
+ const ms = Number(at);
413
+ if (!Number.isFinite(ms)) return false;
414
+ const { date, weekday, hour } = beijingParts(ms);
415
+ if (weekday === 0 || weekday === 6) return false;
416
+ if (PUBLIC_HOLIDAYS.has(date)) return false;
417
+ return PEAK_HOURS.some(([from, to]) => hour >= from && hour < to);
418
+ }
419
+ /** Multiplier applied to the off-peak card. */
420
+ function peakFactor(at) {
421
+ return isPeak(at) ? PEAK_FACTOR : 1;
422
+ }
423
+
424
+ /**
425
+ * Turn-end time for each per-turn usage object. The object is owned by the
426
+ * chat snapshot, so keying by it keeps the selector's identity stable
427
+ * while still telling the render which billing window to use.
428
+ */
429
+ const TURN_TIMES = new WeakMap();
430
+
352
431
  const PRICE_OVERRIDES = (() => {
353
432
  const raw = readStorage(PRICES_KEY);
354
433
  if (raw === null) return {};
@@ -403,15 +482,18 @@ window.__ModuleLoader__.load({
403
482
  function totalTokens(usage) {
404
483
  return billedInput(usage) + num(usage.outputTokens);
405
484
  }
406
- function costOf(usage, rates) {
485
+ /** CNY for this usage at the given off-peak rates, scaled by the peak factor. */
486
+ function costOf(usage, rates, factor) {
407
487
  if (rates === null) return null;
408
- const usd =
409
- (num(usage.uncachedInputTokens) * rates.input +
488
+ const scale = Number.isFinite(factor) && factor > 0 ? factor : 1;
489
+ const cny =
490
+ ((num(usage.uncachedInputTokens) * rates.input +
410
491
  num(usage.cacheReadTokens) * rates.cacheRead +
411
492
  num(usage.cacheWriteTokens) * rates.cacheWrite +
412
493
  num(usage.outputTokens) * rates.output) /
413
- 1e6;
414
- return Number.isFinite(usd) ? usd : null;
494
+ 1e6) *
495
+ scale;
496
+ return Number.isFinite(cny) ? cny : null;
415
497
  }
416
498
  function cacheHitPercent(usage) {
417
499
  const prompt = billedInput(usage);
@@ -438,14 +520,15 @@ window.__ModuleLoader__.load({
438
520
  if (value >= 0.01) return value.toFixed(3);
439
521
  return value.toFixed(4);
440
522
  }
441
- function moneyText(usd, currency, rate, spaced) {
442
- if (usd === null) return null;
523
+ function moneyText(cny, currency, rate, spaced) {
524
+ if (cny === null) return null;
443
525
  const gap = spaced === true ? " " : "";
444
- const cny = "≈" + gap + "¥" + amount(usd * (Number.isFinite(rate) && rate > 0 ? rate : readRate()));
445
- const dollar = "≈" + gap + "$" + amount(usd);
526
+ const perUsd = Number.isFinite(rate) && rate > 0 ? rate : readRate();
527
+ const yuan = "≈" + gap + "¥" + amount(cny);
528
+ const dollar = "≈" + gap + "$" + amount(cny / perUsd);
446
529
  if (currency === "USD") return dollar;
447
- if (currency === "BOTH") return cny + " / " + dollar;
448
- return cny;
530
+ if (currency === "BOTH") return yuan + " / " + dollar;
531
+ return yuan;
449
532
  }
450
533
 
451
534
  // --------------------------------------------------------------- balance
@@ -802,12 +885,21 @@ window.__ModuleLoader__.load({
802
885
 
803
886
  const route = selection?.pending ?? selection?.lastUsed ?? null;
804
887
  const priced = priceFor(route);
805
- const usd = priced === null ? null : costOf(usage, priced.rates);
888
+ const peak = isPeak(Date.now());
889
+ const cny = priced === null ? null : costOf(usage, priced.rates, peak ? PEAK_FACTOR : 1);
806
890
  const total = prompt + output;
807
891
  const cacheHit = cacheHitPercent(usage);
808
- const money = moneyText(usd, config.currency, config.usdToCny);
809
- const titleMoney = moneyText(usd, config.currency, config.usdToCny, true);
810
- const moneyNode = money === null ? null : h("span", { className: "dshuc_money" }, money);
892
+ const money = moneyText(cny, config.currency, config.usdToCny);
893
+ const titleMoney = moneyText(cny, config.currency, config.usdToCny, true);
894
+ const moneyNode =
895
+ money === null
896
+ ? null
897
+ : h(
898
+ "span",
899
+ { className: "dshuc_money" },
900
+ money,
901
+ h("span", { className: "dshuc_window" }, t(peak ? "pill.peak" : "pill.offPeak")),
902
+ );
811
903
  const cacheHitText = cacheHit === null ? null : t("pill.cacheHit", { percent: cacheHit });
812
904
 
813
905
  const segments = [t("pill.tokens", { count: formatTokens(total) })];
@@ -840,6 +932,7 @@ window.__ModuleLoader__.load({
840
932
  ? [t("dialog.cacheWrite"), exactCount(num(usage.cacheWriteTokens))]
841
933
  : null,
842
934
  [t("dialog.output"), exactCount(output)],
935
+ [t("dialog.window"), t(peak ? "dialog.windowPeak" : "dialog.windowOffPeak")],
843
936
  ].filter((row) => row !== null);
844
937
 
845
938
  return h(
@@ -883,7 +976,9 @@ window.__ModuleLoader__.load({
883
976
  let found;
884
977
  for (const node of nodes.values()) {
885
978
  if (node === null || typeof node !== "object" || node.kind !== "turn-tail") continue;
886
- if (node.data?.turn === turnNumber && node.data?.tokenUsage !== undefined) found = node.data.tokenUsage;
979
+ if (node.data?.turn !== turnNumber || node.data?.tokenUsage === undefined) continue;
980
+ found = node.data.tokenUsage;
981
+ if (typeof node.data.time === "number") TURN_TIMES.set(found, node.data.time);
887
982
  }
888
983
  let summed = null;
889
984
  if (Array.isArray(snapshot.legacy?.nodes)) {
@@ -926,8 +1021,11 @@ window.__ModuleLoader__.load({
926
1021
  const total = typeof usage.totalTokens === "number" ? usage.totalTokens : totalTokens(usage);
927
1022
  const route = usage.routes?.[0] ?? selection?.pending ?? selection?.lastUsed ?? null;
928
1023
  const priced = priceFor(route);
929
- const usd = priced === null ? null : costOf(usage, priced.rates);
930
- const money = moneyText(usd, config.currency, config.usdToCny);
1024
+ // The turn's own end time decides its window; the summed fallback has
1025
+ // no timestamp, so it is priced at whatever window it is read in.
1026
+ const factor = peakFactor(TURN_TIMES.get(usage) ?? Date.now());
1027
+ const cny = priced === null ? null : costOf(usage, priced.rates, factor);
1028
+ const money = moneyText(cny, config.currency, config.usdToCny);
931
1029
  text = (usage.estimated ? "≈" : "") + formatTokens(total) + " tok" + (money === null ? "" : " · " + money);
932
1030
  }
933
1031
 
@@ -1136,10 +1234,10 @@ window.__ModuleLoader__.load({
1136
1234
  "tr",
1137
1235
  { key: model },
1138
1236
  h("td", null, model),
1139
- h("td", null, "$" + rates.input),
1140
- h("td", null, "$" + rates.cacheRead),
1141
- h("td", null, "$" + rates.cacheWrite),
1142
- h("td", null, "$" + rates.output),
1237
+ h("td", null, "¥" + rates.input),
1238
+ h("td", null, "¥" + rates.cacheRead),
1239
+ h("td", null, "¥" + rates.cacheWrite),
1240
+ h("td", null, "¥" + rates.output),
1143
1241
  ),
1144
1242
  ),
1145
1243
  ),
@@ -1231,6 +1329,9 @@ window.__ModuleLoader__.load({
1231
1329
  MODEL_PRICES,
1232
1330
  priceFor,
1233
1331
  costOf,
1332
+ isPeak,
1333
+ peakFactor,
1334
+ beijingParts,
1234
1335
  cacheHitPercent,
1235
1336
  formatTokens,
1236
1337
  amount,
package/locale/en.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Usage and spend",
4
- "description": "Session token usage and estimated spend under the composer; currency and exchange rate are configured on this page.",
4
+ "description": "Session token usage and estimated spend under the composer, priced at DeepSeek peak and off-peak rates; currency and exchange rate are configured on this page.",
5
5
  "detail": "README.md"
6
6
  }
7
7
  }
package/locale/zh.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "用量与花费",
4
- "description": "在会话底部显示 token 用量与估算花费;币种和汇率可在本插件页配置。",
4
+ "description": "在会话底部显示 token 用量与估算花费,按官方峰谷价计价;币种和汇率可在本插件页配置。",
5
5
  "detail": "README.zh.md"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-usage-spend",
3
- "version": "0.1.0",
4
- "description": "Session token usage and estimated spend for DSH: a composer usage pill, the amount appended to the built-in per-turn usage action, and a settings form for currency and exchange rate.",
3
+ "version": "0.2.0",
4
+ "description": "Session token usage and estimated spend for DSH, priced with DeepSeek peak and off-peak rates: a composer usage pill, the amount appended to the built-in per-turn usage action, and a settings form for currency and exchange rate.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "itiswaller",