[prefactor] LLMClient/EmbeddingClient 契约携带 token 用量 #84
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/78-usage-in-client-contracts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #78
What
纯 prefactor,不交付任何用户可见行为 —— 让
LLMClient/EmbeddingClient契约携带每次调用的 token 用量,为 ADR-0010 六池成本治理铺地基。此前AnyLLMClient.complete()拿到 any-llm 响应后直接把 usage 丢弃了。PRD #77 第一片。实现要点
usage.TokenUsage(单独成模块:两条调用边界都要用,后续记账层也要用;放任一侧都会让另一侧反向 import)。None表示「provider 没回传」,不等于TokenUsage(0, 0)。这条区分是后续记账的地基:把「不知道」当 0 累加,「今天花了多少钱」就会悄悄失真。提取器任一层读不到即整体None,不半补;也绝不抛错 —— 拿不到用量是记账精度问题,不该让一次正常的对话请求失败。EmbeddingClient.embed()返回类型改为EmbeddingResult(vector, usage),而不是新开embed_with_usage():留着旧方法就等于留下一条绕过记账的路,六池治理会出现「这个数为什么对不上」的黑洞。破坏面只有 5 个 src 调用点 + 2 个测试构造点。Review
两轴都抓到实质问题:
tests/test_presence_events_e2e.py里手写了一个与DummyEmbeddingClient一模一样的内联替身,我漏了 —— 而且那条路径压根不 embed,测试照样全绿。讽刺的是这正是本 ticket 单独拆出来要防的东西。根因是我 grep 时只搜了.embed(调用点、没搜async def embed实现点。已改为直接用现成替身。Spec 轴另穷举确认除此之外无遗漏(6 处 embed 实现、11 处 complete 实现)。_usage_of里那段判定是同一条策略写了两遍,改规则容易只改一边。抽出共用的read_token_count,各自保留字段/语义差异。TokenUsage.__add__与total_tokens:累加就是记账,本片明确不含(AC6)。__add__更糟 —— 它顺手把「累加时怎么对待None」这条真正的记账策略提前拍死了。EmbeddingResult.vector改必填:空向量永远不是合法 embedding。它原是照抄AssistantTurn的默认值,但那边有明确理由(几十个既有构造点),这边只有 3 个。Spec 轴 introspect 了实装的
any_llm_sdk 1.19.0,确认prompt_tokens/completion_tokens字段名正确 —— 排除了「字段名写错导致永远静默返回 None」这个对 prefactor 而言最坏的结果。自查时先发现过一个测试缺口:最初只单测了
_usage_of提取器,变异验证时把usage=_usage_of(response)改回usage=None,19 条测试全绿 —— 提取器写对了但适配器忘了接上,照样一个 token 都记不到。已补四条真的走AnyLLMClient.complete()/AnyLLMEmbeddingClient.embed()的测试。已知局限(如实记录,未修)
gemini/lmstudio的 any-llm embedding 转换器会伪造零用量(硬写Usage(prompt_tokens=0)),那时这里会返回TokenUsage(0, 0)—— 按本仓定义那是「确实没花」,与「不知道」混为一谈。core 无从分辨「真算出 0」和「懒得填」,已写进_usage_ofdocstring 而不假装它不存在。默认配置走 openai,真回传用量,不受影响。Tests
全仓 1097 个测试通过(新增 26)。变异复核三处:两侧适配器(各挂 1 条)、共用类型判定(两侧测试同时挂)。