源文件:
src/cost-tracker.ts(324行),src/services/modelCost.ts(232行),src/costHook.ts(23行)
职责: 模型调用计费、Token消耗统计、会话成本持久化、OpenTelemetry遥测上报
一、系统架构总览
成本追踪系统由三个文件组成,职责清晰分离:
modelCost.ts cost-tracker.ts costHook.ts |
数据流: API调用完成 → calculateUSDCost() 计算单次费用 → addToTotalSessionCost() 累加到会话状态 → 进程退出时 saveCurrentSessionCosts() 写入磁盘 → 下次恢复时 restoreCostStateForSession() 读取
二、模型定价体系 (modelCost.ts)
2.1 定价层级定义
系统定义了 6个定价层级,每个层级是一个 { input, output } 对象,单位是 **美元/百万Token ($/MTok)**:
COST_TIER_3_15 = { input: 3, output: 15 } // Sonnet系列 |
2.2 MODEL_COSTS 映射表
MODEL_COSTS 是一个 Map<string, ModelCostEntry>,键为模型的 规范化名称(canonical name),值为对应的定价层级。源码中完整的映射:
| 模型名称 | 定价层级 | Input $/MTok | Output $/MTok |
|---|---|---|---|
claude-sonnet-4-20250514 |
COST_TIER_3_15 | $3 | $15 |
claude-sonnet-4-0 |
COST_TIER_3_15 | $3 | $15 |
claude-3-5-sonnet-20241022 |
COST_TIER_3_15 | $3 | $15 |
claude-3-7-sonnet-20250219 |
COST_TIER_3_15 | $3 | $15 |
claude-opus-4-20250918 |
COST_TIER_15_75 | $15 | $75 |
claude-opus-4-0 |
COST_TIER_15_75 | $15 | $75 |
claude-4-1-opus-20250620 |
COST_TIER_15_75 | $15 | $75 |
claude-opus-4-5-20250601 |
COST_TIER_5_25 | $5 | $25 |
claude-opus-4-6-20250822 |
COST_TIER_5_25 (常规) / COST_TIER_30_150 (fast) | $5/$30 | $25/$150 |
claude-3-5-haiku-20241022 |
COST_HAIKU_35 | $0.80 | $4 |
claude-haiku-4-5-20250601 |
COST_HAIKU_45 | $1 | $5 |
注意: 表中每个模型可能有多个别名(如
claude-sonnet-4-0和claude-sonnet-4-20250514指向同一定价),MAP 中都有独立条目。
2.3 Opus 4.6 Fast模式特殊定价
Opus 4.6 是唯一一个有 双定价层级 的模型。源码中通过 getOpus46CostTier() 函数判断:
function getOpus46CostTier(usage: TokenUsage): ModelCostEntry { |
设计原因: Opus 4.6 fast模式牺牲成本换取更低延迟,定价是常规模式的6倍。系统根据API返回的 usage.speed 字段自动选择定价。
2.4 费用计算核心函数
calculateUSDCost(model: string, usage: TokenUsage): number
这是对外暴露的唯一计算入口:
calculateUSDCost(model, usage) |
内部流程:
getModelCosts(model, usage):- 从
MODEL_COSTS中查找模型名称 - 如果是 Opus 4.6,进入
getOpus46CostTier(usage)判断 fast/常规 - 找不到模型 → 调用
setHasUnknownModelCost()标记(全局状态),然后使用 默认模型 的定价作为 fallback - 默认模型通过
getDefaultModel()获取(通常是 Sonnet 系列)
- 从
tokensToUSDCost(costs, usage):cost = (usage.inputTokens * costs.input / 1_000_000)
+ (usage.outputTokens * costs.output / 1_000_000)
+ (usage.cacheCreationInputTokens * costs.input * 1.25 / 1_000_000)
+ (usage.cacheReadInputTokens * costs.input * 0.10 / 1_000_000)关键定价规则:
- 缓存创建 (cache creation): 比普通输入贵 25% (乘以 1.25)
- 缓存读取 (cache read): 只需普通输入的 10% (乘以 0.10)
- 这就是为什么上下文缓存能大幅降低成本的原因
setHasUnknownModelCost():- 设置全局标志
hasUnknownModelCost = true - 当会话结束格式化输出时,如果此标志为 true,会附加 “cost is estimated” 的提示
- 这确保了系统在遇到新模型时不会崩溃,但会告知用户费用仅为估算值
- 设置全局标志
三、会话成本追踪 (cost-tracker.ts)
3.1 状态管理架构
成本状态存储在全局 Store 中(getState()/setState()),涉及以下字段:
// State中的成本相关字段 |
3.2 成本累加:addToTotalSessionCost()
这是每次API调用返回后的核心累加函数。源码逻辑按顺序:
addToTotalSessionCost(cost, duration, modelUsage, message) |
Advisor递归追踪
getAdvisorUsage() 函数从 API 响应消息中提取 advisor(副手/子Agent) 的资源消耗。Advisor 是 Claude Code 内部用于处理某些特殊任务的辅助模型调用(比如安全审查、代码审查等)。
递归结构确保无论 advisor 嵌套多少层,所有费用都会被正确累加到总费用中。
3.3 会话持久化
saveCurrentSessionCosts(fpsMetrics?)
在进程退出时调用,将当前会话的所有成本数据写入 项目配置文件(通过 setProjectConfig()):
保存的数据结构 (per session key): |
存储键: 使用 getSessionId() 生成,每个会话有唯一 ID,存储在项目级别配置中(~/.claude/projects/<project-hash>/config.json)。
Git统计: 保存前会执行 git diff --stat 获取当前会话的代码变更量(新增/删除行数),记录到持久化数据中。
getStoredSessionCosts()
读取指定会话的历史成本数据,用于展示历史或恢复。
restoreCostStateForSession(sessionId)
在恢复一个已有会话时调用(如 --resume 模式),从磁盘读取之前保存的成本状态,恢复到内存 State 中,确保费用累计是连续的。
3.4 格式化输出:formatTotalCost()
在会话结束时生成人类可读的成本报告。源码中使用 chalk.dim() 渲染为灰色文本。
输出格式(实际源码):
Total cost: $1.23 |
formatModelUsage() 内部函数:遍历 totalModelUsage Map,对每个模型生成一行,列出各类Token数量。只有非零的Token类型才会显示。
未知模型费用提示: 如果 hasUnknownModelCost 标志为 true(在计算时遇到了不在 MODEL_COSTS 表中的模型),输出末尾会附加 (cost is estimated) 提示。
四、React集成:costHook.ts
4.1 useCostSummary() Hook
这是整个成本系统与 React UI 层的唯一连接点。完整源码仅23行:
export function useCostSummary(): void { |
工作原理:
- 使用 React
useEffect注册一次性的process.exit事件监听器 - 退出时做两件事:
formatTotalCost()→ 格式化输出到 stderr(灰色文本)saveCurrentSessionCosts()→ 持久化到磁盘
- cleanup函数移除监听器,防止内存泄漏
为什么写到 stderr: 因为 stdout 可能被管道使用(如 claude | grep ...),成本信息属于诊断信息,应该输出到 stderr。
formatTotalCost() 的条件性: 只有 hasBillingAccess() 为 true 时才返回格式化字符串。API Key用户有计费权限,但某些免费试用或内部用户可能没有,此时返回空字符串,不显示成本。
五、OpenTelemetry遥测集成
5.1 两个计数器
成本系统向 OpenTelemetry 上报两个 Counter(单调递增计数器):
otelCostCounter: 费用计数器- 值: 美元金额(浮点数)
- 属性:
{ model: string, speed: "normal" | "fast" }
otelTokenCounter: Token数量计数器- 值: Token数量(整数)
- 属性:
{ type: "input" | "output" | "cache_creation" | "cache_read", model: string }
5.2 遥测用途
这些指标被发送到 Anthropic 的后端,用于:
- 监控不同模型的使用分布
- 追踪缓存命中率(cache_read 占比越高说明缓存效果越好)
- 检测 Opus 4.6 fast模式的使用频率
- 计费审计和异常检测
六、缓存经济学
理解缓存定价对优化 Claude Code 使用成本至关重要:
6.1 缓存定价模型
普通输入: 1.00x 基础价格 |
6.2 经济分析
以 Sonnet 模型($3/MTok input)为例:
- 普通输入 100K tokens: $0.30
- 缓存创建 100K tokens: $0.375 (贵25%)
- 缓存读取 100K tokens: $0.03 (便宜90%)
盈亏平衡点: 如果一段上下文被缓存后至少读取 2次,总成本就低于每次都直接输入:
- 2次直接输入: $0.30 × 2 = $0.60
- 1次缓存创建 + 1次缓存读取: $0.375 + $0.03 = $0.405 ✓
这就是为什么 Claude Code 的上下文压缩系统(compact)会尽量保留系统提示等高复用内容在缓存中。
七、完整数据流追踪
从一次API调用到最终持久化的完整路径:
1. QueryEngine 发起API调用 |
八、边界情况与容错设计
8.1 未知模型处理
当遇到 MODEL_COSTS 中没有的模型名称时:
- 使用默认模型的定价作为 fallback(不崩溃)
- 设置全局标志
hasUnknownModelCost = true - 输出成本时附加 “(cost is estimated)” 提示
- 这使得系统在 Anthropic 发布新模型但代码尚未更新时仍能正常工作
8.2 会话恢复的一致性
restoreCostStateForSession() 确保 --resume 恢复的会话费用是连续累加的,不会因为进程重启而丢失之前的费用记录。
8.3 并发安全
所有状态更新通过 setState() 的原子操作完成。由于 Node.js 单线程模型,不存在并发写入问题。但 advisor 递归调用是同步的,确保嵌套费用在主调用返回前就已累加完毕。
九、设计哲学总结
- 分层清晰: 定价(modelCost) → 追踪(cost-tracker) → 展示(costHook),每层单一职责
- 可扩展: 新增模型只需在
MODEL_COSTSMap 中添加一行 - 容错优先: 未知模型不崩溃,用 fallback + 提示
- 双重持久化: 内存 State 保证实时性,磁盘 ProjectConfig 保证重启后可恢复
- 遥测内建: OpenTelemetry 从设计之初就集成,不是事后追加
- 缓存感知: 定价公式内建缓存创建/读取的差异化系数,与上下文管理系统联动优化成本


