DSH Plugin Store
Back to home

zh667

TokenLedger

Token usage accounting for DeepSeek Harness, reconciled against New API and Sub2API relay-site billing

Stars
0
Language
JavaScript
Created
Aug 14, 2026
Updated
Aug 14, 2026
Other
GitHub repo

Introduction

TokenLedger

🚧 早期开发中:已作为 DSH 插件在真实 DSH 中跑通全链路;Web UI 页面尚未完成,也未发布 npm。

⚠️ 非官方声明:TokenLedger 是独立的第三方社区项目,与 DeepSeek 无隶属、赞助或背书关系。「DeepSeek」及相关商标归其权利人所有。

统计 DeepSeek Harness 的 Token 消耗,并和 New API、Sub2API 中转站的实际扣费对账。

用量统计本身在 DSH 生态里已经有几十个实现。TokenLedger 存在的理由是它们都缺的那一半:你的用量记录里没有「这笔钱花在哪个中转站」,所以永远对不上中转站的账单。

它解决什么问题

你在两个中转站买了 deepseek-v4 的额度。月底一个站说你花了 ¥47,另一个说 ¥89。你手里有 DSH 的会话日志,但日志里只有 provider 路由名和模型名,没有站点身份——你无法回答「这 ¥89 里有多少是我真的发出去的请求」。

TokenLedger 把 (中转站, Provider, 模型) 作为一等维度记录下来,再去读两个站自己的账单 API,把两边并排放,并明确标注这次比对的证据等级

等级含义
request有共享的请求标识,能一一对应
aggregate按站点、模型、时间窗聚合比对
summary站点只暴露累计额度/余额,无法细分

只有汇总数据时,界面不会假装是 request 级。估算费用、站点扣费、钱包余额、内部额度单位是四种不同的事实,永远不会被静默相加或换算。

现在能用的部分

import { foldUsage, bySite, byModel } from "dsh-tokenledger";
import { RelaySiteRegistry, createSiteResolver } from "dsh-tokenledger/relay-sites";

const registry = new RelaySiteRegistry([
  { id: "nine", type: "newapi",  baseUrl: "https://api.relay-one.example/v1" },
  { id: "sub",  type: "sub2api", baseUrl: "https://api.relay-two.example" },
]);

// DSH 的 provider 路由 → 它配置的 Base URL
const resolveSite = createSiteResolver(registry, {
  relayA:   "https://api.relay-one.example/v1/chat",
  official: "https://api.deepseek.com",
});

const days = foldUsage(sessionEvents, { resolveSite });

bySite(days);            // 按中转站汇总——对账用的 DSH 侧数字
byModel(days, {}, "nine"); // 只看某个站的模型分布

三个别人会做错的地方

这三条都有测试覆盖(test/usage.test.js),也是照抄现成实现时最容易漏的:

1. 请求失败了照样扣费。 用量除了挂在 assistant/message 上,也会从 assistant/chunk{type:'usage'} 流出。请求在报出 usage 之后失败,就永远等不到 assistant/message——但供应商已经收钱了。只订阅 assistant/message 会系统性少算这部分,而这恰恰是账单看起来偏高时最需要解释的部分。

2. 同一个 (turn, step) 会被报告两次。 后来的样本是替换前一个,不是累加。而且替换时必须从原先归属的那一天和那条路由里减回去——跨天、跨增量折叠边界时尤其容易错。

3. 孤儿 usage chunk 不带任何身份。 assistant/messagemessage.source 里自带 provider 和 model,但 StreamChunk 的 usage 变体只有 {type:'usage', usage}。所以失败请求的那条记录必须回退到最近一次 request/header 归因,且要认得 reason: 'resume'(进程重启会重发 header,那不是换模型)。归不上的记为显式 unknown,绝不猜。

另外:inputTokenscacheReadTokenscacheWriteTokens 三个桶互斥,相加才是计费输入(DSH 的适配器已经把 DeepSeek prompt_tokens 里的缓存命中减出去了);reasoningTokensoutputTokens 的子集,只做展示,加进总数就是重复计费。

中转站身份

站点用 Base URL 的精确 origin 识别,不从模型名猜。归属在折叠时就写死进记录——改了某个 provider 的 Base URL 只影响之后的调用,历史归属永不重写。

凭证只存引用,不存值;需要区分同一域名下的多把 key 时用不可逆指纹(credentialFingerprint)打标签。API Key 不进 URL、不进日志、不进用量行、不进诊断报告——查询串会漏进浏览器历史和反代日志,比 Authorization 头容易泄露得多。

