dsh-usage-spend 0.1.0 → 0.2.1

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,16 +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.
4
-
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.
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.
6
4
 
7
5
  ## What you get
8
6
 
9
7
  - **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`.
8
+ - **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.
9
+ - **Per-turn action**: the built-in `用量 85.3M tok` becomes `85.3M tok · ≈¥3.09`, priced at that turn's end time.
10
+ - **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
11
  - **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
- - **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.
12
+ - **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. A `峰` / `谷` badge after the amount names the billing window, and the tooltip spells it out. Can be turned off.
14
13
 
15
14
  ## Install
16
15
 
@@ -39,31 +38,47 @@ Turning the row off removes the pill, the per-turn amount and the sidebar balanc
39
38
 
40
39
  ## How the estimate works
41
40
 
42
- The price table is a models.dev snapshot of DeepSeek pricing (USD / 1M tokens):
41
+ 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:
42
+
43
+ | Model | Input (cache miss) | Input (cache hit) | Output |
44
+ |---|---:|---:|---:|
45
+ | `deepseek-flash` (DeepSeek-V4.1-Flash-1017) | 1 | 0.02 | 4 |
46
+ | `deepseek-v4-pro` (DeepSeek-V4-Pro-0813) | 4.5 | 0.15 | 13.5 |
47
+
48
+ `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.
49
+
50
+ ### Peak and off-peak
43
51
 
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 |
52
+ DeepSeek charges half the peak price off-peak, so **peak = twice the table**.
49
53
 
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 `≈`.
54
+ - Peak: Beijing time, **Monday to Friday**, `09:00–12:00` and `14:00–18:00`
55
+ - Everything else is off-peak: nights, the lunch gap, **weekends** and **whole public holidays**
56
+ - Holidays follow the State Council schedule; the plugin ships the 2026 dates and needs one update a year
57
+
58
+ How the numbers are produced:
59
+
60
+ - Cost = (uncached input × input + cache reads × cache-read + cache writes × cache-write + output × output) / 1e6 × window multiplier
61
+ - Cache writes bill at the cache-miss input price, matching DeepSeek
62
+ - **Per-turn amounts use that turn's end time**, so they are exact
63
+ - **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
64
+ - The `峰` / `谷` badge at the end of the sidebar balance line names the window in effect. It is hidden when the sidebar is collapsed or the balance chip is turned off; the card's billing-window row still shows it
65
+ - CNY is the base currency; USD is converted with the configured rate and only affects that column
66
+ - Discounts, granted credit, Search/Fetch fees and the platform's actual bill are **not** included. This is an estimate only.
54
67
 
55
68
  ## Custom prices
56
69
 
57
- Write `localStorage["dsh.cost.prices"]` from the browser console, keyed by model name (USD / 1M tokens):
70
+ Write `localStorage["dsh.cost.prices"]` from the browser console, keyed by model name (**CNY per 1M tokens, off-peak**; peak doubles it):
58
71
 
59
72
  ```json
60
73
  {
61
- "deepseek-flash": { "input": 0.15, "cacheRead": 0.003, "cacheWrite": 0.15, "output": 0.6 }
74
+ "deepseek-flash": { "input": 1, "cacheRead": 0.02, "cacheWrite": 1, "output": 4 }
62
75
  }
63
76
  ```
64
77
 
65
78
  Keys may be the full model name, `provider/model`, or the bare model name. Reload to apply.
66
79
 
80
+ > 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.
81
+
67
82
  ## Compatibility and limits
68
83
 
69
84
  - Targets the client slot / projection API of DSH `0.2.2-rc.1`.
package/README.zh.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # 用量与花费(dsh-usage-spend)
2
2
 
3
- 在 DSH 的输入框下方显示本次会话的 token 用量与**估算花费**,并把金额补到内置的每轮用量按钮上。币种和汇率在 **插件** 页配置。
4
-
5
- 一个包同时是 bundle(声明 `usage-cost` 这一行)和功能本体,装上即出现插件页卡片。
3
+ 在 DSH 的输入框下方显示本次会话的 token 用量与**估算花费**,按 DeepSeek 官方的**峰谷价**计价,并把金额补到内置的每轮用量按钮上。币种和汇率在 **插件** 页配置。
6
4
 
7
5
  ## 功能
8
6
 
9
7
  - **底部用量 pill**:`85.3M tok · ≈¥3.09 · 缓存命中 99.4%`,替换内置的 `usage` 行。
