docs: fold independent review into est_tokens design

The reviewer found two real defects. First, changing the usage fallback
to (0, 0) breaks the success-side settlement too, not just the failure
side: retry.py:338 and embedding.py:271 take actual from the same
return value, so a call whose gateway never sends a usage frame would
have its whole pre-deduction refunded -- systematic TPM undercounting.
Added as change item 9. Second, dropping the OCR item: types.py:51 and
ocr.py:9 both state OCR's zero token count is a fact, not an unknown,
so "measured" was already accurate, and relabelling it would pollute
the very metric used to justify the chosen option.

Also pins the cost short-circuit after the cache_hit branch, confines
value-domain enforcement to producers so no bare ValueError escapes
chat(), completes the authoritative-document list, and narrows the
p90 rejection to the read-port argument.
This commit is contained in:
2026-07-30 05:06:00 -04:00
parent fd7d9d330b
commit ac7c86fdee
@@ -3,7 +3,10 @@
- **日期**: 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
- **修订的权威文档**(经独立审查补全):
- `ARCHITECTURE.md` §7.7 行 428(`SourceConfig.est_tokens` 描述)、§5.1 行 331(`usage_source` 值域)、**§4.4 行 305**("token 按 `est_tokens` 预扣")、**§7.1 行 384**("打捞路径强制 `usage_source="estimated"`",因 §3.2 #4 变为有条件)
- `migrations/chsanalyzer.md` 行 151 与 G2(行 185)
- **`.env.example` 行 11**("TPM > 0 时 EST_TOKENS 必填 > 0",约束已废除)
## 1. 问题:一个常量被派了两份互相矛盾的差事
@@ -62,7 +65,11 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按
| `estimated` | 有实测数字但可信度降级 | 打捞路径(收到 usage 帧但流被截断) | 按 token 换算 |
| `unavailable` | 用量信息不可得 | usage 帧缺失、失败尝试、终态失败、OCR 端点 | **NULL** |
`estimated` 保留且有真实生产者(打捞),同时保证历史库里既有的 `estimated` 行读兼容。值域定为 `types.py` 模块级 frozenset 常量,替代目前"注释里写 `measured | estimated`、代码不校验"的状态(P5)。
`estimated` 保留且有真实生产者(打捞),同时保证历史库里既有的 `estimated` 行读兼容。
**不变式的准确表述**: 产生了真实网关调用、但用量不可得的行 → cost 为 NULL。缓存命中行不在此列(见 §3.2 #5)。
**值域的强制落点**: `types.py` 模块级 frozenset 常量,仅约束**库内生产侧**——所有写入 `usage_source` 的位置从该常量取值,测试断言库内产出恒在三态内。**不在 `LLMResponse`/`Usage`/`TransportResult` 等 frozen dataclass 上加 `__post_init__` 值域校验**,两条理由:① 它们是运行时构造点(如 `retry.py:418`),裸 `ValueError` 不属 `errors.py` 四分类,`RetryMW` 不捕它,会直接逃出 `chat()`,违反错误分类驱动铁律;② `LLMResponse` 是三项目已消费的公共类型,新增运行时校验是下游可见行为变更,超出本任务。故 §6 的值域测试断言"库内所有生产点的产出值落在三态内",而非"越界字符串被拒"。
### 3.2 逐处改动
@@ -72,18 +79,20 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按
| 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`(唯一换算点,改一处即全库生效) |
| 5 | `middleware/telemetry.py:130-135` | cost 分支增加短路:`usage_source == "unavailable"``None`。**插在 `cache_hit` 分支之后**:缓存命中未产生新调用,`0.0` 是事实而非未知,既有"缓存命中 0.0"语义保持不动。故 `cache_hit=True``usage_source="unavailable"` 的行 cost 仍是 `0.0`,与 §3.1 不变式不冲突(那条只管产生了真实调用的行) |
| 6 | `middleware/telemetry.py:58,100` | 失败尝试与终态失败的 `usage_source``estimated``unavailable`(用量确实不可得;这两行 cost 本已是 None,语义对齐不改金额) |
| 7 | `middleware/ratelimit.py:26` | `source.est_tokens``source.effective_est_tokens()` |
| 8 | `retry.py:370``embedding.py:294` | 失败保守结算同步改用 `effective_est_tokens()`**必须同改**:预扣派生值而结算退 `est_tokens=0` 会让 `delta` 为负、退掉全部押金,反而丢掉"失败可能已被计费"的保守意图 |
| 9 | `ocr.py:411` | 硬编码的 `measured``unavailable`:OCR 端点不返回 usage,声称"实测 0 token"是假陈述 |
| 8 | `retry.py:370``embedding.py:294` | **失败侧**保守结算改用 `effective_est_tokens()`。必须同改:预扣派生值而结算退 `est_tokens=0` 会让 `delta` 为负、退掉全部押金,丢掉"失败可能已被计费"的保守意图 |
| 9 | `retry.py:338``embedding.py:271` | **成功侧**结算:`usage_source == "unavailable"` 时按 `effective_est_tokens()` 结算,而非 `prompt+completion`(此时恒为 0)。**这条是保持既有行为、不是新增保守**:改前 `_resolve_usage` 恰好返回 `est_tokens`,使 `actual == 预扣量``delta == 0`、押金留存;#3 把它改成 `(0, 0)` 后若不同改,成功调用的押金会被整笔退回,对"从不返回 usage 帧的网关源"构成系统性 TPM 计量失效——闸门退化成进门即放行、出门即清账,正是降级方向铁律要防的击穿 |
| 10 | `embedding.py:383,390` | 二值合并扩为三态:任一批 `unavailable` → 整体 `unavailable`;否则任一 `estimated``estimated`;否则 `measured` |
| 11 | `embedding.py:397` `_total_cost` | 跳过 `unavailable` 批;若存在 `unavailable`整体 cost 记 NULL(部分求和会给出一个偏低却看似有效的金额) |
| 11 | `embedding.py:397` `_total_cost` | 存在 `unavailable`整体 cost 记 NULL(逐批求和会给出一个偏低却看似有效的金额) |
### 3.3 明确不改的
`retry.py`/`embedding.py` 失败路径按预扣量做**限流**结算的行为保留——那是限流语境,保守方向正确(失败请求可能已被网关计费),且该值只流向 `_settle_and_release`,不进遥测。`SourceConfig.est_tokens` 字段与 `{SCOPE}__{PROVIDER}__{N}__EST_TOKENS` 环境键**保留不删不改名**(迁移兼容硬约束,ARCHITECTURE §5.1)。`RateLimiter` 端口签名不变。
**`ocr.py:411``usage_source="measured"` 保留不改**(初稿曾列为改动项,独立审查后剔出)。库既有立场是 OCR 的 0 token 属**事实**而非未知——`types.py:51` "token 用量;OCR 等无计费调用填 0"、`ocr.py:9` "settle 恒为 0(OCR 无 token 计费)"——故 `measured` 是准确陈述。改成 `unavailable` 还会反噬 §2.1 的核心度量:`COUNT(*) WHERE usage_source='unavailable'` 本用于量化账目缺口,灌进本无缺口的 OCR 行就失去意义。
## 4. 旧版行为审计(迁移保留项的推翻声明)
| 旧版行为 | 出处 | 本次处置 |
@@ -97,9 +106,11 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按
## 5. 非功能维度
**并发与取消**: `effective_est_tokens()` 是无状态纯方法(只读 frozen dataclass 字段),并发安全、无锁、可重复调用。本次改动不新增 `await` 点、不改变任何 `try/finally` 结构,取消穿透路径与 in-flight 释放语义原样不动。#8 的同步修改反而消除了一处"取消/失败后押金被过度退还"的结算偏差
**并发与取消**: `effective_est_tokens()` 是无状态纯方法(只读 frozen dataclass 字段),并发安全、无锁、可重复调用。本次改动不新增 `await` 点、不改变任何 `try/finally` 结构,取消穿透路径与 in-flight 释放语义原样不动。#8 #9 合起来保证预扣与结算恒取同一派生值,成功/失败/取消三侧均不产生押金偏差——这是本设计里最容易漏的一致性约束(初稿只写了失败侧,独立审查发现成功侧缺口)
**降级方向**: 不改变任何后端的降级方向。遥测侧仍是静默降级(`telemetry.py:160` 的 warning 不冒泡);限流侧仍是 `GovernanceBackendError` 上抛而非放行。需要强调的是本方案**不引入对遥测后端的读依赖**——这正是否决 issue 建议的 p90 自估的核心理由:那会让限流预扣依赖一个允许静默降级的后端,把两条方向相反的降级铁律焊在一起
**降级方向**: 不改变任何后端的降级方向。遥测侧仍是静默降级(`telemetry.py:161` 的 warning 不冒泡);限流侧仍是 `GovernanceBackendError` 上抛而非放行;TPM 计量不因 usage 帧缺失而静默失效(#9)
否决 issue 建议的 p90 自估,主论据是 **`TelemetryRecorder` 目前是纯只写端口,自估需要新增读接口并强制所有后端(含 `none`)实现**,公共 API 扩张远大于它要省掉的一个可选字段,且尚无实测证据表明派生默认值不够用(§8)。初稿曾论证"那会把两条方向相反的降级铁律焊在一起",此论据经审查后**撤回**:p90 方案完全可以在遥测读失败时回退到纯派生值,限流侧仍能保持 fail-closed,故并非必然冲突。结论不变,理由收窄。
**幂等与重复**: `Permit.settle()`/`release()` 的幂等 flag 语义不变。#8 使预扣与结算取自同一派生函数,同一请求重复结算仍是 no-op。
@@ -114,11 +125,13 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按
| 约束解绑 | `tpm=6000, est_tokens=0` 构造成功(改前抛 ValueError) | `tests/unit/test_types.py` |
| 派生尺度无关 | `tpm=6000→100``tpm=600000→10000``tpm=0→0`、显式值优先、`tpm=30→max(1,·)` 不为 0 | 同上 |
| cost 不再造假 | `est_tokens=4000` + usage 缺失 → `0/0/unavailable``record_llm_call` 收到 `cost=None`(改前 `0.032`) | `tests/unit/test_openai_compat.py``test_telemetry.py` |
| 缓存命中不受牵连 | `cache_hit=True``unavailable` → cost 仍为 `0.0`(锁定 §3.2 #5 的分支次序) | `test_telemetry.py` |
| 打捞前置条件 | 打捞 + usage 帧存在 → `estimated` 且 cost 非 None;打捞 + usage 缺失 → `unavailable` 且 cost 为 None(回归 §3.2 #4) | `test_openai_compat.py` |
| 结算不退多 | 未填 `est_tokens``tpm>0` 时失败请求,TPM 窗口残留量等于派生预扣量而非 0(回归 §3.2 #8) | `tests/contracts/test_limiter_contract.py` |
| **失败侧**结算不退多 | 未填 `est_tokens``tpm>0` 时失败请求,TPM 窗口残留量等于派生预扣量而非 0(回归 §3.2 #8) | `tests/contracts/test_limiter_contract.py` |
| **成功侧**结算不退多 | usage 缺失的**成功**调用后,TPM 窗口残留量等于派生预扣量而非 0(回归 §3.2 #9,本设计最易漏的一条)。现有锚点 `test_retry.py:149``_src("a", tpm=1000, est_tokens=400)` 旁加一个 `est_tokens=0` + usage 缺失的用例 | `tests/unit/test_retry.py``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` |
| OCR 不变 | OCR 成功行仍为 `measured` 且 settle 恒 0(防回归,锁定 §3.3 的剔出决定) | `tests/unit/test_ocr_client.py` |
| 值域封闭 | 库内所有生产点的产出恒落在三态内;公共 dataclass 不因越界值抛异常(锁定 §3.1 的落点决定) | `test_types.py` |
限流侧断言随 `tests/contracts/test_limiter_contract.py` 同时覆盖内存与 Redis 两后端(Redis 走真实实例,遵守共享后端不并跑纪律)。
@@ -134,4 +147,6 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按
## 9. 规模判定
改动跨 8 个源文件 + 2 份权威文档 + 6 个测试文件,属跨多文件功能 → 本设计经人类审批后须走 `writing-plans` 出实施计划,不得直接进实现
改动面(独立审查后重算):**6 个源文件**(`types.py``transports/openai_compat.py``middleware/telemetry.py``middleware/ratelimit.py``middleware/retry.py``embedding.py`;`ocr.py` 已剔出)、**7 个测试文件**、**3 份权威文档**(ARCHITECTURE.md、`migrations/chsanalyzer.md``.env.example`),外加按 `docs-convention.md` §2 必须同步的 CHANGELOG 与用户文档站 wiki(版本 bump 不得裸发)
属跨多文件功能 → 本设计经人类审批后须走 `writing-plans` 出实施计划,不得直接进实现。