成本跟踪
ZeroClaw 会将每一次计费的 API 调用记录到一个仅追加的账本中,将开销归因到发起调用的代理,强制执行每日/每月预算,并在仪表盘的 Cost 选项卡中展示汇总数据。计费规则保存在配置文件中,因此运维人员无需重新构建即可对其进行编辑。
本页介绍 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 值的流程如下:
-
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| ...)下的闭包。) -
在 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。 -
resolve_rates_opt 先尝试模型 id,然后对
provider/model字符串尝试路径后缀形式(因此如果 operator 只保存了短格式,anthropic/claude-opus-4-7会退化为claude-opus-4-7)。它会针对每个维度返回一个Option,因此 operator 未配置的任何维度都可以在计费前由实时定价回退(见下文)补上。只有当 配置 和实时回退都使输入和输出保持为0.0时,才会触发一次性的missing_pricing警告,因此真正“我们无法为此定价”的记录仍会在日志中显现。 -
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 会为每个计费响应向 <workspace>/state/costs.jsonl 追加一条 CostRecord,每行一个 JSON 对象。启动时会读取该账本,因此仪表板的当月按代理汇总数据在重启后仍会保留。
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.track_per_agent 为 true(默认)时,每条记录的 CostRecord 都会携带发起该记录的 agent 别名。仪表板的 Spend by agent 面板和 GET /api/cost?agent=<alias> 会使用此字段。设置 track_per_agent = false 是针对高流量安装环境的一项优化——在这类环境中,额外的 HashMap 聚合会在性能分析中显现出来;其代价是在所有位置都会丢失按 agent 区分的维度。
运算符表面
配置界面
/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;两者都应针对该新请求填充数据。
保存后检测到与 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 警告刷屏日志。 当 resolve_rates 返回 (0.0, 0.0) 时,每个 (provider_type, model) 组合会触发一次该警告。可能是该模型未配置费率,也可能是上游返回的模型 id 与费率表中的不一致(某些供应商即使你配置的是 claude-3-5-sonnet,也会返回带版本号的 id,如 claude-3-5-sonnet-20241022)。请添加该警告所指明的确切 id,或者设置不带版本号的 id 并依赖 resolve_rates 的后缀匹配机制。