Chuyển đến nội dung chính

dsh-meter

Đã xác minh

@dshworks/dsh-meter · v0.4.0 · MIT · Giao diện 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.

Cài đặt

dsh plugin add @dshworks/dsh-meter

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Thẻ

Readme

dsh-meter

English | 中文

DeepSeek 开始按时段计价了,这是配套的电表。

每天两段高峰,空闲时段半价。花费不再是事后翻账本才看的数字,而是你此刻正站在里面的费率

输入框下面一行:这个会话花了多少、当前是哪一档、还有多久换档。悬停展开卡片,里面是时段表、缓存的经济账,以及账户余额。

site ci powered by dsh license: MIT

输入框下方的计价行与展开的卡片:会话总价、时段表、缓存与输入拆解、账户余额

安装

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 换到另一档要多少钱 调价前:看新价会让这个会话变成什么样;调价后:看等到空闲时段能省多少
缓存省下了多少 把每一次命中都按未命中重算出来的对照值

设置面板:花钱之前就能看

那一行需要一个会话,而且要这个会话已经产生过计费请求——于是这个插件本该回答的问题, 必须先付钱才问得出来。设置 → 峰谷价在没有会话的情况下直接回答。

dsh 设置里的峰谷价面板:当前档位与倒计时、七天时段网格、公开价目表、账号余额

它刻意不是一张表单。两个配置项都在插件自己的配置里,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.1schema 改名为 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.mjslib/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(先看账号余额币种,再退到界面语言)、usdcny
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_UTCtariffSchedule() 里读,所以排期一改,句子跟着改。一段告诉模型错误高峰时段的提示,就是插件在钱的问题上骗它。
  • 同一个时段内文本恒定。 在这里放倒计时会每分钟变一次,为了一句话滚掉整个会话的提示前缀缓存——这是一个成本插件所能想到的、最昂贵的省钱方式。它只在时段边界翻转(翻转后第一次请求会错过前缀缓存),之后保持不变。
  • 这是提醒,不是限流。 模型可以无视它;请求层面没有任何强制。需要硬上限的话,那是另一个插件。

开发

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