Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

成本跟踪

ZeroClaw 会将每一次携带 token 的模型调用记录到一个只追加的账本中,包括定价信息仅部分可用的调用。它将开销归因到发起该调用的智能体,并针对已定价部分执行每日/每月预算限制,同时向运维人员展示开销情况以及缺失定价的风险敞口。定价规则位于配置文件中,运维人员无需重新构建即可对其进行编辑。

本页介绍 schema、查找流程以及操作接口。代码位于 crates/zeroclaw-config/src/cost/ 和 crates/zeroclaw-runtime/src/agent/cost.rs。

配置模式

有两个相关部分负责管理该层面。cost 涵盖预算强制执行和记录行为。cost.rates.* 是由运营方管理的费率表;每个子部分的点分路径都与匹配的 providers.* 路径相对应,只是将末尾的 <alias> 段替换为正在计价的上游资源。

为什么键是资源 ID,而不是别名

[providers.models.anthropic.<alias>] 条目以操作者选定的别名(如 glados、production)作为键,该别名需符合别名验证器的规则:小写 ASCII 字符、单个下划线、不含连字符。[cost.rates.providers.models.anthropic.<resource>] 条目则以上游模型 id作为键,即其在用量遥测数据中呈现的形式(如 claude-opus-4-7、gpt-4o-mini、whisper-1):这些 id 字符串来自提供方的命名空间,几乎总是包含连字符。

schema 会用 #[resource_key] 标记每一个费率表 HashMap(位于 crates/zeroclaw-macros/src/lib.rs 中)。该属性会使字段在 create_map_key / rename_map_key 中跳过 validate_alias_key,因此网关的 POST /api/config/map-key 能够接受带连字符的 id。如果没有该属性,create_map_key 会拒绝每一个实际可用的模型 id,费率表 UI 也将无法正常工作。别名和资源 id 共享相同的磁盘结构(HashMap<String, T>),但它们是采用不同验证器的不同命名系统。

槽位列表是唯一可信来源

[cost.rates.providers.models.<type>]、[cost.rates.providers.tts.<type>] 和 [cost.rates.providers.transcription.<type>] 下的每种 provider 类型的插槽,由驱动 [providers.*] 插槽包装器的相同宏展开而来:

#![allow(unused)]
fn main() {
// crates/zeroclaw-config/src/providers.rs
for_each_model_provider_slot!(emit_model_cost_rates_struct);
for_each_tts_provider_slot!(emit_tts_cost_rates_struct, super::schema::TtsCostRates);
for_each_transcription_provider_slot!(emit_transcription_cost_rates_struct, super::schema::TranscriptionCostRates);
}

添加新的模型提供商类型只需在 for_each_model_provider_slot! 中增加一行;费率表插槽、提供商配置插槽以及仪表盘下拉菜单都会基于它自动展开。无需手写分发表,也无需在前端维护并行的字符串列表。

请求时的定价

