dsh-meter
已验证@dshworks/dsh-meter · v0.4.0 · MIT · Web 界面
The DeepSeek time-of-use meter for dsh: what this session cost, which tariff is running, when it flips, and the account balance behind it — one line under the composer.
安装
dsh plugin add @dshworks/dsh-meter 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
说明文档
dsh-meterEnglish | 中文 DeepSeek 开始按时段计价了,这是配套的电表。每天两段高峰,空闲时段半价。花费不再是事后翻账本才看的数字,而是你此刻正站在里面的费率。 输入框下面一行:这个会话花了多少、当前是哪一档、还有多久换档。悬停展开卡片,里面是时段表、缓存的经济账,以及账户余额。 |
![]() |
安装
dsh plugin --profile web add @dshworks/dsh-meter
dsh --profile web
dsh plugin 转发给 pnpm,因此 PATH 里要有 pnpm。除此之外无需配置:会话产生第一次计费请求后,计价行就会出现在输入框下方。
那一行
2 turns · 2 steps | LLM 3.1s | TTFT avg 1.3s · 41 tok/s | Cache hit 50% | Input 44.3K tok
¥0.0672 | peak | off-peak in 48m
上面是 dsh 自带的统计行,原样保留;下面才是本插件。它新增一行,而不是抢占官方那一格——想在官方统计行里追加内容,只能用同 id 更低 priority 把它顶掉,那样每次上游改动都会跟着坏。
悬停或聚焦展开卡片。处在高峰时段时,档位用琥珀色标出,倒计时指向下一个空闲时段:

| 卡片里有什么 | 为什么值得占这个位置 |
|---|---|
| 会话总价、请求数、模型 | 这个数字只出现一次,用最大的字号 |
| 24 小时时段条,按你所在时区绘制,带实时游标 | 官方按 UTC 公布高峰窗口。深夜心算时区不如直接看一条 |
| 缓存命中 / 未命中输入 / 输出,各自的 token 与金额 | 命中价只有未命中的 1/30。这一行能看出前缀是否稳定 |
| 账户余额,以及其中多少是赠送额度 | 赠送额度会过期,充值余额不会 |
| 同样的 token 换到另一档要多少钱 | 调价前:看新价会让这个会话变成什么样;调价后:看等到空闲时段能省多少 |
| 缓存省下了多少 | 把每一次命中都按未命中重算出来的对照值 |
设置面板:花钱之前就能看
那一行需要一个会话,而且要这个会话已经产生过计费请求——于是这个插件本该回答的问题, 必须先付钱才问得出来。设置 → 峰谷价在没有会话的情况下直接回答。

