给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准

8 阅读5分钟

最近在重度使用 DeepSeek Harness(下称 DSH)。它的输入框下面有一排很实用的统计胶囊:轮次、缓存命中、Token 用量、上下文占用——唯独没有「钱」。于是写了个小插件把它补上:dsh-spend-pill,在输入框下方显示本次会话花费的人民币金额,点开还能看分模型、分时段的明细。

这篇文章记录它的设计、踩坑和验证方式,也顺带聊聊「给 DSH 写插件」大概是什么体验。

juejin-cover.png

一、为什么不能「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,其余时间(周末、法定节假日)按空闲价,正好是高峰价的一半。

这意味着三件事:

  1. 一条消息的缓存命中、未命中、输出三个桶单价不同,不能合并算;
  2. 同一段会话里切换过模型,要按每次请求的模型分别计价;
  3. 一次请求可能跨越 12:00 或 18:00 的峰谷边界,得用请求发出时间判断时段,而不是消息返回时间。

二、整体方案:Host 侧算钱,浏览器侧只展示

DSH 的插件是 Cordis 插件,分 Host 与 Client 两半。这个插件没有在前端硬乘 token 数,而是:

  1. Host 侧注册一个会话投影 tokenCost,跟着会话日志增量折叠;
  2. 投影通过 wire 暴露给浏览器;
  3. 浏览器侧在 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 或点击外部关闭。

四、踩过的坑

  1. Linked bundle 解析不到 zod:本地以链接方式安装时,包内 import 'zod' 解析失败。会话投影注册需要 schema,我用了一个最小的 { parse() } 兜底实现,等发布 npm 后再换回真 schema。
  2. 截图拿不到:DSH 的 Web 页面需要认证 URL,自动化截不了图,README 的配图最后用 SVG 手画的界面示意图。
  3. 同名插件冲突:发布前发现社区已有一个同名、功能相近的插件,于是改名成了 dsh-spend-pill,避免被当成复制品。
  4. 验证不能只看「装上了」:除了槽位注册,我从会话日志用独立脚本重新折叠了一遍计价,和投影结果逐字段对账,确认替换、重试、跨时段都对。

五、效果与安装

输入框下方多一枚胶囊,点击弹出明细(示意):

  • 模型与费用
  • 未缓存输入 / 缓存命中输入 / 缓存写入 / 输出:token 数与金额
  • 高峰时段小计 / 空闲时段小计 / 合计

安装(目前是 GitHub Release 的预构建包,还没发 npm):

dsh plugin --profile desktop add \
  https://github.com/debug-null/dsh-spend-pill/releases/latest/download/dsh-spend-pill.tgz

项目地址:github.com/debug-null/…

代码结构很简单:index.js 注册投影,lib/cost.js 是纯函数计价折叠,client.js 是胶囊与弹层,test/ 下是单测,CI 跑语法检查和测试。

六、后续计划

  • 预算提醒:会话费用超过阈值时弹一次 toast
  • 跟随宿主「紧凑模式」收缩胶囊
  • 发布 npm,并补上 2027 年节假日表

如果你也在用 DSH,欢迎试试、提 issue 或 PR;如果发现计价规则和官方文档有不一致,更欢迎直接指出来。