从 [cost.rates.*] 到记录的 cost_usd 值的流程如下:

  1. Orchestrator 启动时构建定价映射。 当通道管理器为某个代理实例化运行时上下文时,它会遍历 config.cost.rates.providers.models.iter_entries(),并将费率合并到 HashMap<provider_type, HashMap<key, f64>> 中,其中 key 为 "<model_id>.input"、"<model_id>.output" 或 "<model_id>.cached_input"。旧版的按别名定义的 [providers.models.<type>.<alias>].pricing 表也会被合并进来;冲突时以 [cost.rates.*] 为准,因为它是面向未来的接口。(参见 crates/zeroclaw-channels/src/orchestrator/mod.rs 中 cost_tracking: CostTracker::get_or_init_global(...).map(|tracker| ...) 下的闭包。)

  2. 在 agent 循环内记录。 每次成功的 LLM 响应都会触发 crates/zeroclaw-runtime/src/agent/cost.rs 中的 record_tool_loop_cost_usage(provider_name, model, usage)。该函数会获取 provider_name 对应的定价映射槽位,调用 resolve_rates(map, model),乘以 token 数量,并通过全局 CostTracker 存储 CostRecord。

  3. 每维度的解析与校验。 resolve_rates_opt 首先尝试模型 id,然后对于 provider/model 形式的字符串尝试路径后缀形式(因此若运营者只存储了简短形式,anthropic/claude-opus-4-7 会退化为 claude-opus-4-7)。它为每个维度保留一个 Option,使实时定价和全局目录仅填充缺失项。显式配置的 0.0 输入或输出费率保持已配置且免费。0.0 的缓存输入费率同样视为已配置,但并非免费:现有的成本构造函数将其视为“无缓存折扣”,并按标准输入费率计费缓存 token。负数、非有限值或超过共享的每配置单位 $1,000,000 安全上限的不合理大额费率,即使旧版或宽松的配置加载路径放行了它们,也会在记录边界被视为不可用。

    缺失的输入、缓存输入和输出维度仅在该维度包含 token 时才进行评估。一次性运行时警告会指出不完整的维度。因此,部分计价的调用会记录其已知成本,并单独记录无法计价的 token 子集。

  4. CostTracker 是进程全局单例(crates/zeroclaw-config/src/cost/tracker.rs 中的 OnceLock)。重新加载会将最新的 CostConfig 应用于现有的跟踪器;如果在启动时禁用了成本跟踪,那么后续在重新加载时将 cost.enabled = true 会按需构建该跟踪器。编排器的定价映射也会在每次守护进程重新加载时根据当前配置重建,因此费率修改会在重新加载后的下一次请求中生效。

来自网关的实时定价

运维人员不必为每个模型手动维护费率。提供方可以通过在该 provider 配置块中设置 live_pricing = true 来选择直接从其自身网关拉取 token 价格(同时保留其现有的 api_key 和 model 设置);价格来自网关自己的 /models 列表。

行为:

  • 网关是主要来源。 提供方现有的 /models 端点(即 onboarding 用来列出模型的同一个端点)会被解析以获取每个模型的定价。那些在此处发布价格的网关会将其报告为按 token 计的十进制字符串(OpenRouter 和 Kilo 的 pricing{prompt,completion,...}),并按每 1M token 的美元价格进行换算。若某个网关的 /models 列表根本不包含任何定价(例如 opencode zen,只列出 model id),则由下面的 models.dev 回退方案覆盖。不会重复保存端点 URL 或凭据:它们从提供方现有配置中读取。
  • models.dev 回退。 网关未定价的模型(或者根本没有 HTTP /models 列表的提供方,例如像 kilocli 这样的子进程网关)会回退到公共 models.dev 目录(api.json),并以该模型家族的 models.dev 名称为键(参见 crates/zeroclaw-providers/src/catalog.rs 中的 catalog_source_for)。回退目录会在每个刷新周期重新获取,因此这两个来源都会以相同的每小时节奏跟踪上游价格变动。
  • 配置始终优先。 实时报价仅填充模型没有 [cost.rates] / pricing 条目的维度。已配置的费率(包括有意设置的 0.0)绝不会被覆盖。这是补缺机制,不是替代。
  • 每个 gateway 只调用一次,仅限标记的模型。 共享同一 gateway 的别名会去重为一次 /models 获取;在该响应中,只会为每个已选择加入的别名填充其各自配置的 model,而不会填充该 gateway 列出的所有模型。
  • 后台刷新,绝不阻塞。 单个任务每小时刷新一次进程范围内的价格快照。成本记录路径同步读取缓存的快照,并且绝不会在内联时发起网络调用,因此慢速网关不会阻塞请求计费。
  • 默认关闭。 在没有提供者设置 live_pricing = true 的情况下,不会有刷新任务,也不会有网络流量;行为与未启用该功能的构建完全相同。运行时关闭最后一个已标记的提供者(配置重新加载)会在下一次刷新周期清除快照,因此实时价格会停止填充而无需重启。快照仅存在于 zeroclaw_providers::pricing(见 crates/zeroclaw-providers/src/pricing.rs);它由 record_tool_loop_cost_usage 读取,并且只会在 channels supervisor 和 gateway 启动时各生成一次。

