claw-code 源码分析:成本追踪(Cost)与 Hook——企业落地时,计量与策略注入该挂在哪一层?

涉及源码rust/crates/runtimeusage.rsconversation.rshooks.rsconfig.rspermissions)、rust/crates/api(流式 Usage)、rust/crates/plugins(插件 HookRunner)、rust/crates/claw-cli/cost、流事件);Python 侧 src/query_engine.pysrc/cost_tracker.pysrc/costHook.py


1. 先把概念拆开:「计量」≠「美元估算」≠「预算截断」

本仓库里与「成本」相关的代码至少有三层含义:

含义 主要位置 企业用途
提供商返回的 token 用量(事实数据) api::Usage、流事件中的 usage 计费分摊、配额、审计日志的原始计量
会话内累计与展示 runtime::UsageTrackerTokenUsage 会话级看板、/cost、JSON 输出
美元估算 runtime::usagepricing_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 挂点即 ConversationRuntimeUsageTracker :所有经该 runtime 的 turn 都会更新同一套累计。若在 HTTP serverLSP 等多入口后面复用同一 runtime 模型,从这里暴露 usage() 给外层,比在各入口重复解析流更一致。

2.3 美元估算:挂在 usage 模块即可,不要当唯一真相

TokenUsage::estimate_cost_usd*pricing_for_model 使用字符串匹配模型名(haiku/sonnet/opus)和默认单价,用于摘要行与 /cost 类展示。

企业建议财务级计费 应使用合同价、折扣、批次价目表,放在 计费服务或网关 ;仓库内 USD 估算适合 开发者可见性粗略告警 ,与 token 原始计量 解耦。

2.4 Python 移植工作区:QueryEnginePortcost_* 占位

QueryEngineConfig.max_budget_tokenssubmit_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 分支中,顺序为:

  1. PermissionPolicy::authorize(可带交互式 prompter)
  2. hook_runner.run_pre_tool_use(可拒绝)
  3. tool_executor.execute
  4. hook_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 配置挂载:RuntimeFeatureConfigRuntimeHookConfig

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 也有独立的 HookRunnerplugins/src/hooks.rs),与 runtime 配置型 HookRunner 是两条线。若企业同时启用 插件生态全局 shell hook ,需要明确 合并顺序与优先级 (当前架构下应由集成方在组装 RuntimeFeatureConfig / 插件 registry 时统一设计)。


4. 直接回答:企业落地「该挂哪一层」?

4.1 计量(Metering)

需求 推荐挂点 原因
记录每轮 LLM 的 provider usage API 客户端 → AssistantEvent::UsageUsageTracker::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 做「绝不能执行」的决策(执行已发生);强约束应放在 PermissionPreToolUse

5. 小结

  • 计量 :以 api::Usage 为事实来源 ,在 ConversationRuntimeUsageTracker 做会话聚合;企业租户与审计在 server/gateway 边界 补全上下文并落库。 usage.rs 的美元数适合展示与粗算,不宜单独作为计费真相。
  • 策略授权 挂在 permissions可扩展、可外置脚本 挂在 RuntimeHookConfig 驱动的 HookRunner ,并注意与 plugins 钩子体系的边界。
  • Python costHook / QueryEnginePort :parity 与模拟为主,不是 Rust 企业主路径的计量与策略核心。

相关推荐
marsh02063 小时前
31 openclaw微服务架构实践:构建分布式系统
微服务·ai·云原生·架构·编程·技术
数据知道5 小时前
claw-code 源码分析:OmX `$team` / `$ralph`——把 AI 辅助开发从偶发灵感变成可重复流水线
数据库·人工智能·mysql·ai·claude code·claw code
陌殇殇5 小时前
002 Spring AI Alibaba框架整合百炼大模型平台 — 聊天、文生图、语音、向量模型整合
人工智能·spring·ai
shuair5 小时前
openclaw对接飞书
ai·飞书·openclaw
饕餮争锋6 小时前
CLI为什么在大模型领域流行
后端·ai
花千树-0106 小时前
Java 接入多家大模型 API 实战对比
java·开发语言·人工智能·ai·langchain·ai编程
UXbot6 小时前
AI原型设计工具评测:从创意到交互式Demo,5款产品全面解析
前端·ui·设计模式·ai·ai编程·原型模式
wenha6 小时前
大模型基础(一):什么是LLM?
ai
GeeLark7 小时前
GeeLark 3月功能更新合集
ai·自动化·aigc