它刻意不是一张表单。两个配置项都在插件自己的配置里,harness 本来就有编辑插件配置的地方; 为两个字段再造一条写入路径,那是「设置页」的条件反射,不是内容。这里放的是仪表:
| 面板展示 | 为什么在这里 |
|---|---|
| 当前档位,大字号,配倒计时和结束时刻 | 你打开这个面板的理由,在读到别的东西之前就已经回答 |
| 本周——七个本地日 × 二十四小时,高峰为琥珀色,带实时游标 | 卡片上的条带回答「今天什么时候换挡」。周末转为空闲价之后这变成了一个按周的问题,168 格里 35 格琥珀色,是看清「高峰是每周 35 小时而不是 49 小时」最快的方式 |
| 按你账号计价币种显示的公开价目表 | 按 DeepSeek 公布的精度显示——是 ¥1.5,不是 ¥1.50。价目是报价,不是账单 |
| 你的余额 | 面板打开时读一次,走的是卡片用的同一条宿主路由 |
网格按你的日期排,不是按北京的日期排,而每一格都问那个给请求定价的同一个函数—— 所以画面和账单不会不一致,你也不用自己做时区换算。
中文界面里,小时格子上藏了个彩蛋。峰、谷本来就是电力峰谷电价的老词,而「峰」那一半, 离卖 token 那家公司创始人的名字只差一个字。把鼠标放到格子上:文峰、文谷。
验证
2026-08-24 对 dsh 0.1.1-rc.2 做过真实验证,DeepSeek-V4-Pro 与 V4-Flash 真实会话,不是 mock:
这次是重跑,因为只读源码不够。 到 rc.8 为止,这一段一直用「rc.7 与 rc.8 的
src/逐字节相同」来论证投影契约没动过。后来它动了:0.1.1-rc.1把schema改名为stateSchema,并把面向客户端的view挪进了可选的wire。 注册表是逐个字段去读定义的,从不校验,所以旧写法不会报错——它被理解成 「这个投影只留在宿主端」,于是在一个完全健康的 harness 上,那一行什么都不渲染。 一个会退化成沉默的契约,靠 diff 源码是查不出来的;下面这张表是重新跑出来的, 不是推出来的。
| 结论 | 怎么验的 |
|---|---|
| 能在标准 web profile 里加载 | dsh --profile web --dump-config 能列出 @dshworks/dsh-meter;/plugins/@dshworks/dsh-meter/client.js 返回 200 |
| 那一行渲染出来了 | 输入框下方的 ¥0.2185 | off-peak | peak in 14h,直接从活的 DOM 里读出来——正是能抓住 rc.1 那次改名的检查 |
| 读数正确 | V4-Flash 与 V4-Pro 合计 36.1K 未命中输入 + 147 输出 = ¥0.2162 + ¥0.0023 = ¥0.2185,与正上方 dsh 自己那行 token 统计一致 |
| 重启后仍在 | 重启服务、冷启动重开一个八天前的会话,投影从持久日志重放出同一个数 |
| 卡片能打开 | 分模型明细、缓存对照(全高峰 ¥0.2185 / 全低谷 ¥0.1093)、账号余额,页面无报错 |
| 币种自动识别 | 真实账号经 /dsh-meter/balance 返回 {"currency":"cny", ...},整个界面无需配置直接切到 ¥ |
| 两种主题、两种档位 | 亮色/暗色、统一价/高峰,见上图,摄于 2026-08-15——那次早于 8/16 切换,其中的统一价读数是新会话已不会再出现的状态 |
| 130 个测试,CI 绿 | pnpm test — 折叠逻辑、时段时钟、价目表、省钱模式的提示语、余额读取、对着两代注册表替身跑的注册契约,外加生成产物的同步校验 |
两种货币,不做换算
DeepSeek 公布的是两张互相独立的价目表——国际站按美元,中国站按人民币。一个账号只按其中一张计费,两张表也不是彼此的汇率换算,所以用汇率去折算的插件会错两次。
dsh-meter 两张表都算,然后让账号自己决定用哪张。GET /user/balance 会返回该账号的计价币种(真实账号会同时列出两行,只有一行有余额),插件读有余额的那一行并按它显示。不需要配置,也不靠界面语言去猜。
余额请求跑在宿主侧,用的是 LLM adapter 同一套凭据接口;浏览器通过 GET /dsh-meter/balance 只拿到解析好的数字,拿不到 key。它只在计价行挂载和你展开卡片时发出,不做轮询——设 balance: false 可以整个关掉,计价功能不受影响。
价目表
原样写在 lib/core.js 里,单位为每百万 tokens。
| 缓存命中 | 缓存未命中 | 输出 | |
|---|---|---|---|
| v4-flash 空闲 | $0.007 / ¥0.05 | $0.22 / ¥1.5 | $0.66 / ¥4.5 |
| v4-flash 高峰 | $0.014 / ¥0.10 | $0.44 / ¥3 | $1.32 / ¥9 |
| v4-pro 空闲 | $0.022 / ¥0.15 | $0.66 / ¥4.5 | $1.98 / ¥13.5 |
| v4-pro 高峰 | $0.044 / ¥0.30 | $1.32 / ¥9 | $3.96 / ¥27 |
高峰为 UTC 01:00–04:00 与 06:00–10:00(北京时间 09:00–12:00、14:00–18:00)。其余时间都是空闲时段,包括两个窗口之间那两小时。空闲价正好是高峰价的一半——但仍高于它取代的统一价,输出约为 2.3 倍。
已退役的统一价,保留用于回算历史
在 UTC 2026-08-16 16:00 之前全天按此计费。官方页面已不再列出它;电表保留它,是因为切换前记录的会话必须仍按当时真实的价格计算。
| 缓存命中 | 缓存未命中 | 输出 | |
|---|---|---|---|
| v4-flash 统一价 | $0.0028 / ¥0.02 | $0.14 / ¥1 | $0.28 / ¥2 |
| v4-pro 统一价 | $0.003625 / ¥0.025 | $0.435 / ¥3 | $0.87 / ¥6 |
相较于它,v4-pro 缓存命中输入涨到 6 倍(高峰 12 倍)、缓存未命中输入 1.5 倍(高峰 3 倍)、输出 2.3 倍(高峰 4.6 倍)。涨幅最陡的正是最便宜的那种 token,也正是 agent 发得最多的那种。
来源:https://api-docs.deepseek.com/quick_start/pricing,每天与中英文两份页面自动比对(见下节),最近一次确认一致为 2026-08-20——并且核对过真实账单:v4-pro 一次 188,542 缓存未命中 tokens 的调用,空闲时段实扣 ¥0.84,即每百万 ¥4.46,对应公布的 4.5。
价目表也是一份数据源
自己做用量估算?取这份数据,别抄上面表格里的数字。
https://dsh.works/dsh-meter/pricing.json
静态 JSON,无需 key,不限流。两种货币、两个档位、24 小时 UTC 档位表、用于回算历史的已退役统一价,以及三个计费桶的定义——由 scripts/build-feed.mjs 从 lib/core.js 生成,所以它不可能报出一个电表自己不认的价。
JavaScript 里可以跳过这次 fetch,直接调同一个模块:
import { costOf, tariffAt } from '@dshworks/dsh-meter/core'
const tokens = { miss: 188_542, hit: 1_204_880, out: 9_310 }
costOf(tokens, 'deepseek-v4-pro', tariffAt(Date.now()), 'cny')
lib/core.js 不依赖任何包。价目表、档位时钟、成本折叠都是纯函数,内部不读时钟,所以历史会话按它当时真实的档位回算。
现在是什么价?
这份数据不包含「现在」,却能回答这个问题:它发布的是 24 小时档位表,由你用当前 UTC 小时去索引。正因为如此,CDN 缓存十分钟、或者把它编进二进制里,都不会让「当前是哪个档位」变旧:
curl -s https://dsh.works/dsh-meter/pricing.json | jq -r '
(now|gmtime|.[3]) as $h
| .timeOfUse.scheduleUtc[$h] as $t
| "\($t) · v4-pro 输出 $\(.models["deepseek-v4-pro"].rates[$t].usd.out)/1M"'
如果换成一个直接返回答案的接口,它在缓存存活期间就是错的——每天四次,而且恰好错在「差一倍」的那条边界上。
两种会静默出错的写法:
- 用本地小时索引。
scheduleUtc按 UTC 小时索引,不会旋转到你的时区。new Date().getHours()会返回一个看着合理、但对地球上大多数人来说是错的档位。 - broken-down time 的下标。 jq 的
gmtime是[年, 月, 日, 时, …],小时是.[3]。写成.[2]取到的是「日」——而它同样是 24 元素数组的合法下标。写这一节时是 UTC 09:59,.[2]给出offpeak,真实档位是peak。看上去毫无破绽。
档位表锚定在北京时间(UTC+8),而中国自 1991 年起不再实行夏令时——正是这个固定偏移,才让「一张 UTC 小时表」可以安全发布。数据里在 timeOfUse.anchor 写明了这一点;verify-pricing 会分别核对英文页的 UTC 表述与中文页的北京时间表述,再核对两者是否仍然一致。
有一件事任何数据源都救不了:now 用的是你自己机器的时钟。它要是漂了,你的档位也就漂了。
为什么不直接用现成的价格源? 因为没有一个是对的。2026-08-20 复核,也就是切换四天之后,做估算最常用的两个源仍然把 DeepSeek 已退役的统一价当作现价发布:
| 数据源 | v4-pro 缓存未命中输入 | 相对真实账单少算 |
|---|---|---|
| models.dev | $0.435 | 空闲 1.5 倍,高峰 3.0 倍 |
| LiteLLM | $0.435 | 空闲 1.5 倍,高峰 3.0 倍 |
| 本数据源 | 空闲 $0.66 / 高峰 $1.32 | — |
在缓存命中输入这一桶——agent 发得最多的那一桶——models.dev 在高峰时段少算 12 倍。而且这不是他们补一次数据就能修的:两家的 schema 都是「每模型每桶一个固定单价」,根本没有放档位的位置。一个每天有七个小时是错的价格,在这两种结构里都表达不出来。
OpenRouter 的 /api/v1/models 是准的,但回答的是另一个问题——它报的是 OpenRouter 转售这个模型的报价,不是 DeepSeek 从你账户里扣的钱。
这些数字没有人手工敲
我们不敲,也不建议你敲。手工维护的价目表一定会烂掉,而最好的证据来自厂商自己:DeepSeek 官方的 pi 集成文档里那段 cost 配置,v4-flash 的缓存读取价是一个 10 倍的小数点错误(写成 0.028,实际是 0.0028),v4-pro 的数字则正好是价目表的 4 倍——那是 Azure 的转售价,被贴进了 DeepSeek 自己的文档,而这一页是给人直接复制到 agent 配置里的。
所以价目表由机器写、由人审:
| 读 | scripts/verify-pricing.mjs |
抓取中英文两份页面——价格、高峰窗口、模型列表——与价目表逐项比对 |
| 写 | scripts/apply-pricing.mjs |
把抓到的数字写回 lib/core.js,然后用校验脚本重新读一遍自己的输出,读不通就回滚 |
| 发 | npm run build |
用改写后的价目表重新生成 bundle、站点和数据源 |
| 判 | 你 | 任务只开 PR,从不自己合并 |
每天一次的定时任务把这四步跑完。本地:npm run verify:pricing 只看,npm run sync:pricing 才改。
为什么只开 PR 不自动合并。 CI 能检查的全是结构性不变量——空闲价是高峰价的一半、输出比缓存未命中贵、当前请求不会按已退役的统一价计费——而一个「错得很合理」的解析结果能同时满足所有这些。没有任何测试能把「对的价格」和「像样的价格」区分开,但一个人看一眼 diff 可以。
上一次调价,我们盯着的任何渠道都没有收到通知——这是要装警报的理由;厂商自己那个 10 倍的手误,则是把键盘从所有人手里拿走的理由。
钱是怎么算出来的
| 行为 | 细节 |
|---|---|
| 数据来源 | 持久会话日志里由供应商上报的 token 数。不采样,不推测 |
| 每次请求的档位 | 按发出时刻(step/start)判定,而不是按回答结束的时刻。UTC 00:59 发出的请求即使流式输出跨进高峰窗口,仍按空闲价计;跨档会话的两侧各按各的档位算 |
| 计费桶 | inputTokens 按未命中价,cacheReadTokens 按命中价,outputTokens 按输出价。dsh 上报的是互不重叠的三个数(DeepSeek adapter 会从 prompt_tokens 里减掉命中部分),推理 token 已包含在输出里 |
| 缓存写入 | 并入未命中桶。DeepSeek 没有单独的写入价,首次进入缓存的 prompt 按未命中计;DeepSeek adapter 也从不上报这一项 |
| 失败的请求 | 某一步的 usage chunk 即使请求随后失败也会计入;最终 message 会替换这个样本而不是叠加,模型名与请求头不一致时同样如此 |
| 未知模型 | 只计数、只列名,绝不定价。没有公开价格的模型贡献 token 与请求数、金额为零,卡片会明说 |
| 持久性 | 一个会话投影(costMeter),从日志折叠而来。翻页、压缩、刷新、重启服务都不影响,走标准投影缓存 |
对模型的影响
默认没有。dsh-meter 不加工具、不加系统提示、不加消息、不发模型请求,完全不碰请求本身。花费是付钱的人该看的东西,不该占 agent 的上下文。唯一的例外要你自己打开:省钱模式(savingMode: true)会贡献一段系统提示,说明当前处在哪个计费时段;这段文本渲染为空时,整节都不存在。
KV 缓存影响
默认无。开启省钱模式后,meter:tariff 这一节的文本在同一个计费时段内逐字节相同,所以提示前缀——以及它的缓存——能从一次请求保持到下一次;只有在时段切换的那一刻才变,因此切换后的第一次请求会错过前缀缓存,之后重新命中。
配置
以下都是校验过的配置项,写在 profile 的 cordis.patch.yml 里:
- id: dsh-meter
config:
currency: cny # 固定价目表,不再自动识别
balance: false # 完全不调用 /user/balance
| 键 | 默认值 | 含义 |
|---|---|---|
currency |
auto |
显示哪张价目表:auto(先看账号余额币种,再退到界面语言)、usd、cny |
balance |
true |
是否把账户余额提供给 Web UI |
apiKeyEnv |
DEEPSEEK_API_KEY |
存放 API key 的凭据引用 |
baseUrl |
https://api.deepseek.com |
读取余额的接口来源 |
balanceTtlMs |
300000 |
展开卡片时重新拉取余额的最小间隔 |
balanceTimeoutMs |
4000 |
单次余额请求的超时 |
savingMode |
false |
贡献一段系统提示,告诉模型当前处在哪个计费时段,好让它相应地调整行为 |
savingPeakPrompt |
内置提示语 | 高峰窗口期间注入的文本 |
savingOffPeakPrompt |
'' |
非高峰期间注入的文本;默认为空,意味着整节渲染为空、不花一个提示 token |
省钱模式 — 让电表开口说话
默认关闭,因为一个只读的仪表不该自己长进 agent 的上下文里。把 savingMode 打开,电表就不再只是旁观者:每次组装提示时,它会贡献一节 meter:tariff,告诉模型下一次请求将按哪个时段计费、以及在这个时段里该怎么做。
在高峰窗口内,这一节的内容是 savingPeakPrompt——默认是电表自己的提示语:点名高峰时段、说清账单翻倍、并要求模型答得简洁,优先用已有的上下文而不是新开工具调用,遇到昂贵又能等的任务就说出来、提议等窗口关掉再跑。窗口之外这一节渲染 savingOffPeakPrompt,默认为空,所以没有什么要提醒的时候,电表说话的成本是零 token。想让模型在低谷时段反而放开手脚?把 savingOffPeakPrompt 设成类似「现在是半价时段,可以放心多花 token」的文字。
有四点是刻意为之:
- 时段在组装时读取,用的是计费折叠同一个时钟。 提示和账单永远不会对「下一次请求落在边界哪一侧」产生分歧——周末也一样:周末整天按低谷计费,因此完全不会触发提示。
- 提示语里的时段是推导出来的,不是手写的。
peakHoursPhrase()从PEAK_WINDOWS_UTC和tariffSchedule()里读,所以排期一改,句子跟着改。一段告诉模型错误高峰时段的提示,就是插件在钱的问题上骗它。 - 同一个时段内文本恒定。 在这里放倒计时会每分钟变一次,为了一句话滚掉整个会话的提示前缀缓存——这是一个成本插件所能想到的、最昂贵的省钱方式。它只在时段边界翻转(翻转后第一次请求会错过前缀缓存),之后保持不变。
- 这是提醒,不是限流。 模型可以无视它;请求层面没有任何强制。需要硬上限的话,那是另一个插件。
开发
pnpm install
pnpm test # 先校验产物同步,再跑 vitest
pnpm run build # 改完 core/ui 后重新生成三份产物
pnpm run verify:pricing # 与 DeepSeek 官方价格页逐项比对(需联网)
lib/core.js 是价目表、时段时钟和折叠逻辑,lib/balance.js 是账户读取,src/ui.js 是浏览器界面。三份产物都由这一份价目表生成——lib/client.js(dsh 实际分发的 bundle)、docs/index.html(站点)、docs/pricing.json(数据源)——价目表因此只存在于一个文件里,任何一份产物与源码不同步 pnpm test 就会失败。
verify:pricing 是唯一联网的脚本,并且刻意不放进 pnpm test:单元测试不该因为一个文档站点慢而失败,价格警报也不该因为这周没人提 PR 而沉默。它按自己的日程每天跑。
已知限制
- 这是估算,不是账单。 官方价目乘以上报 token。优惠价格、以及任何挡在 API 前面的中转都看不到。
- 档位按发出时间戳判定,用的是 dsh 这台机器的时钟。若 DeepSeek 实际在档位边界另一侧收到请求,可能差出一两秒。
- 非 DeepSeek 线路不定价。 它们的 token 会被计数、模型会被列名,但卡片会标为未定价,而不是把 DeepSeek 的价格悄悄套到别人家的 API 上。
- 价目表是编译进去的。 DeepSeek 会调价;本插件带的是 2026-08-17 复核过的峰谷价目表,跟进下一次调价需要发新版本。
- 只有 Web。 投影本身任何界面都能读,但读数是为 Web UI 做的,没有 TUI 版本。
参考与致谢
dsh 插件登记处已经收录了约 50 个花费/用量插件,其中不少就出现在 DeepSeek 公布调价日期的那一周,读它们的实现影响了这一版的取舍。缓存节省的表述来自 deepseek-cli 的本地用量账本。
许可
MIT
