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 企业主路径的计量与策略核心。

相关推荐
小白跃升坊18 小时前
Codex 增强部署:基于 Codex++ 接入 DeepSeek
ai·ai编程·codex·deepseek·ai coding·codex++
AlfredZhao18 小时前
GPT 省钱,不是别用最新模型,而是别浪费缓存
gpt·ai
doiito21 小时前
【Agent Harness】Gliding Horse 本体论系统设计:给 AI Agent 装上“语义大脑”
ai·rust·架构设计·系统设计·ai agent
码哥字节1 天前
一周 30k+ stars 的 Skill 生态,3 个仓库代表 3 种工程师哲学
claude code·agent skills
小七-七牛开发者1 天前
周一上线 | SpaceX 收购 Cursor、支付宝进入 AI 时代、DeepSeek 完成 500 亿元融资
ai·agent·token·glm·智谱·claudecode·ai coding·周一上线
doiito2 天前
【Agent Harness】为什么我把 JSON‑LD “编译成 DAG” 后,整个 Agent 平台立刻聪明了
ai·rust·架构设计·系统设计·ai agent
码哥字节2 天前
我把整个代码库喂给 Claude Code,工具超 50 个就静默丢失,这个坑太阴了
mcp·claude code·ai编程工具
xiezhr2 天前
折腾半小时,终于让AI 能直接帮我写飞书文档了
ai·飞书·ai agent·飞书cli·飞书文档
岳小哥AI2 天前
Claude Fable和Claude Mythos 5同时发布:注意力机制下愈加强大的AI大模型
ai·ai基础
Artech2 天前
[MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案
ai·agent·maf·aicontextprovider·chathistorymemoryprovider·mem0provider