Files
PolyGateway/research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md
T
iomgaa fd7d9d330b docs: design est_tokens decoupling from usage fallback
Split the two jobs SourceConfig.est_tokens has been doing: TPM entry
pre-deduction, where conservative means safe, and the telemetry usage
fallback, where feeding a worst-case upper bound through the output
price inflates cost by ~26x.

Records the approved decisions: usage_source gains an "unavailable"
state whose cost is NULL, and an unset est_tokens derives from
tpm//60 so the in-flight ceiling stays scale-invariant. Also declares
the CHS "conservative accounting" migration item as intentionally
dropped, and the pre-existing global-TPM gap as knowingly unfixed.

Refs: gitea issue #2
2026-07-30 04:10:52 -04:00

12 KiB
Raw Blame History

est_tokens 解耦设计(issue #2)

  • 日期: 2026-07-30
  • 触发: Gitea issue #2《est_tokens 应由库按实测自估,而不是让调用方填一个没有正确取值的常量》
  • 档位: 强制档(改公共 API 语义 + usage_source 公共值域 + 推翻一条已声明保留的迁移行为)→ 需人类审批门
  • 修订的权威文档: ARCHITECTURE.md §7.7(SourceConfig.est_tokens 描述)、§5.1(usage_source 值域)、migrations/chsanalyzer.md 行 151 与 G2

1. 问题:一个常量被派了两份互相矛盾的差事

SourceConfig.est_tokens 同时承担两个职责,而两者对"保守"的定义方向相反:

职责 语境 "保守"意味着 填大的后果
TPM 入场预扣 限流 多押金,宁可压吞吐也不击穿网关 安全(只是慢)
usage 缺失时的用量兜底 计费 不存在保守方向 账单虚高

CHS 原版 config.py:55 把它定义为"须 ≥ 最坏情形 token"——按定义是上界。拿上界当实测值记账,必然系统性高估。库把遥测拆成 prompt_tokens/completion_tokens 两列后又把整个估值塞进 completion(openai_compat.py:146),而 pricing.py:70-72prompt×input价 + completion×output价 换算,输出单价通常是输入的数倍——双重高估

实测算例:est_tokens=4000,单价输入 1 元/百万、输出 8 元/百万,真实消耗 400+100:

记账 token cost
真实 400 / 100 0.0012 元
现状 0 / 4000 0.032 元(26 倍)

第二个症状是装配约束:types.py:125tpm > 0 ⇒ est_tokens > 0 把供应商配额(运维可从配额页抄到)与库的实现细节(预扣量,无人能正确取值)绑死。下游 CHSAnalyzer 删掉 est_tokens 配置项后,tpm 就再也不能填非 0,只能在自己的配置模型里把 tpm 限死为 0 绕开——库把内部细节泄漏进了配置面。

2. 备选方案对比

2.1 决策点一:usage 不可得时遥测记什么

方案 做法 权衡
A(选定) 0/0,usage_source 扩一个 unavailable,cost 记 NULL 缺数据可被统计:SUM(cost) 跳过 NULL,COUNT(*) WHERE usage_source='unavailable' 能量化账的缺口。代价:公共值域变更,需进 CHANGELOG
B 0/0,沿用 estimated 改动最小(等于把 est_tokens>0 路径统一到 est_tokens=0 的现状行为)。否决:cost 算出 0.0,"免费"与"未知"在数据上不可区分,缺口不可量化
C 保留 est 兜底,只修 prompt/completion 分配比例 保住 CHS"保守计量"意图。否决:比例是又一个没有正确取值的魔数,且未触及"拿上界当实测"这个根因,仍高估约 9 倍

2.2 决策点二:est_tokens 未填时的默认预扣量

先排除"不预扣":try_acquire 传 0 会让 TPM 窗口在请求飞出到 settle 回来的整段时间形同虚设,大批请求可同时入场,正是"防击穿网关"要防的场景,与 CLAUDE.md 降级方向铁律相悖。

方案 源甲 tpm=6000 源乙 tpm=600000 权衡
派生 tpm//60(选定) 押 100 → 60 个在途 押 10000 → 60 个在途 尺度无关:任何配额规模都给出同一行为上限,语义可写进 docstring("一次调用约占一秒钟的配额份额")
固定常量 1000 押金占配额 1/6 → 仅 6 个在途,小请求场景白慢数倍 押金占 1/600 → 600 个在途,大请求场景照样撞 429 否决:常量与配额规模无关,在途上限随配额乱飘,无法解释取值

2.3 派生逻辑的落点

方案 权衡
SourceConfig.effective_est_tokens()(选定) 纯方法只读自身字段,落 types.py 内核不违反依赖铁律;零装配变更、零端口变更;三个调用点(QuotaGate、retry/embedding 失败结算)共用一份
注入 GlobalLimitsQuotaGate,派生取全局与单源 tpm 的较紧者 能覆盖"单源 tpm=0 而全局 tpm>0"的场景。否决:需改三处装配(retry.py:186/embedding.py:116/ocr.py:119),且它修的是一个既有缺口(见 §7),超出本任务范围
派生下沉到两个 limiter 后端 否决:try_acquire(source_key, est_tokens) 的入参会变成谎言(后端忽略它),且逻辑要写两遍,违反 D3"语义契约只有一份"与 P7"决策与存储分离"

3. 选定方案

3.1 usage_source 三态值域

含义 生产者 cost
measured usage 帧完整可信 正常路径 按 token 换算
estimated 有实测数字但可信度降级 打捞路径(收到 usage 帧但流被截断) 按 token 换算
unavailable 用量信息不可得 usage 帧缺失、失败尝试、终态失败、OCR 端点 NULL

estimated 保留且有真实生产者(打捞),同时保证历史库里既有的 estimated 行读兼容。值域定为 types.py 模块级 frozenset 常量,替代目前"注释里写 measured | estimated、代码不校验"的状态(P5)。

3.2 逐处改动

# 位置 改动
1 types.py:125 删除 tpm > 0 ⇒ est_tokens > 0;est_tokens 保留字段、语义降为"可选调优覆盖"
2 types.py SourceConfig 新增 effective_est_tokens():显式值 > 0 则原样返回;否则 tpm > 0 时返回 max(1, tpm // 60),tpm == 0 时返回 0
3 openai_compat.py:146,176 两处兜底改为 (0, 0, "unavailable") / (0, "unavailable"),不再读 source.est_tokens
4 openai_compat.py:336 打捞覆盖加条件:仅当 usage_source == "measured" 时降级为 estimated,否则保持 unavailable(否则 0/0 会被标 estimated 而算出假的 0.0)
5 middleware/telemetry.py:130-135 cost 分支增加短路:usage_source == "unavailable"None(唯一换算点,改一处即全库生效)
6 middleware/telemetry.py:58,100 失败尝试与终态失败的 usage_sourceestimatedunavailable(用量确实不可得;这两行 cost 本已是 None,语义对齐不改金额)
7 middleware/ratelimit.py:26 source.est_tokenssource.effective_est_tokens()
8 retry.py:370embedding.py:294 失败保守结算同步改用 effective_est_tokens()必须同改:预扣用派生值而结算退 est_tokens=0 会让 delta 为负、退掉全部押金,反而丢掉"失败可能已被计费"的保守意图
9 ocr.py:411 硬编码的 measuredunavailable:OCR 端点不返回 usage,声称"实测 0 token"是假陈述
10 embedding.py:383,390 二值合并扩为三态:任一批 unavailable → 整体 unavailable;否则任一 estimatedestimated;否则 measured
11 embedding.py:397 _total_cost 跳过 unavailable 批;若存在 unavailable 批则整体 cost 记 NULL(部分求和会给出一个偏低却看似有效的金额)

3.3 明确不改的

retry.py/embedding.py 失败路径按预扣量做限流结算的行为保留——那是限流语境,保守方向正确(失败请求可能已被网关计费),且该值只流向 _settle_and_release,不进遥测。SourceConfig.est_tokens 字段与 {SCOPE}__{PROVIDER}__{N}__EST_TOKENS 环境键保留不删不改名(迁移兼容硬约束,ARCHITECTURE §5.1)。RateLimiter 端口签名不变。

4. 旧版行为审计(迁移保留项的推翻声明)

旧版行为 出处 本次处置
usage 缺失按 est_tokens 估算并标 estimated,不静默用 0 CHS invokers.py:241-254;migrations/chsanalyzer.md:151 标记为保留 有意放弃。理由:CHS 只记单个 total_tokens,不存在 prompt/completion 分配问题;库拆两列后无法忠实分配,且 est_tokens 按 CHS 自身定义是最坏情形上界。"保守"在限流语境安全、在计费语境只有错误一个方向
缺失时不静默用 0(拒绝 VT 的"填 0 且不标注") 同上;m1-core-design.md:222 行 10 保留。本方案记 0 但带 unavailable 显式标记且 cost 为 NULL,反静默的原始意图完整保留——被放弃的只是"编一个数字"这个手段
est_tokens 作 TPM 入场预扣常量 CHS config.py:55 保留,仅由必填降为可选覆盖
tpm > 0 ⇒ est_tokens > 0 装配校验 m1-core-plan.md:93 替换为库内派生,校验删除
打捞路径强制 estimated m1-core-design.md §6 保留,补一个前置条件(§3.2 #4)
遥测 INSERT OR IGNORE 幂等、写失败降级不冒泡、列只增 m1-core-design.md:218 保留,本次无 DDL 变更

5. 非功能维度

并发与取消: effective_est_tokens() 是无状态纯方法(只读 frozen dataclass 字段),并发安全、无锁、可重复调用。本次改动不新增 await 点、不改变任何 try/finally 结构,取消穿透路径与 in-flight 释放语义原样不动。#8 的同步修改反而消除了一处"取消/失败后押金被过度退还"的结算偏差。

降级方向: 不改变任何后端的降级方向。遥测侧仍是静默降级(telemetry.py:160 的 warning 不冒泡);限流侧仍是 GovernanceBackendError 上抛而非放行。需要强调的是本方案不引入对遥测后端的读依赖——这正是否决 issue 建议的 p90 自估的核心理由:那会让限流预扣依赖一个允许静默降级的后端,把两条方向相反的降级铁律焊在一起。

幂等与重复: Permit.settle()/release() 的幂等 flag 语义不变。#8 使预扣与结算取自同一派生函数,同一请求重复结算仍是 no-op。

持久化与原子性: 无 DDL 变更(两 schema 的 cost 列已可空);无新增落盘点;Redis Lua 脚本不改(仍接收调用方算好的 est)。历史数据不迁移:旧行的 estimated 语义在新值域中依然合法可读。

6. 错误处理与测试策略

值域校验失败属配置/内部不变量违反 → ValueError(装配期 fail-loud),不进四分类运行时错误。本次不改变任何调用失败的分类归属。

测试 断言要点 文件
约束解绑 tpm=6000, est_tokens=0 构造成功(改前抛 ValueError) tests/unit/test_types.py
派生尺度无关 tpm=6000→100tpm=600000→10000tpm=0→0、显式值优先、tpm=30→max(1,·) 不为 0 同上
cost 不再造假 est_tokens=4000 + usage 缺失 → 0/0/unavailablerecord_llm_call 收到 cost=None(改前 0.032) tests/unit/test_openai_compat.pytest_telemetry.py
打捞前置条件 打捞 + usage 帧存在 → estimated 且 cost 非 None;打捞 + usage 缺失 → unavailable 且 cost 为 None(回归 §3.2 #4) test_openai_compat.py
结算不退多 未填 est_tokenstpm>0 时失败请求,TPM 窗口残留量等于派生预扣量而非 0(回归 §3.2 #8) tests/contracts/test_limiter_contract.py
三态合并 混合批 measured+unavailable → 整体 unavailable 且 cost 为 NULL tests/unit/test_embedding.py
OCR OCR 成功行 usage_source == "unavailable" tests/unit/test_ocr_client.py
值域封闭 越界字符串被拒;三态皆被接受 test_types.py

限流侧断言随 tests/contracts/test_limiter_contract.py 同时覆盖内存与 Redis 两后端(Redis 走真实实例,遵守共享后端不并跑纪律)。

7. 已知限制(本次不修,显式声明)

单源 tpm == 0 而全局 tpm > 0 时,effective_est_tokens() 返回 0,全局 TPM 闸拿 0 预扣、入场保护形同虚设。这是既有行为(现状约束只管 cfg.tpm > 0,该场景下 est_tokens=0 本就合法),本方案不引入也不修复它。修它需要把 GlobalLimits 注入 QuotaGate(§2.3 备选二),属独立议题,建议另开 issue。

8. 下游影响与发布

est_tokens 从必填降为可选后,CHSAnalyzer 可删掉"tpm 必须为 0"的绕行校验并填真实 TPM。usage_source 出现第三个值、且不可得行的 cost 由数值变 NULL,是下游可见的行为变更:成本汇总若此前依赖"cost 非空"隐含假设需复核。按 docs-convention.md §2,发版须同步 CHANGELOG 与 wiki 的 usage/成本口径说明,并在 issue #2 回帖结论。

遥测驱动的自适应预估(issue 原建议)不在本次范围,待默认派生值在真实负载下出现实测问题后再评估。

9. 规模判定

改动跨 8 个源文件 + 2 份权威文档 + 6 个测试文件,属跨多文件功能 → 本设计经人类审批后须走 writing-plans 出实施计划,不得直接进实现。