涉及源码 :
rust/crates/runtime(usage.rs、conversation.rs、hooks.rs、config.rs、permissions)、rust/crates/api(流式Usage)、rust/crates/plugins(插件HookRunner)、rust/crates/claw-cli(/cost、流事件);Python 侧src/query_engine.py、src/cost_tracker.py、src/costHook.py。
1. 先把概念拆开:「计量」≠「美元估算」≠「预算截断」
本仓库里与「成本」相关的代码至少有三层含义:
| 含义 | 主要位置 | 企业用途 |
|---|---|---|
| 提供商返回的 token 用量(事实数据) | api::Usage、流事件中的 usage |
计费分摊、配额、审计日志的原始计量 |
| 会话内累计与展示 | runtime::UsageTracker、TokenUsage |
会话级看板、/cost、JSON 输出 |
| 美元估算 | runtime::usage 里 pricing_for_model + estimate_cost_usd* |
仅供参考的 rough cost,不宜单独作为法务计费依据 |
策略注入则再分两类:
| 类型 | 机制 | 典型用途 |
|---|---|---|
| 同步、进程内授权 | PermissionPolicy::authorize |
允许/拒绝某工具调用(可带 PermissionPrompter) |
| 可外置脚本扩展 | HookRunner(PreToolUse / PostToolUse) |
DLP、审计、在工具执行前后跑组织脚本(退出码 2 = 拒绝) |
下文按源码真实挂载点说明「该挂哪一层」。
2. 计量(Cost / Usage):数据从哪来、在哪聚合
2.1 源头:API 流里的 Usage
上游响应类型里携带 Usage(input/output/cache 等),例如 MessageDeltaEvent:
rust
// 149:164:rust/crates/api/src/types.rs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
#[serde(default)]
pub cache_creation_input_tokens: u32,
#[serde(default)]
pub cache_read_input_tokens: u32,
pub output_tokens: u32,
}
impl Usage {
#[must_use]
pub const fn total_tokens(&self) -> u32 {
self.input_tokens + self.output_tokens
}
}
企业建议 :若要做多租户计费或合规报表 ,应在可靠边界 上记录「提供商返回的 usage 快照」:例如在 API 客户端实现层 (把 StreamEvent 解析为 AssistantEvent::Usage 之处)或 紧接其后的 runtime ,带上 request_id / session_id / tenant_id(需自行扩展)写入日志或导出管道。不要只在 CLI 打印里存在。
2.2 聚合:ConversationRuntime + UsageTracker
运行时在一次 stream 后构建 assistant message,并把本轮 TokenUsage 记入 tracker:
rust
// 178:182:rust/crates/runtime/src/conversation.rs
let events = self.api_client.stream(request)?;
let (assistant_message, usage) = build_assistant_message(events)?;
if let Some(usage) = usage {
self.usage_tracker.record(usage);
}
UsageTracker 维护「最近一轮」与「累计」,并可从已有 Session 消息重建(便于恢复会话后继续统计):
rust
// 163:194:rust/crates/runtime/src/usage.rs
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UsageTracker {
latest_turn: TokenUsage,
cumulative: TokenUsage,
turns: u32,
}
impl UsageTracker {
#[must_use]
pub fn from_session(session: &Session) -> Self {
let mut tracker = Self::new();
for message in &session.messages {
if let Some(usage) = message.usage {
tracker.record(usage);
}
}
tracker
}
pub fn record(&mut self, usage: TokenUsage) {
self.latest_turn = usage;
self.cumulative.input_tokens += usage.input_tokens;
...
self.turns += 1;
}
}
企业建议 :会话级计量 的 canonical 挂点即 ConversationRuntime 内 UsageTracker :所有经该 runtime 的 turn 都会更新同一套累计。若在 HTTP server 或 LSP 等多入口后面复用同一 runtime 模型,从这里暴露 usage() 给外层,比在各入口重复解析流更一致。
2.3 美元估算:挂在 usage 模块即可,不要当唯一真相
TokenUsage::estimate_cost_usd* 与 pricing_for_model 使用字符串匹配模型名(haiku/sonnet/opus)和默认单价,用于摘要行与 /cost 类展示。
企业建议 :财务级计费 应使用合同价、折扣、批次价目表,放在 计费服务或网关 ;仓库内 USD 估算适合 开发者可见性 与 粗略告警 ,与 token 原始计量 解耦。
2.4 Python 移植工作区:QueryEnginePort 与 cost_* 占位
QueryEngineConfig.max_budget_tokens 在 submit_message 里用投影用量做简单截断,语义是「port 模拟」而非真实提供商 usage:
python
# 87:94:src/query_engine.py
projected_usage = self.total_usage.add_turn(prompt, output)
stop_reason = 'completed'
if projected_usage.input_tokens + projected_usage.output_tokens > self.config.max_budget_tokens:
stop_reason = 'max_budget_reached'
...
self.total_usage = projected_usage
CostTracker / apply_cost_hook 仅为极简占位,与 Rust 主路径无运行时耦合:
python
# 6:13:src/cost_tracker.py
@dataclass
class CostTracker:
total_units: int = 0
events: list[str] = field(default_factory=list)
def record(self, label: str, units: int) -> None:
self.total_units += units
self.events.append(f'{label}:{units}')
结论 :企业若以 Rust runtime + 真实 API 落地,不要把 Python 的 QueryEnginePort 当作计量或预算的权威实现;它更适合 parity / 文档与测试镜像。
3. Hook:策略注入该挂在哪一层?
3.1 执行顺序(硬事实)
在 PermissionOutcome::Allow 分支中,顺序为:
PermissionPolicy::authorize(可带交互式 prompter)hook_runner.run_pre_tool_use(可拒绝)tool_executor.executehook_runner.run_post_tool_use(可标记错误/附加反馈)
rust
// 201:246:rust/crates/runtime/src/conversation.rs
for (tool_use_id, tool_name, input) in pending_tool_uses {
let permission_outcome = if let Some(prompt) = prompter.as_mut() {
self.permission_policy
.authorize(&tool_name, &input, Some(*prompt))
} else {
self.permission_policy.authorize(&tool_name, &input, None)
};
let result_message = match permission_outcome {
PermissionOutcome::Allow => {
let pre_hook_result = self.hook_runner.run_pre_tool_use(&tool_name, &input);
if pre_hook_result.is_denied() {
...
} else {
let (mut output, mut is_error) =
match self.tool_executor.execute(&tool_name, &input) {
Ok(output) => (output, false),
Err(error) => (error.to_string(), true),
};
...
let post_hook_result = self
.hook_runner
.run_post_tool_use(&tool_name, &input, &output, is_error);
...
因此:
- 需要保证「未授权绝不执行工具」 → 必须用
PermissionPolicy(或在其之上封装企业策略),不能单靠 hook。 - 需要在执行前后跑外部队列/脚本、扫描输入输出 → 用
HookRunner(配置见下)。
3.2 配置挂载:RuntimeFeatureConfig → RuntimeHookConfig
Hook 命令列表挂在特性配置里,与插件、MCP 等并列:
rust
// 47:62:rust/crates/runtime/src/config.rs
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeFeatureConfig {
hooks: RuntimeHookConfig,
plugins: RuntimePluginConfig,
mcp: McpConfigCollection,
...
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeHookConfig {
pre_tool_use: Vec<String>,
post_tool_use: Vec<String>,
}
ConversationRuntime::new_with_features 用其构造 HookRunner:
rust
// 126:144:rust/crates/runtime/src/conversation.rs
pub fn new_with_features(
...
feature_config: RuntimeFeatureConfig,
) -> Self {
let usage_tracker = UsageTracker::from_session(&session);
Self {
...
hook_runner: HookRunner::from_feature_config(&feature_config),
}
}
企业建议 :组织级 hook 清单 (只读挂载、镜像内固定路径)适合从 合并后的 RuntimeFeatureConfig 注入(例如全局配置 + 项目配置 merged),避免每个入口(CLI / server)各写一份。
3.3 拒绝语义:退出码 2
Runtime 侧子进程 hook:0 放行,2 明确拒绝,其它非零为 warn(默认仍允许继续,与插件 crate 行为一致思想):
rust
// 185:195:rust/crates/runtime/src/hooks.rs
match output.status.code() {
Some(0) => HookCommandOutcome::Allow { message },
Some(2) => HookCommandOutcome::Deny { message },
Some(code) => HookCommandOutcome::Warn {
message: format_hook_warning(
command,
code,
message.as_deref(),
stderr.as_str(),
),
},
企业建议 :把「必须拦下」的规则放在 exit 2 的 pre-hook,或 PermissionPolicy;仅告警用非 0 非 2。
3.4 第二套 Hook:plugins crate 的 HookRunner
插件清单聚合出的 PluginHooks 也有独立的 HookRunner(plugins/src/hooks.rs),与 runtime 配置型 HookRunner 是两条线。若企业同时启用 插件生态 与 全局 shell hook ,需要明确 合并顺序与优先级 (当前架构下应由集成方在组装 RuntimeFeatureConfig / 插件 registry 时统一设计)。
4. 直接回答:企业落地「该挂哪一层」?
4.1 计量(Metering)
| 需求 | 推荐挂点 | 原因 |
|---|---|---|
| 记录每轮 LLM 的 provider usage | API 客户端 → AssistantEvent::Usage → UsageTracker::record |
与对话状态一致,避免多入口重复解析 |
| 会话/用户/租户累计 | runtime 外露 usage() + 外层(server/gateway)写审计 |
tracker 不自带 tenant,需边界补全上下文 |
| 美元展示与粗告警 | usage.rs 估算 |
实现已有;与合同计费分离 |
| 硬预算截断 | 建议在 runtime 或 API 前增加策略 (本仓库无统一 max_budget 与 Rust runtime 对齐) |
Python port 仅有模拟;生产需单独设计 |
4.2 策略注入(Policy)
| 需求 | 推荐挂点 | 原因 |
|---|---|---|
| 工具白名单/黑名单、模式(如只读) | PermissionPolicy + PermissionPrompter |
在 hook 之前执行,语义是「授权」 |
| 组织脚本、DLP、审计流水 | RuntimeHookConfig + HookRunner |
已有 Pre/Post、stdin JSON + 环境变量、exit 2 拒绝 |
| 与插件包绑定 | plugins::HookRunner + 插件 registry |
适合分发可版本化的扩展 |
4.3 不建议的挂点
- 仅 在 CLI 打印或某一条 HTTP 路由里各自算 usage:易与
ConversationRuntime累计不一致。 - 仅用 USD 估算做扣费。
- 仅 用 PostToolUse 做「绝不能执行」的决策(执行已发生);强约束应放在 Permission 或 PreToolUse。
5. 小结
- 计量 :以
api::Usage为事实来源 ,在ConversationRuntime的UsageTracker做会话聚合;企业租户与审计在 server/gateway 边界 补全上下文并落库。usage.rs的美元数适合展示与粗算,不宜单独作为计费真相。 - 策略 :授权 挂在
permissions;可扩展、可外置脚本 挂在RuntimeHookConfig驱动的HookRunner,并注意与plugins钩子体系的边界。 - Python
costHook/QueryEnginePort:parity 与模拟为主,不是 Rust 企业主路径的计量与策略核心。