est_tokens 应由库按实测自估,而不是让调用方填一个没有正确取值的常量 #2
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
问题
SourceConfig.est_tokens是按 token 限流时的入场预扣量,由配置的人填。这个值没有正确取值,理由有三条,一条比一条硬。一、正确值属于请求,不属于源。 同一个模型账号,一次短文本分类几百 token,一次带图的表格提取上万。下游项目 CHSAnalyzer 的上一版给三个 VLM 源一律填了 4000,因为它没有别的办法。
二、它没有安全方向。 填大了压吞吐,而且从外面看不出来(表现为「怎么这么慢」);填小了撞 429,进而触发熔断。一个参数如果连「往哪个方向填错更安全」都答不上来,配置的人就没有任何决策依据。
三、它还有第二个身份。
_settle在模型没返回 usage 帧时,会把这个预估值当成实际用量写进遥测。也就是说一个拍脑袋的常量会污染计费数据,而这条路径是静默的——没有人会发现遥测里那一行的 token 数是编的。为什么库比调用方更适合解决它
TelemetryRecorder每次调用都落了实测 token 数,permit 事后本来就按实测 settle。也就是说库手里已经有算这个值需要的全部数据,而调用方手里没有。按 Ousterhout 在《A Philosophy of Software Design》8.2 节的说法,这正是配置参数不该存在的典型形态:
这里的答案明确是「不能」。他给的替代方案也正好对应——那节举的例子是传输协议的重传间隔,与其让人填,不如让协议自己测量成功请求的响应时间再推算,这样还能随运行条件自动调整,而配置参数 "can easily become out of date"。
一个可能的方向(仅供参考,具体怎么做由你们定)
按 scope + model 维度,从近期遥测记录里取一个分位数(比如 p90)当预扣量,冷启动没有历史时退到一个保守常量。这样它随负载自动调整,也不会再有「用途内部差异很大」的问题。
需要考虑的几点:冷启动窗口多长、要不要按 model 而不只按 scope 分桶、以及遥测后端是
none时怎么退化。对下游的影响
一旦库能自估,
SourceConfig.est_tokens就可以从必填降为可选或直接移除,_validate_gates里tpm > 0强制est_tokens > 0那条约束也能跟着去掉。现在这条约束正在下游造成一个陷阱:CHSAnalyzer 新版已经把
est_tokens从配置面移除(判定它没有正确取值),结果账号级的tpm就再也不能填非 0——运维照着供应商配额页填一个真实数字,配置层通过、装配期才炸。下游只能在自己的配置模型里加一条校验把tpm限死为 0,并解释为什么。这是个绕路,不是解法。参考:下游的裁决记录在 CHSAnalyzer 的
research-wiki/design/0006-config-key-audit.md决策三。不提 PR 的原因
这件事的正确解法在库内部(遥测数据、限流器、settle 路径三者的交界),由熟悉这几处的人做更合适。下游先在自己那边绕过,等库这边有结论再跟进。
感谢这份 issue,论证比一般的功能请求扎实得多,尤其是把「下游正在被卡住」的具体形态写清楚了。我们核对了库内代码,核心诉求接受,但有两条理由我们认为归因偏了,建议的解法也不打算照做。逐条回应。
一、我们认同的
1. 那条硬约束确实是库的边界缺陷,这是最有说服力的一条。
types.py:125的tpm > 0 ⇒ est_tokens > 0把两个本不该耦合的概念绑死了:tpm是供应商的真实配额(运维照配额页抄一个数就行),est_tokens是库的实现细节(入场预扣多少)。库要求用户为了填前者必须先猜出后者,这是把内部实现泄漏进了配置面。你们删掉猜测项后tpm只能填 0、配置层过而装配期炸 —— 这个陷阱由库负责,不该由下游加校验绕开。2.
est_tokens兼任两个职责,违反单一职责,而且第二个职责实现得比 issue 说的还糙。transports/openai_compat.py:146,usage 帧缺失时的兜底:它把
prompt_tokens记成 0,把整个限流预扣量塞进completion_tokens。prompt token 绝不可能是 0,这个分配本身就是错的,cost会照着这个错数算出来。一个限流押金常量根本不该出现在计费路径上 —— 这一点与限流完全无关,是独立的 bug。二、我们认为归因偏了的
理由二「它没有安全方向」不成立。 预扣量填大就是安全方向:多押金 → 提前触顶 → 压吞吐,但不会撞 429、不会连带熔断;填小才危险。这是限流预扣的常识。issue 真正想说的是「填大的代价不易观测(只表现为变慢)」—— 这是可观测性问题,不是「无从决策」。区分这两者会影响解法选择:一个没有安全方向的参数必须消灭,一个有安全方向但默认值难定的参数,给个保守默认值就够了。
「静默污染计费数据」说过头了。
usage_source是 18 个冻结遥测字段之一,telemetry/postgres.py的表里有这一列,估算行明确标着estimated。下游做成本汇总时WHERE usage_source = 'measured'即可过滤。准确的说法是「数字是编的,但带着标记、可被发现」,而不是「没人会发现」。当然这不改变第一部分第 2 条的结论——编出来的数字本身就不该存在。理由一的幅度有限。
settle是多退少补的(backends/memory/limiter.py:52,delta = actual - est),所以est填不准只在请求 in-flight 期间失真,结算后自动找平。长流式请求会明显一些,但不是「填错就一直错」。三、为什么不采用「遥测 p90 自估」
方向上讲得通(数据确实在库手里),但落到本库的架构上有三处硬障碍:
TelemetryRecorder(ports.py:247)是纯只写端口,只有record_llm_call。要取分位数就得给它加查询接口,并强制所有后端实现。把一个观测端口改造成读写存储端口,公共 API 的扩张远大于它想省掉的那一个 int 字段telemetry与backends同层且互不依赖,limiter 读不到遥测。预估器只能落在 middleware 层,是一个新子系统none时没有历史,冷启动同理,两者都必须退到一个保守常量 —— 那个常量的取值问题跟今天完全一样为消除一个可选 int 字段而引入带分桶、分位数、冷启动窗口和降级路径的自适应子系统,在本库的 YAGNI 纪律下过不了关。
四、我们准备怎么做
拆成两步,收益的绝大部分在第一步,且第一步不需要任何新子系统:
第一步 · 拆掉两个身份 + 解绑约束
其一,usage 缺失时不再拿限流押金去编计费数字:记 0 并由
usage_source表明用量不可得,宁可算不出成本,也不算错成本。这直接消灭第一部分第 2 条。其二,去掉
tpm > 0 ⇒ est_tokens > 0,由库内提供文档化的保守默认预扣量,est_tokens从必填降为「想调优才填」的可选项。这直接解掉你们遇到的陷阱:运维可以正常填真实 TPM,不必再在下游配置模型里把tpm限死为 0。第二步 · 暂不启动
遥测驱动的自适应预估留到出现实测证据(默认预扣量在真实负载下明显不合适)之后再评估。
具体方案会先走本库的 brainstorming 流程出 2-3 个备选并经人类确认,定稿后在本 issue 更新。届时
est_tokens的公共语义变化会按迁移兼容约束处理(字段保留、降为可选,不删不改名)。五、给下游的临时建议
你们目前在自己配置模型里把
tpm限死为 0 的绕法,在第一步发版前请保留。发版后可以直接删掉那条校验并填真实 TPM,est_tokens不填即可 —— 我们会在 CHANGELOG 和 wiki 里标明这个行为变更。再次感谢,
prompt_tokens记 0 那个 bug 是顺着这份 issue 才翻出来的。已实现,方案与前一条回复一致
实现完成在分支
feature/est-tokens-decoupling,版本号定为 1.0.3(patch:字段与环境键都没删没改名、端口签名未动、API 严格向后兼容,变的是遥测的数据口径)。尚未合并与发包,下面写明你们可以动手的时点。设计与计划都过了独立审查与人类批准门,存档在主仓库
research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md与research-wiki/plans/2026-07-30-est-tokens-decoupling-plan.md。最终落地的两条
一、
est_tokens不再兼任遥测用量兜底。 usage 帧缺失时库如实记0/0,usage_source取新值unavailable,cost落 NULL。不再拿"最坏情形上界"冒充实测值——你们指出的双重高估(上界当实测 + 整块塞进单价更贵的 completion)确实存在,实测算例是 26 倍。值域现在三态,每个都有真实生产者:
measuredestimatedunavailable二、
est_tokens由必填降为可选调优覆盖,tpm > 0 ⇒ est_tokens > 0的装配约束已删除。 不填时库按max(1, tpm // 60)派生——语义是"一次调用约占一秒钟的配额份额",尺度无关:tpm=6000与tpm=600000都收敛到约 60 个在途,而固定常量会让在途上限随配额规模乱飘。显式填值仍然优先,字段与{SCOPE}__{PROVIDER}__{N}__EST_TOKENS环境键保留不删不改名。你们可以动手的时点
等 1.0.3 发到私有源之后,CHSAnalyzer 那边可以:
tpm限死为 0 的绕行校验,以及为它写的解释注释;TPM,EST_TOKENS留空即可。升级时请复核一处:成本汇总若依赖"cost 非空"这个隐含假设,现在用量不可得的行 cost 是 NULL 了。
SUM(cost)会天然跳过它们(这正是选 NULL 而非0.0的原因——0.0会让"免费"与"未知"在数据上不可区分)。账目缺口这样量化:AND cache_hit = false不可省:缓存命中行没产生新调用,cost 是事实上的0.0,本无缺口,不加限定会让度量偏高。按
usage_source分支的代码需要认识新值;历史库里既有的estimated行语义不变、照常可读。两处与前一条回复不同的地方
OCR 那条我们撤回了。 前一条回复里我们说
ocr.py把无 usage 的 OCR 调用标measured是"假陈述",准备一并改掉。独立审查推翻了这个判断:库既有立场是 OCR 的 0 token 属事实而非未知(types.py的Usagedocstring 与ocr.py模块 docstring 都这么写),所以measured本就准确。而且改成unavailable会把本无缺口的 OCR 行灌进上面那个缺口度量,反而破坏了我们用来否决"记 0 但沿用 estimated"方案的核心理由。这条已从范围里剔除,OCR 一字未动。实现期还抓出一个我们自己差点埋下的 bug。 把兜底改成
(0, 0)之后,retry.py与embedding.py的成功侧结算(actual = prompt + completion)会变成 0,而入场押的是预扣量,差额为负会把押金整笔退回——对"从不返回 usage 帧"的网关源等于 TPM 闸完全失效。设计初稿只处理了失败侧,是独立审查发现的。现在预扣与结算恒取同一个effective_est_tokens(),成功侧与非 dead 瞬时失败侧差额都是 0。未采纳遥测 p90 自估
理由收窄为一条:
TelemetryRecorder目前是纯只写端口,自估需要给它加查询接口并强制所有后端(含none)实现,公共 API 的扩张远大于它要省掉的一个可选字段;而且尚无实测证据表明派生默认值不够用。等tpm // 60在真实负载下暴露问题再说。前一条回复里我们还论证过"那会把两条方向相反的降级铁律焊在一起",这条撤回——审查指出 p90 完全可以在遥测读失败时回退到纯派生值,限流侧仍能 fail-closed,并非必然冲突。结论不变,理由改了。
一个已知限制,另开 issue 跟
单源
tpm == 0而{SCOPE}__GLOBAL__TPM > 0时,派生值为 0,全局 TPM 闸拿 0 预扣、入场保护形同虚设。这是既有行为(旧约束也只管单源tpm > 0),本次没修——修它要把GlobalLimits注入QuotaGate、改三处装配,属独立议题。如果你们的部署形态会踩到(只配全局 TPM 而不配单源 TPM),说一声,我们优先排。合并发包后会在本 issue 补一条,届时再关闭。
v1.0.3 已发布
已合并到
main(tagv1.0.3)并发到实验室私有源。合并结果上复验make ci:605 passed,覆盖率 92%。你们那边现在可以动手了:删掉配置模型里把
tpm限死为 0 的绕行校验与相应注释,按供应商配额页填真实TPM,EST_TOKENS留空。升级时记得复核成本汇总对"cost 非空"的隐含假设,缺口查询用
WHERE usage_source = 'unavailable' AND cache_hit = false(cache_hit限定不可省,理由见上一条)。wiki 的
指南-遥测与成本、解释-治理行为、参考-配置键、参考-公共API四页已同步新口径。绕行校验删干净、跑通之后由你们关闭本 issue;若升级中撞到问题,直接在这里回。