状态

126 个测试,零运行时依赖(SQLite 用 Node 内置的 node:sqlite)。

模块状态
用量折叠(双事件源 / 替换语义 / 路由归因)
中转站注册表与 origin 归一化
区间 / 按模型 / 按站点查询
SQLite 汇总索引(按会话分行)与全量重建
增量 checkpoint、跨重启折叠等价性
费率表(生效日期 / 分桶计价 / 峰谷时段)
费用估算(未定价返回 null 而非 0)
CSV / JSON 导出与索引诊断
New API 适配器(余额 / 聚合 / 请求级 + 扣费复算)
Sub2API 适配器(余额 / 累计 / 双费用口径)
中转站软件指纹识别(零凭证)
对账引擎(证据等级 / 拒绝不可比)
接真实 DSH 会话日志✅ 已端到端验证
DSH 插件封装(dsh.bundle + Cordis 行)✅ 已在真实 DSH 里跑通
/tokenledger 报表命令
原生 settings 页面⬜ 见下
发 npm / 提交索引收录

原生页面为什么还没做

不是在等版本号。整个 DSH 都还在 rc——本项目宿主侧依赖的 dsh-sessiondsh-session-persistence,和客户端那套包,全都是 0.1.0-rc.6,同一个版本线。拿 rc 当客户端的门槛,对宿主侧就是双标。

(顺带一个坑:这些库包的 npm latest 标签还停在 0.0.1-rc.1,真正在用的版本在 next 上。npm view <包> version 读的是 latest,会给你一个过期的数字。)

而且他们两天发了 7 个版本、公开当天 3 小时内发了 3 次,没有任何稳定下来的迹象——这种东西不该进计划的里程碑。

真实的差别在依赖的性质:宿主侧依赖的是数据契约(事件形状、字段名),改了会破坏所有已存在的会话日志,所以它在物理上就很稳;客户端侧要依赖的是 8 个包的 React 接口,纯代码接口没有历史包袱,改起来没代价。再加上一条打包链和一套 Remote RPC。

而原生页面相对现在的文字报表,唯一增量是交互——筛选从敲参数变成点击。文字报表已经能回答全部问题。

所以触发条件是需求,不是版本号:有人说「报表能用,但我想点筛选器」的时候再做。

作为 DSH 插件安装

dsh plugin --profile web add github:zh667/TokenLedger

就这一条。 dsh plugin 转发给 pnpm;因为本包声明了 dsh.bundle,DSH 会自动把它登记进该 profile 的 dsh.profile.bundles,bundle patch 随即自动挂载插件行——不需要改 package.json,也不需要手写任何 YAML。

零配置即可用:所有调用归到 direct,按天按模型的报表已经是对的。

想要中转站维度,在该 profile 的 cordis.patch.yml 里加三行:

- id: tokenledger
  config:
    relays:
      my-route: https://relay.example.com/v1

my-routedsh-llm-pi-aiconfig.providers 下的键名,也就是每条 assistant 消息上 AssistantProvenance.provider 的值。这是唯一一个本代码推导不出来的东西——站点 id(取精确域名)和站点跑的哪套软件(指纹识别)都会自动得出。要覆盖就用长形式:

      my-route:
        baseUrl: https://relay.example.com/v1
        id: my-label
        type: sub2api

卸载:dsh plugin --profile web remove dsh-tokenledger

采集器扫描而不是订阅:listSnapshots() 的 revision 让未变动的会话零成本跳过,readFrom(id, seq) 只读尾部。订阅会把这段代码放进请求热路径,而且插件没运行时写入的一切会永久丢失——重启后静默少算。扫描是幂等且自愈的。

任何失败都是采集器的问题,不是 DSH 的:日志损坏、数据库锁住、上游改形状,全部降级成计数 + 一条日志 + 跳过该会话。记账值得做,但不值得让一轮对话失败。

端到端实测(2026-08-14,真实会话 + 真实中转站)

不是 fixture:一次真实的 agent 对话,真实扣费。

1. detect      api.<relay> -> newapi (confidence 1)      零凭证
2. fold        真实会话日志 -> input=10119 output=26 requests=1
3. relay       扣费 8991 quota(¥0.131269)
               用它自己的比率复算 = 8991,delta=0
4. reconcile   level=aggregate,token 差额全为 0
               扣费 ¥0.1312686 vs 估算 ¥0.131263(+0.000006)