像 [cost.rates] 一样,实时价格只会影响快照生成后发起的请求;不会对过去的记录进行追溯性重新计价。

持久化

CostTracker::record_usage_with_agent 为每个包含 token 的响应向 <workspace>/state/costs.jsonl 追加一条 CostRecord,每行一个 JSON 对象。启动时会读取该账本,以便仪表盘中当月按 agent 统计的汇总在重启后依然保留。

Each TokenUsage 记录包含两个定价来源(provenance)字段:

  • unpriced_tokens:指其有效费率不可用的输入、缓存输入或输出 token 子集。对于已完全计价的调用,其值为 0,包括以刻意设置的免费 0.0 输入或输出费率计价的调用,以及其 0.0 缓存输入费率回退为标准输入费率的调用。
  • pricing_available:该来源信息的兼容性摘要。当 unpriced_tokens > 0 时,新记录会将其设置为 false。

在这些字段出现之前写入的账本行,反序列化时会得到 pricing_available = true 和 unpriced_tokens = 0。因此,旧版账本行的零花费并不能证明当时的定价信息可用。缺失的来源信息事后无法安全地重建。

cost_usd 是在记录时根据当时生效的费率表计算得出的。记录是不可变的:如果操作员在某些请求已被记录之后才添加费率,那么这些已有记录将保持 cost_usd = 0。只有在费率配置完成之后(并且守护进程已重新加载,使编排器的定价映射重建)发出的请求才会带有非零成本。

首次启用费率表后,这是最常见的意外情况。解决办法是等待新的请求;不会对已有请求进行追溯重新定价。

预算执行

CostConfig::enforcement.mode 决定当预计成本会使 daily_total 或 monthly_total 超出配置的限额时的处理方式:

  • warn:默认值;以 warn 级别日志记录该事件并放行请求。
  • block:使用 BudgetExceeded 错误拒绝该请求。
  • route_down:将 route_down_model(一个更廉价的替代方案)替换原始模型。该替换在请求被分发之前发生。

allow_override = true 允许请求通过在 CLI 上传递覆盖令牌(zeroclaw --override)来绕过 block。默认为 false。warn_at_percent 控制网关在达到硬性限制前何时显示警告横幅;默认为 80%。

预算比较使用已记录的 cost_usd,因此无法计入 unpriced_tokens 子集。当当月存在未计价的 token 时,每日或每月总额低于上限并不能保证安全。在依赖上限执行之前,请先配置缺失的费率维度并生成新的请求;现有账本记录永远不会被追溯重新计价。

按代理归属

当 cost.track_per_agent 为 true(默认)时,每条记录的 CostRecord 都会携带发起该记录的 agent 别名。仪表板的 Spend by agent 面板和 GET /api/cost?agent=<alias> 会使用此字段。设置 track_per_agent = false 是针对高流量安装环境的一项优化——在这类环境中,额外的 HashMap 聚合会在性能分析中显现出来;其代价是在所有位置都会丢失按 agent 区分的维度。

运算符表面

CLI 状态

zeroclaw status 会从账本中打印当天和当前月份的支出。当任一模型在当前 UTC 月内记录了 unpriced_tokens > 0 时,它还会打印一条定价不可用警告,列出受影响的模型以及这些模型未计费的 token 总数。该警告读取的是按月汇总的模型统计(CostTracker::get_current_month_model_stats),而不是仪表板使用的按日 by_model 明细,因此本月较早某天产生的未计费用量在 UTC 日期切换后以及重启后仍然可见。该警告有意独立于总支出金额:混合了已计费与未计费记录的行仍会显示,而完全配置为免费的模型则不会触发任何警告。

这条警告表示,显示的花费是下限,剩余的上限余量是上界,而非完整的统计结果。旧版记录不会触发该警告,因为其历史定价来源未知。