10
- - **点击卡片**:标题行显示精确 token 数与金额,下面是缓存命中、未缓存输入、缓存读取、缓存写入、输出的分桶。
11
- - **每轮按钮**:把内置的 `用量 85.3M tok` 改成 `85.3M tok · ≈¥3.09`。
8
+ - **点击卡片**:标题行显示精确 token 数与金额,下面是缓存命中、未缓存输入、缓存读取、缓存写入、输出、计费时段。
9
+ - **每轮按钮**:把内置的 `用量 85.3M tok` 改成 `85.3M tok · ≈¥3.09`,金额按该轮结束时间判断峰谷。
10
+ - **峰谷计价**:北京时间工作日 9:00–12:00、14:00–18:00 为高峰(价目 ×2),其余时段含周末与法定节假日为空闲。
12
11
  - **插件页配置**:`币种`(人民币 / 美元 / 同时显示)、`汇率(1 USD = ? CNY)`、是否显示左侧栏余额,并在表单下方以只读表格展示当前单价表。
13
- - **左侧栏余额**:在侧栏底部显示 `余额 ¥12.34`,每 60 秒或点击时刷新;赠金放在 tooltip 里;折叠栏显示紧凑金额。可关闭。
12
+ - **左侧栏余额**:在侧栏底部显示 `余额 ¥12.34`,每 60 秒或点击时刷新;赠金放在 tooltip 里;折叠栏显示紧凑金额。金额后面带 `峰` / `谷` 小标签,标明当前的计费时段,悬停 tooltip 里有完整说明。可关闭。
14
13
 
15
14
  ## 安装
16
15
 
@@ -39,31 +38,47 @@ dsh plugin --profile <profile> add dsh-usage-spend
39
38
 
40
39
  ## 估算口径
41
40
 
42
- 价格表是 models.dev 的 DeepSeek 快照(USD / 1M token):
41
+ 单价取自 DeepSeek 官方价目(`api-docs.deepseek.com/zh-cn/quick_start/pricing`,2026-11-07 核对),单位是**元 / 百万 token**,下表为**空闲时段**价格:
42
+
43
+ | 模型 | 输入(缓存未命中) | 输入(缓存命中) | 输出 |
44
+ |---|---:|---:|---:|
45
+ | `deepseek-flash`(DeepSeek-V4.1-Flash-1017) | 1 | 0.02 | 4 |
46
+ | `deepseek-v4-pro`(DeepSeek-V4-Pro-0813) | 4.5 | 0.15 | 13.5 |
47
+
48
+ `deepseek-v4-flash` 与 `deepseek-v4-flash-vision-exp` 是**已下线但仍可调用**的旧模型名,请求由 V4.1-Flash 提供并按 Flash 价格计费,因此与 `deepseek-flash` 同价。`deepseek-chat` / `deepseek-reasoner` 已于 2026-07-24 停止服务,不再有价格。
49
+
50
+ ### 峰谷计价
43
51
 
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 |
52
+ 官方规则:**空闲时段价格是高峰时段的一半**,即高峰 = 表中价格 ×2。
49
53
 
50
- - 费用 = 未缓存输入 × 输入价 + 缓存读取 × 缓存读价 + 缓存写入 × 缓存写价 + 输出 × 输出价。
51
- - 人民币金额按配置的汇率换算,默认 `7.2`。
52
- - **不包含**峰谷倍率、折扣、赠送额度或平台实际账单,只用于估算。
53
- - 每轮金额优先使用会话里的精确用量;精确用量不可得时按该轮 assistant 消息求和并显示 `≈`。
54
+ - 高峰时段:北京时间**周一至周五** `9:00–12:00`、`14:00–18:00`
55
+ - 其余时段全部为空闲:夜间、午休、**周末**、**中国法定节假日全天**
56
+ - 节假日按国务院办公厅的放假安排,插件内置 2026 年日期(元旦、春节、清明、劳动节、端午、中秋、国庆);次年安排公布后需要更新一次
57
+
58
+ 计算方式:
59
+
60
+ - 费用 = (未缓存输入 × 输入价 + 缓存读取 × 缓存读价 + 缓存写入 × 缓存写价 + 输出 × 输出价)/ 1e6 × 时段倍率
61
+ - 缓存写入按缓存未命中的输入价计费,与官方一致
62
+ - **单轮金额按该轮结束时间**判断峰谷,是精确的
63
+ - **会话总额按当前时段**估算:整份 token 统计没有时间维度,无法回溯每一轮分别落在哪个时段。所以隔天回看老会话时,总额按你打开它时的时段算,而每轮按钮上的金额不变
64
+ - 左侧栏余额那行末尾的 `峰` / `谷` 小标签就是当前用的哪档;侧栏收起、或关掉余额显示时看不到它,卡片里的「计费时段」仍然在
65
+ - 币种默认人民币;选美元时按配置的汇率把人民币换算过去,汇率只影响美元那一栏
66
+ - **不包含**折扣、赠送额度、Search/Fetch 费用或平台实际账单,只用于估算
54
67
 
