@deepseek-ai/dsh-token-meter 提供 ctx.tokenMeter,一个具备回放感知能力的单一计量服务,它回答基于持久化会话日志的两个问题:下一个请求要花多少钱(请求压力)以及 当前活动的对模型可见内容有多少(surface)。它从不依赖实时的适配器状态——一切皆由会话事件推导而来,因此具有确定性、可重启恢复,并且天然适合投影(projection)。
包
| 包 | 描述 |
|---|---|
@deepseek-ai/dsh-token-meter | "具备回放感知能力的 token 计量服务(ctx.tokenMeter)" |
它与 dsh-llm、dsh-session、dsh-compaction、dsh-session-projection、dsh-invariants 和 cordis 协同;唯一不在 workspace 内的依赖是 zod@^4.4.3。
计量什么
该服务计量的是 token,而不是金额。在这个 harness 里,"用量核算"被拆分为两个互补的视角:
- Provider 上报的用量 —— 每次模型调用返回的确切
usage块,存储在会话的assistant/message事件上,并折叠进tokenUsage投影(这些是 provider 返回的真实 token 计数)。 - 启发式压力/ surface ——
TokenMeter.measure(session, requestHeader?)回放日志,并对一个"surface"(有序的模型可见消息集合)加上一个"baseline"锚点定价。这是对一个请求成本的预测,用于压缩(compaction)与长上下文决策。
在整个 meter 中不存在逐 token 的价目表或美元成本估算——计费/成本不在这个 seam 的范围内。
数据模型
// packages/llm/token-meter/src/types.ts
export type TokenMeasurementBaseline =
| { readonly kind: 'none'; tokens: 0 }
| { readonly kind: 'estimated'; tokens: number } // full heuristic price
| { readonly kind: 'usage'; tokens: number; usage: Readonly<TokenUsage> } // provider-anchored
export interface TokenMeasurement {
logRevision: number // = next unread event seq
baseline: TokenMeasurementBaseline
surfaceDeltaTokens: number // signed repricing vs. the baseline anchor
totalTokens: number // non-negative current request+response pressure
surfaceTokens: number // heuristic total across the current surface
nodes: readonly TokenSurfaceNode[] // ordered (seq, tokens) per surface position
}基线(Baselines)
measure() 为请求请求头(规范请求信封,即 provider/model/system/tools)选取一条基线:
usage—— 当最近一次成功调用的规范信封与所给请求头匹配,且其 provider 总数不小于该调用的完整启发式价格(保守锚点规则)。provider 用量按原样复用,仅对自该调用以来的signed surface delta重新定价。estimated—— 否则,对信封(system prompt + tool schemas)与当前 surface 进行启发式重新定价。none—— 在存在任何 surface 为空的请求之前。
这样既避免了每次读取都支付启发式计算的代价,又保持了正确性:自锚点以来的新大消息会被加入;缩小了 surface 的压缩会被减去。
固定的启发式估算器
packages/llm/token-meter/src/estimate.ts 是一个刻意保持简单、密度固定的估算器(与 contextBreakdown 投影逐字共享,以保证两个 surface 一致):
| 元素 | 价格 |
|---|---|
| 密度 | 每 token 4 个字符(CHARS_PER_TOKEN) |
| 每个块的固定结构开销 | 4 个 token(BLOCK_OVERHEAD) |
| 每条消息的 role 字段框架 | 4 个 token(ROLE_OVERHEAD) |
| system prompt | ceil(system.length / 4) + ROLE_OVERHEAD |
| tool schemas | ceil(JSON.stringify(tools).length / 4) + BLOCK_OVERHEAD |
| 未知块 | 保守的结构性 JSON 价格 |
estimateMessage、estimateHeader、estimateContent 都是导出的纯函数,因此调用方无需会话即可为单条消息定价。
事件与事件循环如何喂给它
这里没有专门的 "usage 事件"。meter 会旁听普通的会话事件:
| 事件 | 在计量中的角色 |
|---|---|
request/header | 确立规范请求信封(system、tools、config) |
step/start / step/end | 定界一个模型步骤(校验 assistant 事件属于某个打开中的步骤) |
assistant/chunk | 原始流式块;一个 usage 块提供一个早期的 provider 样本 |
assistant/message | 终结该步骤;携带确定性的 usage(meter 的主要锚点) |
surface 事件(user/message、assistant/message、……) | 折叠进按 token 定价的 surface |
agent 循环(packages/core/agent-loop/src/agent.ts)通过 ctx.llm 流式接收块,分别以 assistant/chunk 追加,然后在步骤结束处以 assistant/message(带 usage 与 sourceEventSeqs)追加——这正是 meter 回放的内容。meter 复用 BlockAssembler,从引用的 chunk 序列重新组装 provider 输出,以在没有第二次流的情况下对 provider 用量与启发式价格进行比较。
投影(用量出现的位置)
通过可选的 ctx.sessionProjections 注册表,meter 注册三个纯回放投影:
| 投影 | 键 | 视图输出 |
|---|---|---|
| Token 用量 | tokenUsage | { uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } 按 turn/step 去重后的总计 |
| 上下文压力 | contextPressure | { contextWindow?, pressureTokens?, projectedTokens? } —— 来自最新用量样本加 surface 变动的 prompt 侧占用 |
| 上下文构成 | contextBreakdown | 当前 surface 的按来源定价(instructions/catalog/snapshot/…… 各行) |
tokenUsage 是 UI 读取累计模型用量所依赖的投影,contextPressure 则用于上下文窗口占用。两者都是"最后一个获胜"且累积时不重复计数:同一 turn/step 重复的 usage 会替换较早的值,而不是再次累加。
用量在 UI 中如何出现(简述)
packages/client/ui-settings-models与会话头部 surface 使用来自 API gateway 的session.models与sessionModelSelect,而非直接使用 meter。- trajectory/会话头部消费
tokenUsage与contextPressure投影(在 mux 事件流上以session/projection帧交付,并由历史尾部的 projections 块播种)。
meter 本身在 host 侧;client 通过会话投影流水线以及转发的 settings/document-updated/llm/adapters-updated 失效事件看到它。
包
| 包 |
|---|
@deepseek-ai/dsh-token-meter |
@deepseek-ai/dsh-llm(定义 TokenUsage) |
@deepseek-ai/dsh-session-projection(注册表) |
延伸阅读
- LLM 层 —— meter 回放的
usageStreamChunk、TokenUsage,以及assistant/chunk/assistant/message事件。 - DeepSeek provider —— 适配器
mapUsage如何产出 meter 消费的不相交计数。 - API gateway —— client 如何通过
session/projection帧与session.models接收用量。 packages/llm/token-meter/src/index.ts——TokenMeter、measure、_sync、基线选取。packages/llm/token-meter/src/estimate.ts—— 固定的启发式估算器。packages/llm/token-meter/src/usage-projection.ts——tokenUsage/contextPressure投影折叠。