配置界面

  • /config/cost → Limits 选项卡:所有扁平的 [cost].* 字段(enabled、limits、enforcement、track_per_agent)。费率表行不在此处编辑,它们与拥有该模型的提供商绑定,因此位于下一层级。
  • /config/providers.<category>/<type> → Costs 标签页:该提供商类型的费率表编辑器。+ Add 输入会从所有已配置别名的 providers.<category>.<type>.*.model 中推荐上游资源 id,因此操作员可以为他们实际绑定的每个模型一键添加一行费率。这是编辑 [cost.rates.providers.<category>.<type>.*] 的唯一入口。

仪表盘

仪表盘的费用选项卡显示三个面板以及一个时间窗口选择器(今天/最近 7 天/最近 30 天/本月/全部时间):

  • 支出总计:来自 costs.jsonl 的每日和每月总计。
  • 按 agent 统计的开销 · <window>:在所选窗口内按 agent 进行的汇总统计。当 track_per_agent 为 true 时可见。
  • 按模型统计费用 · <window>:按模型汇总。每行的模型 ID 均可点击;点击后会根据已配置的别名解析出所属的提供方类型,并跳转到该提供方的 Costs 选项卡。当模型 ID 未绑定到任何已配置的提供方时,点击不会有任何响应(孤立模型没有对应的费率表路由)。

网关

  • GET /api/cost:当前的 CostSummary(与仪表板的成本概览结构一致)。添加 ?agent=<alias> 以查看单个代理的视图。
  • GET /api/config/templates:模式所注册的每个以映射为键的区块,供 Rates 选项卡的“类别 × 提供商类型”下拉菜单使用。
  • POST /api/config/map-key?path=cost.rates.providers.<category>.<type>&key=<resource> 创建一个新的费率行。如果不存在此类映射段,则该路径会被拒绝;资源键传递的是 #[resource_key] 而非 validate_alias_key。

故障排除

配置费率后仪表盘上所有 agent 仍显示 $0.0000。 旧记录是不可变的,由于其发生时尚未设置费率,所以以 cost_usd = 0 进行了记录。在守护进程重新加载后发起一次新的聊天请求,然后查看 Cost overview > Session 以及 Spend by model;两者都应针对该新请求填充数据。

zeroclaw status 显示定价不可用,即使已配置了部分费率。 定价是按携带 token 的维度分别解析的。例如,输入费率不会为输出 token 定价,缓存输入费率也不会为未缓存的输入定价。请添加运行时警告中指明的维度。已配置的 0.0 输入或输出费率是有效的免费费率,不会触发状态警告;已配置的 0.0 缓存输入费率会按标准输入费率计算缓存 token 的费用,同样不会触发该警告。

保存后检测到与 cost.rates.* 路径存在偏差。 v0.8.0 之前的守护进程会在 dirty-save 路径中错误处理带连字符的 HashMap 键,从而静默丢弃对费率表的每一次写入。如果你在 v0.8.0+ 上遇到此问题,那就是一个真正的 bug:dirty-path 解析逻辑位于 crates/zeroclaw-config/src/schema.rs::apply_dirty_path;请提交一个 issue,并附上守护进程版本以及发生偏差的路径。

missing_pricing 警告刷屏日志。 运行时会在每个 (provider_type, model) 组合上只发出一次此警告:当 resolve_rates_opt 合并已配置、实时及全局目录的费率后,某个带 token 的输入、缓存输入或输出维度仍未定价时,即会触发。警告会列出未定价的维度;因此,即使某个模型的其他维度已有定价,只配置了部分费率的模型仍可能触发警告。只需为警告中指明的确切模型 ID 添加缺失的维度。部分提供商返回带版本号的 ID,例如 claude-3-5-sonnet-20241022,即使已配置的 ID 是 claude-3-5-sonnet;解析器在报告缺口之前,也会尝试其文档中记载的路径后缀候选项。