六池归池记账 + /cost 花费查询 #89

Merged
Yushu merged 2 commits from feature/81-cost-accounting into main 2026-07-28 05:42:36 +00:00
Member

Closes #81

六池(reply / proactive / reflection / multimodal / embedding / delegate)归池记账,按池 + 本地日累计,/cost 查今日/本月,经 is_admin 校验。

AC 的一个前提不成立

AC 说「路径信息在调用点已知(RuntimeContext 及各调度入口已经区分得很清楚)」—— RuntimeContext 分不出 reply 与 proactive:反应式 / 即刻追问 / Callback / 环境信号四条路径都传 "reactive",只有 Drive Tick 不同。各调度入口确实知道,但那个枚举不知道。

所以归池键用 issue #79 为决策快照建的 TriggerPath(五条路径各有取值),经纯函数 cost.pool_of() 推导。额外收益:同一条触发的决策快照标签成本池从此由同一个取值推导,不可能各说各话;新增一条触发路径却忘了归池会被测试当场抓到。

四个 grill 决策

  • Callback 送回归 reply:它在兑现用户此前的请求(issue #53「履约不是能动性决策」),也因此继承 #82 的「前台回复优先保」—— 答应过的事不该因预算耗尽而永不兑现。
  • /diagnose 两个探针分别归 reply 与 embedding(#80 明确留给本片的口子)。
  • embedding 与 multimodal 按能力横切:所有 embed() 进 embedding 池、所有兜底多模态旁路进 multimodal 池,与发生在哪条路径无关(ADR 原话「按能力分池」;这两项通常独立 provider、独立计价,混进路径池会让「该关掉哪个功能省钱」失去答案)。
  • 计量包装器而不是在调用点记账:9 处 complete() 里好几处在共享 helper 里(漂移守卫、子智能体),同一处代码被不同池的调用方复用;包装器把归池绑在「调用方拿到的那个 client」上,新增调用点天然被覆盖

两条架构逼出来的口径

  • 定价表按池声明、不按模型:core 压根不知道某次调用用的哪个模型(#80 的发现 —— LLMClient 只有 complete(),provider/model 由 host 私有持有),能挂定价的最细粒度就是池。一个池里混用多价位模型时数字是估算,文档如实这么说。
  • 存 token 不存钱:钱是按池单价估出来的派生量,存 token 才能在配错价格后修正历史。代价是中途调价会重算历史 —— 明知的取舍。

issue #78 刻意留空的「累加时怎么对待 None」在这里定了:provider 没回传用量照样算一次调用、单独计入 calls_without_usage/cost 把它显示出来 —— 静默丢弃会让金额偏低而且没有任何迹象

两轴 review 后修掉的(第二个 commit)

  • 真漏洞:多模态兜底旁路请求游离在记账之外。 那些 client 由本插件按 config 自己现造(ADR-0028 没有独立 host 挂钩点),原样传进 RuntimeLoop 就用了 —— multimodal 池在对话路径上恒空。既有 e2e 抓不到:反应式测试不带媒体。已补真带媒体的测试。
  • /cost 原本零测试 —— AC「经 is_admin 校验」没有守卫。
  • 删掉一层不可达的防御accumulate 的「单个字段缺失」那支到不了(TokenUsage 两字段都是 int,两条边界都是全有或全无),而我那两条测试是靠违反声明类型构造输入才通过的。
  • 并发正确性record_pool_usage 原本是无锁 read-modify-write,而 (pool, day) 全局只有六个热行、每次 provider 调用都撞 —— 丢更新且无迹可循。改成数据库侧原子自增。
  • 其余:PricingEntry 裸元组 → 具名 frozen dataclass;两套存储的求和收敛进 cost.mergedelegate_llm_client 改必填;drive_tick 断言从 <= 收紧成精确等于 {"proactive"}(那条路本就不做 embedding)。

验证

1297 测试全绿。8 处变异逐条验过(主循环不记账 / 委托复用主池 / 反应式记错池 / 丢弃未回传用量 / embedding 不记账 / 未配定价显示 0 元 / 定价打错静默忽略 / 求和开区间),各自只挂对应测试。

其中两处第一次没杀掉测试delegate 池零覆盖、embedding 摘掉记账也没被抓到(断言写的是 <=,"压根没记账"同样满足)。两条都已补强后重验。

Closes #81 六池(reply / proactive / reflection / multimodal / embedding / delegate)归池记账,按池 + 本地日累计,`/cost` 查今日/本月,经 `is_admin` 校验。 ## AC 的一个前提不成立 AC 说「路径信息在调用点已知(`RuntimeContext` 及各调度入口已经区分得很清楚)」—— **`RuntimeContext` 分不出 reply 与 proactive**:反应式 / 即刻追问 / Callback / 环境信号四条路径都传 `"reactive"`,只有 Drive Tick 不同。各调度入口确实知道,但那个枚举不知道。 所以归池键用 **issue #79 为决策快照建的 `TriggerPath`**(五条路径各有取值),经纯函数 `cost.pool_of()` 推导。额外收益:同一条触发的**决策快照标签**与**成本池**从此由同一个取值推导,不可能各说各话;新增一条触发路径却忘了归池会被测试当场抓到。 ## 四个 grill 决策 - **Callback 送回归 reply**:它在兑现用户此前的请求(issue #53「履约不是能动性决策」),也因此继承 #82 的「前台回复优先保」—— 答应过的事不该因预算耗尽而永不兑现。 - **`/diagnose` 两个探针**分别归 reply 与 embedding(#80 明确留给本片的口子)。 - **embedding 与 multimodal 按能力横切**:所有 `embed()` 进 embedding 池、所有兜底多模态旁路进 multimodal 池,与发生在哪条路径无关(ADR 原话「按**能力**分池」;这两项通常独立 provider、独立计价,混进路径池会让「该关掉哪个功能省钱」失去答案)。 - **计量包装器**而不是在调用点记账:9 处 `complete()` 里好几处在共享 helper 里(漂移守卫、子智能体),同一处代码被不同池的调用方复用;包装器把归池绑在「调用方拿到的那个 client」上,**新增调用点天然被覆盖**。 ## 两条架构逼出来的口径 - **定价表按池声明、不按模型**:core 压根不知道某次调用用的哪个模型(#80 的发现 —— `LLMClient` 只有 `complete()`,provider/model 由 host 私有持有),能挂定价的最细粒度就是池。一个池里混用多价位模型时数字是估算,文档如实这么说。 - **存 token 不存钱**:钱是按池单价估出来的派生量,存 token 才能在配错价格后修正历史。代价是中途调价会重算历史 —— 明知的取舍。 issue #78 刻意留空的「累加时怎么对待 `None`」在这里定了:provider 没回传用量照样算一次调用、单独计入 `calls_without_usage`,`/cost` 把它显示出来 —— 静默丢弃会让金额偏低**而且没有任何迹象**。 ## 两轴 review 后修掉的(第二个 commit) - **真漏洞:多模态兜底旁路请求游离在记账之外。** 那些 client 由本插件按 config 自己现造(ADR-0028 没有独立 host 挂钩点),原样传进 RuntimeLoop 就用了 —— multimodal 池在对话路径上恒空。既有 e2e 抓不到:反应式测试不带媒体。已补真带媒体的测试。 - **`/cost` 原本零测试** —— AC「经 `is_admin` 校验」没有守卫。 - **删掉一层不可达的防御**:`accumulate` 的「单个字段缺失」那支到不了(`TokenUsage` 两字段都是 `int`,两条边界都是全有或全无),而我那两条测试是靠**违反声明类型**构造输入才通过的。 - **并发正确性**:`record_pool_usage` 原本是无锁 read-modify-write,而 `(pool, day)` 全局只有六个热行、每次 provider 调用都撞 —— 丢更新且无迹可循。改成数据库侧原子自增。 - 其余:`PricingEntry` 裸元组 → 具名 frozen dataclass;两套存储的求和收敛进 `cost.merge`;`delegate_llm_client` 改必填;drive_tick 断言从 `<=` 收紧成精确等于 `{"proactive"}`(那条路本就不做 embedding)。 ## 验证 1297 测试全绿。8 处变异逐条验过(主循环不记账 / 委托复用主池 / 反应式记错池 / 丢弃未回传用量 / embedding 不记账 / 未配定价显示 0 元 / 定价打错静默忽略 / 求和开区间),各自只挂对应测试。 其中**两处第一次没杀掉测试**:`delegate` 池零覆盖、`embedding` 摘掉记账也没被抓到(断言写的是 `<=`,"压根没记账"同样满足)。两条都已补强后重验。
reply / proactive / reflection / multimodal / embedding / delegate 六池,
按池+本地日累计,`/cost` 查今日/本月,经 is_admin 校验。

AC 的一个前提不成立,据此改了设计:AC 说"路径信息在调用点已知(RuntimeContext
及各调度入口已经区分得很清楚)",但 RuntimeContext **分不出 reply 与 proactive**
——反应式/即刻追问/Callback/环境信号四条路径都传 "reactive",只有 Drive Tick 不同。
所以归池由**各调度入口**指定(`_build_runtime_loop(pool=...)` 必填),不从
RuntimeContext 推导。

四个 grill 决策:
- Callback 送回归 reply(履约是在兑现用户此前的请求,issue #53"履约不是能动性
  决策";也因此继承 #82 的"前台回复优先保"——答应过的事不该因预算耗尽而永不兑现)。
- /diagnose 的两个探针分别归 reply 与 embedding(#80 明确留给本片的口子)。
- embedding 与 multimodal 按**能力横切**:所有 embed 进 embedding 池、所有兜底
  多模态旁路进 multimodal 池,与发生在哪条路径无关(ADR 原话"按能力分池")。
- 用**计量包装器**而不是在调用点记账:9 处 complete() 里好几处在共享 helper 里
  (漂移守卫、子智能体),同一处代码被不同池的调用方复用;包装器把归池绑在
  "调用方拿到的那个 client"上,调用点一行不改,新增调用点天然被覆盖。

一条架构逼出来的口径:**定价表按池声明、不按模型**。core 压根不知道某次调用用的
哪个模型(#80 的发现),能挂定价的最细粒度就是池;一个池里混用多价位模型时数字是
估算,文档如实这么说。存 token 不存钱:钱是派生量,存 token 才能在配错价格后修正
历史(代价是中途调价会重算历史,是明知的取舍)。

issue #78 刻意留空的"累加时怎么对待 None"这条策略在这里定了:provider 没回传用量
照样算一次调用、单独计入 calls_without_usage,`/cost` 把它显示出来 —— 静默丢弃会让
金额偏低**而且没有任何迹象**。

8 处变异逐条验过。其中**两处第一次没杀掉测试**,据此补强:
- delegate 池零覆盖 —— 把 `self._delegate_llm_client` 改回 `self._llm_client`
  全绿。补了一条真跑委托的测试。
- embedding 摘掉记账也没被抓到 —— 断言写的是 `<=`,"embedding 压根没记账"同样满足。
  收紧成 `==`。

1291 测试全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
真漏洞(两轴独立撞上,我在它们跑的同时也自查到了):
  `_build_fallback_multimodal_models` 造的 client **原样传进 RuntimeLoop**,而
  `_describe_media` 那次旁路请求是真实付费调用 —— 对话路径上的 multimodal 池
  恒空,直接违反 AC「不存在游离在记账之外的消耗块」。既有 e2e 抓不到:反应式
  那条测试不带媒体,旁路请求根本不会发生。计量挪进构造处,撤掉贴纸路径的二次
  包装(否则同一次调用记两遍),并补了一条真带媒体的测试。

AC「归池判断是纯函数」此前未兑现:池是各入口的字面量实参,没有任何 path→pool
函数。补 `cost.pool_of(trigger_path)`,键用 **issue #79 的 `TriggerPath`** 而不是
`RuntimeContext` —— 后者分不出 reply/proactive。额外收益:同一条触发的**决策快照
标签**与**成本池**从此由同一个取值推导,不可能各说各话。并补一条「每条 TriggerPath
都有归池」的测试,新增路径忘了归池会当场挂。

`/cost` 原本**零测试**(AC「经 is_admin 校验」没有守卫):补进权限参数表 + 三条
(列全六池、报真实花费、非管理员看不到花费数字)。

空转防御删除:`accumulate` 里「单个字段缺失按 0 计」那支**不可达** —— `TokenUsage`
两个字段都声明 `int`,两条边界都是「任一字段读不到就整体返回 None」。那两条测试
是靠违反声明类型构造输入才通过的。删防御、删测试、写清为什么到不了。

并发正确性:`record_pool_usage` 原本是无锁 read-modify-write,而 `(pool, day)` 全局
只有六个热行、每次 provider 调用都撞 —— 并发丢更新且**无迹可循**。改成数据库侧原子
自增,首次插入撞主键则回到自增重试。

其余:`PricingEntry` 从裸元组换成有名字的 frozen dataclass(config→core 的转换有
名字,同 `_build_fallback_multimodal_models` 先例);两套存储的求和收敛进
`cost.merge`(`accumulate` 已经收敛了累加,求和没享受同一条原则);
`delegate_llm_client` 改必填(缺省回落会让「生产忘了传」静默记错池,与 `pool`/
`injections` 必填同一取向);补 `complete()` 参数标注;模块文档不再声称「只放无 IO
的纯逻辑」;`misconfigured_pool_names` 不再声称与 `/settool` 同等强度地「当场拒绝」。

drive_tick 那条断言从 `<=` 收紧成精确等于 `{"proactive"}` —— 那条路本就不做
embedding(issue #38 零 LLM 摘要),所以连 embedding 池都该是空的。

8 处变异全部重跑并杀掉对应测试。1297 全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Yushu merged commit 396fa8085d into main 2026-07-28 05:42:36 +00:00
Yushu deleted branch feature/81-cost-accounting 2026-07-28 05:42:37 +00:00
Sign in to join this conversation.
No description provided.