55
68
  ## 自定义价格
56
69
 
57
- 在浏览器控制台写入 `localStorage["dsh.cost.prices"]`,格式为「模型名 → USD / 1M token」:
70
+ 价格表可用浏览器 `localStorage` 覆盖,格式为「模型名 → **元** / 1M token」,填**空闲时段**价格(高峰会再乘 2):
58
71
 
59
72
  ```json
60
73
  {
61
- "deepseek-flash": { "input": 0.15, "cacheRead": 0.003, "cacheWrite": 0.15, "output": 0.6 }
74
+ "deepseek-flash": { "input": 1, "cacheRead": 0.02, "cacheWrite": 1, "output": 4 }
62
75
  }
63
76
  ```
64
77
 
65
78
  键可以是完整模型名、`provider/model` 或裸模型名;刷新页面生效。
66
79
 
80
+ > 0.1.0 的覆盖值是美元,0.2.0 起改为人民币并新增峰谷倍率,旧覆盖值需要按新单位重填。
81
+
67
82
  ## 兼容性与限制
68
83
 
69
84
  - 面向 DSH `0.2.2-rc.1` 的客户端 slot / projection API。
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-usage-cost — browser half.
2
+ * dsh-usage-spend — browser half.
3
3
  *
4
4
  * Contributes:
5
5
  * - `conversation.composer.dock` id `usage` at priority -1, replacing the
