最近在重度使用 DeepSeek Harness(下称 DSH)。它的输入框下面有一排很实用的统计胶囊:轮次、缓存命中、Token 用量、上下文占用——唯独没有「钱」。于是写了个小插件把它补上:dsh-spend-pill,在输入框下方显示本次会话花费的人民币金额,点开还能看分模型、分时段的明细。
这篇文章记录它的设计、踩坑和验证方式,也顺带聊聊「给 DSH 写插件」大概是什么体验。
一、为什么不能「token 数 × 一个单价」
DeepSeek 官方价目不是单一价格:
| 模型 | 时段 | 缓存命中 | 未命中 | 输出 |
|---|---|---|---|---|
| deepseek-flash | 高峰 | ¥0.04 | ¥2 | ¥8 |
| deepseek-flash | 空闲 | ¥0.02 | ¥1 | ¥4 |
| deepseek-v4-pro | 高峰 | ¥0.30 | ¥9 | ¥27 |
| deepseek-v4-pro | 空闲 | ¥0.15 | ¥4.5 | ¥13.5 |
单位是元 / 百万 tokens。高峰时段是北京时间工作日 9:00–12:00、14:00–18:00,其余时间(周末、法定节假日)按空闲价,正好是高峰价的一半。
这意味着三件事:
- 一条消息的缓存命中、未命中、输出三个桶单价不同,不能合并算;
- 同一段会话里切换过模型,要按每次请求的模型分别计价;
- 一次请求可能跨越 12:00 或 18:00 的峰谷边界,得用请求发出时间判断时段,而不是消息返回时间。
二、整体方案:Host 侧算钱,浏览器侧只展示
DSH 的插件是 Cordis 插件,分 Host 与 Client 两半。这个插件没有在前端硬乘 token 数,而是:
- Host 侧注册一个会话投影
tokenCost,跟着会话日志增量折叠; - 投影通过
wire暴露给浏览器; - 浏览器侧在
conversation.composer.dock插槽里注册一枚费用胶囊,读取投影并渲染;点击展开明细面板。
好处是:跨分页、跨上下文压缩、冷启动都靠投影机制兜底,前端不重复扫日志。
核心计价折叠大致是这样:
function fold(state, event) {
if (event.type === 'request/header') {
// 记下这次请求的模型与发出时间
return { ...state, model: pick(event), requestTime: event.time };
}
if (event.type === 'assistant/message') {
const usage = event.data.usage;
const buckets = {
uncachedInputTokens: usage.inputTokens,
cacheReadTokens: usage.cacheReadTokens ?? 0,
outputTokens: usage.outputTokens,
};
// 用 requestTime 判峰谷,而不是 event.time
const peak = isPeakTime(state.requestTime ?? event.time);
const rate = ratesFor(state.model)[peak ? 'peak' : 'offPeak'];
const cny =
(buckets.uncachedInputTokens * rate.cacheMiss +
buckets.cacheReadTokens * rate.cacheHit +
buckets.outputTokens * rate.output) / 1e6;
return { ...state, requestTime: null, totals: add(state.totals, cny) };
}
return state;
}
真实实现里还要处理:同一 Turn/Step 的多次结算只取最后一次(替换)、llm/retry-started 后重试要重新计费、旧模型别名、第三方模型回退,以及「高峰/空闲」各段小计。
三、几个值得说的细节
1. 节假日不是「工作日」就能拍脑袋
官方规则里节假日按空闲价。我按国务院办公厅的通知把 2026 年的放假日期内置成表,isPeakTime() 判断时多查一次日历。日历通过配置也可以整表替换,2027 年的安排公布后补上即可。
2. 改价格表,历史费用要重算
会话投影有检查点缓存。如果只改价格表不处理版本,插件升级后历史会话会继续显示按旧价算的钱。解决办法是让投影的 stateVersion 由生效配置的哈希值决定:
const stateVersion = 2 + (stableHash(effectivePricingConfig) % 1_000_000);
配置一变,版本号就变,旧检查点自动失效,投影按新表重算。省掉了一套手工迁移逻辑。
3. 价格表要可配置
cordis.patch.yml 里可以覆盖:
- id: cost-pill
name: dsh-spend-pill
config:
prices:
my-model:
peak: { cacheHit: 0.04, cacheMiss: 2, output: 8 }
offPeak: { cacheHit: 0.02, cacheMiss: 1, output: 4 }
fallbackModel: deepseek-flash
holidays: ['2027-01-01']
没内置价格的第三方模型按 fallbackModel 估算,面板里会标注「按 deepseek-flash 价格估算」。
4. 前端只负责渲染,但视觉要跟宿主一致
胶囊的颜色、字号、hover 态、点击弹层的底色、阴影、圆角都直接对宿主自带的统计胶囊抄,保证和它排在一行时没有违和感。弹层用 portal 挂在 body 上,按触发器位置做视口内 clamp,Esc 或点击外部关闭。
四、踩过的坑
- Linked bundle 解析不到 zod:本地以链接方式安装时,包内
import 'zod'解析失败。会话投影注册需要 schema,我用了一个最小的{ parse() }兜底实现,等发布 npm 后再换回真 schema。 - 截图拿不到:DSH 的 Web 页面需要认证 URL,自动化截不了图,README 的配图最后用 SVG 手画的界面示意图。
- 同名插件冲突:发布前发现社区已有一个同名、功能相近的插件,于是改名成了
dsh-spend-pill,避免被当成复制品。 - 验证不能只看「装上了」:除了槽位注册,我从会话日志用独立脚本重新折叠了一遍计价,和投影结果逐字段对账,确认替换、重试、跨时段都对。
五、效果与安装
输入框下方多一枚胶囊,点击弹出明细(示意):
- 模型与费用
- 未缓存输入 / 缓存命中输入 / 缓存写入 / 输出:token 数与金额
- 高峰时段小计 / 空闲时段小计 / 合计
安装(目前是 GitHub Release 的预构建包,还没发 npm):
dsh plugin --profile desktop add \
https://github.com/debug-null/dsh-spend-pill/releases/latest/download/dsh-spend-pill.tgz
代码结构很简单:index.js 注册投影,lib/cost.js 是纯函数计价折叠,client.js 是胶囊与弹层,test/ 下是单测,CI 跑语法检查和测试。
六、后续计划
- 预算提醒:会话费用超过阈值时弹一次 toast
- 跟随宿主「紧凑模式」收缩胶囊
- 发布 npm,并补上 2027 年节假日表
如果你也在用 DSH,欢迎试试、提 issue 或 PR;如果发现计价规则和官方文档有不一致,更欢迎直接指出来。