最有价值的一条:替换规则在真实流量上生效了。同一个 (turn, step) 1/1 被报告了两次——一次在 assistant/chunk,一次在 assistant/message——折叠后 requests: 1 而不是 2。这是普通的一轮对话,不是边缘情况。任何只订阅一个来源、或者把两个来源相加的实现,在这里就已经错了。

另外发现一个坑:会话日志是 zstd 多帧拼接(每次 flush 一帧)。单次 zstdDecompressSync 只解第一帧然后静默返回一小部分——一个 11.9 KB 的文件看起来只有一行。直接读文件的实现必须按 28 B5 2F FD 帧头逐帧解。更好的做法是根本别直接读文件,走 sessionPersistence.readFrom()

对账引擎:重点是拒绝,不是相减

算出一个差额很容易,难的是知道什么时候这个差额是证据,什么时候它只是把两种根本不同的度量摆在一起的产物。四条规则:

等级取两侧较弱的那个。 DSH 侧永远是按天、按模型、按站点;中转站可能粗得多。而 request 级目前对任何站都不可达——DSH 的会话日志不记录供应商的 request id,所以就算站点给了也没法逐笔 join。

累计数回答不了带时间窗的问题。 只报生涯累计的站,不能拿去跟"最近 30 天"比——那等于让它为窗口之前的每一笔请求背锅。这种组合直接返回 comparable: false,除非 DSH 侧也是全量。

币种绝不换算。 估算是 CNY、扣费是 USD,那是两个事实;编一个汇率去相减,等于伪造用户来查的那个数。

缺失的值是 null 不是 0 零是一个测量结果。

还有一条给报表的:混合报表取最弱等级,否则一个只有 summary 的站会被当成已验证的看。

为什么是「每种软件一个适配器」,不是每个站点一个

站点身份来自 Base URL,不是 key——key 只证明你有权调用。而能拿到什么账单数据取决于这个站跑的是哪套软件。

中转站有成千上万个,中转站程序只有几种。所有跑 New API 的站都答 /api/status、都用内部 quota 单位;所有 Sub2API 都答 /v1/usage、都用真实货币。一个适配器覆盖该软件的全部部署,所以适配器数量跟的是软件生态,不是站点列表。而且这些程序很多互为分叉(One API → New API → VoAPI…),共享路由,一个适配器常能覆盖一整支。

识别不需要凭证——不存在的路由答 404,存在的答 401

端点                Sub2API   New API
/api/status           404       200
/v1/usage             401       404
/api/usage/token      404       401
/api/log/self         404       401

两组实测签名完全可分。不认识的站点也能用,只是少一半:detectRelaySoftware()unknown,对账降级为只有 DSH 侧数字——用量统计照常,只是没得比。绝不会把不认识的站硬套进已知适配器:用 New API 的 quota 换算去读 Sub2API 的余额,会得到一个自信的错数,比诚实的空白更糟。

两个站的账单形状差多少

New APISub2API
金额单位内部 quota 整数,要查 /api/status 换算真实货币,unit: "USD"
粒度请求级日志只有 today + 累计
prompt token缓存(OpenAI 口径)不含缓存(和 DSH 一致)
是否暴露比率是,扣费可独立复算
费用字段一个两个——costactual_cost

最后一行是实测里最要命的:同一批流量 cost: 0.33138075actual_cost: 0.231966525差 30%。一个是标价一个是实扣,合并成"费用"要么虚报要么把折扣藏掉。两个都原样带出,交给对账层决定问的是哪一个。

New API 适配器的实测结果

对一个真实运行的 New API 站点验证过(2026-08-14,只读):1960 条消费记录,全部能用记录自带的比率独立复算出扣费,0 条无法解释。

按计费约定匹配:openai 1415 · anthropic 535 · 仅兜底 10
请求级合计 quota 14,504,892  ==  聚合端点合计 14,504,892(两个独立端点互证)

复算公式(比率全部来自记录本身):

quota = round( (有效输入 + 输出×completion_ratio) × model_ratio × group_ratio )

「有效输入」正是坑所在——同一个站点存在两种语义:OpenAI 系的 prompt_tokens 包含缓存,Anthropic 系的不含且缓存创建单独计价。用错约定会算出负数

路线见 docs/ROADMAP.md

开发

npm test      # node --test,无运行时依赖

许可与致谢

MIT。src/usage.js 的折叠逻辑改编自 dsh-usage-stats(MIT),详见 NOTICE

本项目脱胎于已归档的 LanternDesk——那是一次桌面外壳尝试,放弃的原因写在这里:DSH 里所有值得做的能力都能做成插件,而插件做不到的部分(装包、拉进程、托盘)没有区分度,且原厂随时会补。