@@ -12,11 +12,12 @@
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
- id: "dsh-usage-cost",
20
+ id: "dsh-usage-spend",
20
21
  factory: (require) => {
21
22
  var module = { exports: {} };
22
23
  var exports = module.exports;
@@ -26,8 +27,8 @@ window.__ModuleLoader__.load({
26
27
  const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
27
28
  const h = React.createElement;
28
29
 
29
- const NS = "dsh-usage-cost";
30
- const STYLE_ID = "dsh-usage-cost/styles";
30
+ const NS = "dsh-usage-spend";
31
+ const STYLE_ID = "dsh-usage-spend/styles";
31
32
  const ENTRY_ID = "usage-cost";
32
33
  const CONFIG_KEY = "dsh-usage-spend#usage-cost";
33
34
  const CURRENCY_KEY = "dsh.cost.currency";
@@ -39,12 +40,17 @@ window.__ModuleLoader__.load({
39
40
  const zh = {
40
41
  "pill.tokens": "{count} tok",
41
42
  "pill.cacheHit": "缓存命中 {percent}%",
43
+ "window.peak": "峰",
44
+ "window.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,13 +58,15 @@ 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,也不会请求账号余额。",
61
67
  "balance.label": "余额",
68
+ "balance.windowPeak": "当前按高峰价估算(表价 ×2)",
69
+ "balance.windowOffPeak": "当前按空闲价估算",
62
70
  "balance.loading": "读取中…",
63
71
  "balance.unavailable": "暂不可用",
64
72
  "balance.signedOut": "未登录",
@@ -77,12 +85,17 @@ window.__ModuleLoader__.load({
77
85
  const en = {
78
86
  "pill.tokens": "{count} tok",
79
87
  "pill.cacheHit": "cache hit {percent}%",
88
+ "window.peak": "peak",
89
+ "window.offPeak": "off-peak",
80
90
  "dialog.title": "Token usage",
81
91
  "dialog.cacheHit": "Cache hit",
82
92
  "dialog.input": "Uncached input",
83
93
  "dialog.cacheRead": "Cache read",
84
94
  "dialog.cacheWrite": "Cache write",
85
95
  "dialog.output": "Output",
96
+ "dialog.window": "Billing window",
97
+ "dialog.windowPeak": "Peak (×2)",
98
+ "dialog.windowOffPeak": "Off-peak",
86
99
  "currency.cny": "¥ CNY",
87
100
  "currency.usd": "$ USD",
88
101
  "currency.both": "¥+$",
@@ -90,13 +103,15 @@ window.__ModuleLoader__.load({
90
103
  "config.title": "Usage and spend",
91
104
  "config.currency": "Display currency",
92
105
  "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.",
106
+ "config.rateHint": "Only converts the USD figure; CNY comes straight from the official card.",
107
+ "config.ratesTitle": "Rates (CNY / 1M tok, off-peak)",
108
+ "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
109
  "config.model": "Model",
97
110
  "config.showBalance": "Show the sidebar balance",
98
111
  "config.showBalanceHint": "When off, the balance chip is hidden and no balance request is made.",
99
112
  "balance.label": "Balance",
113
+ "balance.windowPeak": "Estimated at peak rates (twice the card)",
114
+ "balance.windowOffPeak": "Estimated at off-peak rates",
100
115
  "balance.loading": "Loading…",
101
116
  "balance.unavailable": "Unavailable",
102
117
  "balance.signedOut": "Not signed in",
@@ -124,6 +139,8 @@ window.__ModuleLoader__.load({
124
139
  ".dshuc_label{text-overflow:ellipsis;min-width:0;overflow:hidden}",
125
140
  ".dshuc_sep{color:var(--dsw-alias-separator-primary);margin:0 6px}",
126
141
  ".dshuc_money{color:var(--dsw-alias-label-secondary)}",
142
+ ".dshuc_window{flex:none;color:var(--dsw-alias-label-tertiary);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xs,4px);padding:0 3px;font-size:10px;line-height:14px}",
143
+ ".dshuc_balance[data-wide=false] .dshuc_window{display:none}",
127
144
  ".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
145
  ".dshuc_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
129
146
  ".dshuc_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}",
@@ -166,7 +183,7 @@ window.__ModuleLoader__.load({
166
183
  if (typeof document === "undefined") return;
167
184
  if (document.querySelector('style[data-plugin-css="' + STYLE_ID + '"]') !== null) return;
168
185
  const tag = document.createElement("style");
169
- tag.dataset.plugin = "dsh-usage-cost";
186
+ tag.dataset.plugin = "dsh-usage-spend";
170
187
  tag.dataset.pluginCss = STYLE_ID;
171
188
  tag.textContent = CSS;
172
189
  document.head.appendChild(tag);
@@ -330,7 +347,7 @@ window.__ModuleLoader__.load({
330
347
  if (forms === undefined || typeof forms.get !== "function") return;
331
348
  const detach = configStore.attach(forms.get(ENTRY_ID));
332
349
  if (typeof scope.effect === "function") {
333
- scope.effect(() => detach, "dsh-usage-cost: config form");
350
+ scope.effect(() => detach, "dsh-usage-spend: config form");
334
351
  }
335
352
  });
336
353
  } catch {
@@ -339,16 +356,83 @@ window.__ModuleLoader__.load({
339
356
  }
340
357
 
341
358
  // ----------------------------------------------------------------- prices
342
- /** USD per 1M tokens, models.dev snapshot (2026-11-07). */
359
+ /**
360
+ * CNY per 1M tokens, off-peak rates from the official card
361
+ * (api-docs.deepseek.com/zh-cn/quick_start/pricing, checked 2026-11-07).
362
+ * Peak hours cost twice these rates. Cache writes bill at the cache-miss
363
+ * input price, which is what DeepSeek charges for them.
364
+ */
343
365
  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 },
366
+ "deepseek-flash": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
367
+ // Retired names: still accepted by the API, served and billed as Flash.
368
+ "deepseek-v4-flash": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
369
+ "deepseek-v4-flash-vision-exp": { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 },
370
+ "deepseek-v4-pro": { input: 4.5, cacheRead: 0.15, cacheWrite: 4.5, output: 13.5 },
350
371
  };
351
372
 
373
+ // ------------------------------------------------------------ peak hours
374
+ /**
375
+ * Peak = Beijing time, Monday to Friday, 09:00–12:00 and 14:00–18:00,
376
+ * excluding Chinese public holidays. Peak costs twice the off-peak rate.
377
+ * Everything else — nights, weekends, whole holiday days — is off-peak.
378
+ */
379
+ const PEAK_FACTOR = 2;
380
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
381
+ const PEAK_HOURS = [
382
+ [9, 12],
383
+ [14, 18],
384
+ ];
385
+ /** 国务院办公厅 2026 年放假安排;次年公布后追加即可。 */
386
+ const PUBLIC_HOLIDAY_RANGES = [
387
+ ["2026-01-01", "2026-01-03"], // 元旦
388
+ ["2026-02-15", "2026-02-23"], // 春节
389
+ ["2026-04-04", "2026-04-06"], // 清明
390
+ ["2026-05-01", "2026-05-05"], // 劳动节
391
+ ["2026-06-19", "2026-06-21"], // 端午
392
+ ["2026-09-25", "2026-09-27"], // 中秋
393
+ ["2026-10-01", "2026-10-07"], // 国庆
394
+ ];
395
+ const PUBLIC_HOLIDAYS = (() => {
396
+ const days = new Set();
397
+ for (const [from, to] of PUBLIC_HOLIDAY_RANGES) {
398
+ const last = Date.parse(to + "T00:00:00Z");
399
+ for (let at = Date.parse(from + "T00:00:00Z"); at <= last; at += 86_400_000) {
400
+ days.add(new Date(at).toISOString().slice(0, 10));
401
+ }
402
+ }
403
+ return days;
404
+ })();
405
+
406
+ /** Beijing wall-clock parts for an instant, independent of the host timezone. */
407
+ function beijingParts(at) {
408
+ const shifted = new Date(Number(at) + BEIJING_OFFSET_MS);
409
+ return {
410
+ date: shifted.toISOString().slice(0, 10),
411
+ weekday: shifted.getUTCDay(),
412
+ hour: shifted.getUTCHours() + shifted.getUTCMinutes() / 60,
413
+ };
414
+ }
415
+ /** True while DeepSeek bills this instant at the peak rate. */
416
+ function isPeak(at) {
417
+ const ms = Number(at);
418
+ if (!Number.isFinite(ms)) return false;
419
+ const { date, weekday, hour } = beijingParts(ms);
420
+ if (weekday === 0 || weekday === 6) return false;
421
+ if (PUBLIC_HOLIDAYS.has(date)) return false;
422
+ return PEAK_HOURS.some(([from, to]) => hour >= from && hour < to);
423
+ }
424
+ /** Multiplier applied to the off-peak card. */
425
+ function peakFactor(at) {
426
+ return isPeak(at) ? PEAK_FACTOR : 1;
427
+ }
428
+
429
+ /**
430
+ * Turn-end time for each per-turn usage object. The object is owned by the
431
+ * chat snapshot, so keying by it keeps the selector's identity stable
432
+ * while still telling the render which billing window to use.
433
+ */
434
+ const TURN_TIMES = new WeakMap();
435
+
352
436
  const PRICE_OVERRIDES = (() => {
353
437
  const raw = readStorage(PRICES_KEY);
354
438
  if (raw === null) return {};
@@ -403,15 +487,18 @@ window.__ModuleLoader__.load({
403
487
  function totalTokens(usage) {
404
488
  return billedInput(usage) + num(usage.outputTokens);
405
489
  }
406
- function costOf(usage, rates) {
490
+ /** CNY for this usage at the given off-peak rates, scaled by the peak factor. */
491
+ function costOf(usage, rates, factor) {
407
492
  if (rates === null) return null;
408
- const usd =
409
- (num(usage.uncachedInputTokens) * rates.input +
493
+ const scale = Number.isFinite(factor) && factor > 0 ? factor : 1;
494
+ const cny =
495
+ ((num(usage.uncachedInputTokens) * rates.input +
410
496
  num(usage.cacheReadTokens) * rates.cacheRead +
411
497
  num(usage.cacheWriteTokens) * rates.cacheWrite +
412
498
  num(usage.outputTokens) * rates.output) /
413
- 1e6;
414
- return Number.isFinite(usd) ? usd : null;
499
+ 1e6) *
500
+ scale;
501
+ return Number.isFinite(cny) ? cny : null;
415
502
  }
416
503
  function cacheHitPercent(usage) {
417
504
  const prompt = billedInput(usage);
@@ -438,14 +525,15 @@ window.__ModuleLoader__.load({
438
525
  if (value >= 0.01) return value.toFixed(3);
439
526
  return value.toFixed(4);
440
527
  }
441
- function moneyText(usd, currency, rate, spaced) {
442
- if (usd === null) return null;
528
+ function moneyText(cny, currency, rate, spaced) {
529
+ if (cny === null) return null;
443
530
  const gap = spaced === true ? " " : "";
444
- const cny = "≈" + gap + "¥" + amount(usd * (Number.isFinite(rate) && rate > 0 ? rate : readRate()));
445
- const dollar = "≈" + gap + "$" + amount(usd);
531
+ const perUsd = Number.isFinite(rate) && rate > 0 ? rate : readRate();
532
+ const yuan = "≈" + gap + "¥" + amount(cny);
533
+ const dollar = "≈" + gap + "$" + amount(cny / perUsd);
446
534
  if (currency === "USD") return dollar;
447
- if (currency === "BOTH") return cny + " / " + dollar;
448
- return cny;
535
+ if (currency === "BOTH") return yuan + " / " + dollar;
536
+ return yuan;
449
537
  }
450
538
 
451
539
  // --------------------------------------------------------------- balance
@@ -604,6 +692,7 @@ window.__ModuleLoader__.load({
604
692
 
605
693
  function BalanceChip({ wide, balance, t }) {
606
694
  const config = useConfig();
695
+ const peak = isPeak(Date.now());
607
696
  const state = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot, balance.getSnapshot);
608
697
  if (!config.showBalance) return null;
609
698
  const wallets = state.wallets ?? [];
@@ -624,11 +713,13 @@ window.__ModuleLoader__.load({
624
713
  } else {
625
714
  value = "…";
626
715
  }
716
+ const windowText = t(peak ? "balance.windowPeak" : "balance.windowOffPeak");
627
717
  const tooltip =
628
718
  state.status === "ready"
629
719
  ? [
630
720
  t("balance.tooltip", { wallets: wallets.length > 0 ? walletListText(wallets) : formatMoney(0, "CNY") }),
631
721
  bonusWallets.length > 0 ? t("balance.bonus", { wallets: walletListText(bonusWallets) }) : null,
722
+ windowText,
632
723
  t("balance.refresh"),
633
724
  ]
634
725
  .filter((part) => part !== null)
@@ -657,6 +748,7 @@ window.__ModuleLoader__.load({
657
748
  h("span", { className: "dshuc_balanceGlyph", "aria-hidden": true }, symbol),
658
749
  h("span", { className: "dshuc_balanceLabel" }, t("balance.label")),
659
750
  h("span", { className: "dshuc_balanceValue" }, value),
751
+ wide ? h("span", { className: "dshuc_window" }, t(peak ? "window.peak" : "window.offPeak")) : null,
660
752
  ),
661
753
  },
662
754
  );
@@ -704,9 +796,9 @@ window.__ModuleLoader__.load({
704
796
  };
705
797
  const access = scope.remoteAccess;
706
798
  if (access !== undefined && typeof access.whenAccess === "function") {
707
- scope.effect(() => access.whenAccess("account/watch", register), "dsh-usage-cost: balance");
799
+ scope.effect(() => access.whenAccess("account/watch", register), "dsh-usage-spend: balance");
708
800
  } else {
709
- scope.effect(() => register(), "dsh-usage-cost: balance");
801
+ scope.effect(() => register(), "dsh-usage-spend: balance");
710
802
  }
711
803
  });
712
804
  } catch {
@@ -802,11 +894,12 @@ window.__ModuleLoader__.load({
802
894
 
803
895
  const route = selection?.pending ?? selection?.lastUsed ?? null;
804
896
  const priced = priceFor(route);
805
- const usd = priced === null ? null : costOf(usage, priced.rates);
897
+ const peak = isPeak(Date.now());
898
+ const cny = priced === null ? null : costOf(usage, priced.rates, peak ? PEAK_FACTOR : 1);
806
899
  const total = prompt + output;
807
900
  const cacheHit = cacheHitPercent(usage);
808
- const money = moneyText(usd, config.currency, config.usdToCny);
809
- const titleMoney = moneyText(usd, config.currency, config.usdToCny, true);
901
+ const money = moneyText(cny, config.currency, config.usdToCny);
902
+ const titleMoney = moneyText(cny, config.currency, config.usdToCny, true);
810
903
  const moneyNode = money === null ? null : h("span", { className: "dshuc_money" }, money);
811
904
  const cacheHitText = cacheHit === null ? null : t("pill.cacheHit", { percent: cacheHit });
812
905
 
@@ -840,6 +933,7 @@ window.__ModuleLoader__.load({
840
933
  ? [t("dialog.cacheWrite"), exactCount(num(usage.cacheWriteTokens))]
841
934
  : null,
842
935
  [t("dialog.output"), exactCount(output)],
936
+ [t("dialog.window"), t(peak ? "dialog.windowPeak" : "dialog.windowOffPeak")],
843
937
  ].filter((row) => row !== null);
844
938
 
845
939
  return h(
@@ -883,7 +977,9 @@ window.__ModuleLoader__.load({
883
977
  let found;
884
978
  for (const node of nodes.values()) {
885
979
  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;
980
+ if (node.data?.turn !== turnNumber || node.data?.tokenUsage === undefined) continue;
981
+ found = node.data.tokenUsage;
982
+ if (typeof node.data.time === "number") TURN_TIMES.set(found, node.data.time);
887
983
  }
888
984
  let summed = null;
889
985
  if (Array.isArray(snapshot.legacy?.nodes)) {
@@ -926,8 +1022,11 @@ window.__ModuleLoader__.load({
926
1022
  const total = typeof usage.totalTokens === "number" ? usage.totalTokens : totalTokens(usage);
927
1023
  const route = usage.routes?.[0] ?? selection?.pending ?? selection?.lastUsed ?? null;
928
1024
  const priced = priceFor(route);
929
- const usd = priced === null ? null : costOf(usage, priced.rates);
930
- const money = moneyText(usd, config.currency, config.usdToCny);
1025
+ // The turn's own end time decides its window; the summed fallback has
1026
+ // no timestamp, so it is priced at whatever window it is read in.
1027
+ const factor = peakFactor(TURN_TIMES.get(usage) ?? Date.now());
1028
+ const cny = priced === null ? null : costOf(usage, priced.rates, factor);
1029
+ const money = moneyText(cny, config.currency, config.usdToCny);
931
1030
  text = (usage.estimated ? "≈" : "") + formatTokens(total) + " tok" + (money === null ? "" : " · " + money);
932
1031
  }
933
1032
 
@@ -1136,10 +1235,10 @@ window.__ModuleLoader__.load({
1136
1235
  "tr",
1137
1236
  { key: model },
1138
1237
  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),
1238
+ h("td", null, "¥" + rates.input),
1239
+ h("td", null, "¥" + rates.cacheRead),
1240
+ h("td", null, "¥" + rates.cacheWrite),
1241
+ h("td", null, "¥" + rates.output),
1143
1242
  ),
1144
1243
  ),
1145
1244
  ),
@@ -1183,7 +1282,7 @@ window.__ModuleLoader__.load({
1183
1282
  ru: en,
1184
1283
  pt: en,
1185
1284
  }),
1186
- "dsh-usage-cost: dictionaries",
1285
+ "dsh-usage-spend: dictionaries",
1187
1286
  );
1188
1287
  injectStyles();
1189
1288
  attachConfig(ctx);
@@ -1223,7 +1322,7 @@ window.__ModuleLoader__.load({
1223
1322
  );
1224
1323
  }
1225
1324
 
1226
- exports.name = "dsh-usage-cost";
1325
+ exports.name = "dsh-usage-spend";
1227
1326
  exports.inject = ["slots", "locale"];
1228
1327
  exports.apply = apply;
1229
1328
  /** Test-only internals (loaded through the stub module factory). */
@@ -1231,6 +1330,9 @@ window.__ModuleLoader__.load({
1231
1330
  MODEL_PRICES,
1232
1331
  priceFor,
1233
1332
  costOf,
1333
+ isPeak,
1334
+ peakFactor,
1335
+ beijingParts,
1234
1336
  cacheHitPercent,
1235
1337
  formatTokens,
1236
1338
  amount,
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Host loader entry for dsh-usage-cost.
2
+ * Host loader entry for dsh-usage-spend.
3
3
  *
4
4
  * The feature is browser-only: the usage projection, the composer dock and the
5
5
  * turn-footer amount all live in the client runtime, and pricing is a local
@@ -15,6 +15,6 @@ export const Config = z.object({
15
15
  showBalance: z.boolean().default(true).volatile(),
16
16
  });
17
17
 
18
- export const name = "dsh-usage-cost";
18
+ export const name = "dsh-usage-spend";
19
19
 
20
20
  export function apply() {}
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.1",
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",