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 +33 -16
- package/README.zh.md +33 -16
- package/lib/client.js +138 -37
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
| `deepseek-flash`
|
|
47
|
-
| `deepseek-v4-pro`
|
|
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
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
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":
|
|
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
|
-
|
|
43
|
+
单价取自 DeepSeek 官方价目(`api-docs.deepseek.com/zh-cn/quick_start/pricing`,2026-11-07 核对),单位是**元 / 百万 token**,下表为**空闲时段**价格:
|
|
43
44
|
|
|
44
|
-
| 模型 |
|
|
45
|
-
|
|
46
|
-
| `deepseek-flash
|
|
47
|
-
| `deepseek-v4-pro
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
72
|
+
价格表可用浏览器 `localStorage` 覆盖,格式为「模型名 → **元** / 1M token」,填**空闲时段**价格(高峰会再乘 2):
|
|
58
73
|
|
|
59
74
|
```json
|
|
60
75
|
{
|
|
61
|
-
"deepseek-flash": { "input":
|
|
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
|
|
16
|
-
*
|
|
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": "
|
|
56
|
-
"config.ratesTitle": "
|
|
57
|
-
"config.ratesNote": "
|
|
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
|
|
94
|
-
"config.ratesTitle": "Rates (
|
|
95
|
-
"config.ratesNote": "
|
|
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
|
-
/**
|
|
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:
|
|
345
|
-
|
|
346
|
-
"deepseek-v4-flash
|
|
347
|
-
"deepseek-v4-
|
|
348
|
-
"deepseek-
|
|
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
|
-
|
|
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
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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(
|
|
442
|
-
if (
|
|
523
|
+
function moneyText(cny, currency, rate, spaced) {
|
|
524
|
+
if (cny === null) return null;
|
|
443
525
|
const gap = spaced === true ? " " : "";
|
|
444
|
-
const
|
|
445
|
-
const
|
|
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
|
|
448
|
-
return
|
|
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
|
|
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(
|
|
809
|
-
const titleMoney = moneyText(
|
|
810
|
-
const moneyNode =
|
|
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
|
|
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
|
-
|
|
930
|
-
|
|
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, "
|
|
1140
|
-
h("td", null, "
|
|
1141
|
-
h("td", null, "
|
|
1142
|
-
h("td", null, "
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-usage-spend",
|
|
3
|
-
"version": "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",
|