From 61231f7f6e2af2000bf9daf59ed3e74da14d88cc Mon Sep 17 00:00:00 2001 From: iomgaa Date: Thu, 30 Jul 2026 09:51:01 -0400 Subject: [PATCH] docs: fold plan review into the est_tokens plan The reviewer confirmed the T1-T4 ordering holds -- it re-derived every intermediate state and checked that no construction path can produce est_tokens=0 with tpm>0 before T4 -- but found four gaps. Two existing tests go red and the plan never said so: test_types.py:94 asserts the very constraint T4 deletes, and test_embedding.py:105 is a transport-level case for the fallback T3 rewrites, easy to miss while looking only at test_openai_compat.py. The T4 acceptance line claimed all three settlement sides use the derived value, but the cancel branch never assigns actual and leaves it at the retry.py:329 initial zero -- an implementer would have "fixed" a branch the design freezes. Corrected here and in the design section 5 sentence it came from. USAGE_SOURCES would have landed with no consumer, so T1 now carries the two value-domain assertions the design asks for, including the one that pins the no-runtime-validation ruling. --- ...2026-07-30-est-tokens-decoupling-design.md | 2 +- .../2026-07-30-est-tokens-decoupling-plan.md | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md b/research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md index ac2178c..5125001 100644 --- a/research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md +++ b/research-wiki/designs/2026-07-30-est-tokens-decoupling-design.md @@ -106,7 +106,7 @@ CHS 原版 `config.py:55` 把它定义为"须 ≥ 最坏情形 token"——按 ## 5. 非功能维度 -**并发与取消**: `effective_est_tokens()` 是无状态纯方法(只读 frozen dataclass 字段),并发安全、无锁、可重复调用。本次改动不新增 `await` 点、不改变任何 `try/finally` 结构,取消穿透路径与 in-flight 释放语义原样不动。#8 与 #9 合起来保证预扣与结算恒取同一派生值,成功/失败/取消三侧均不产生押金偏差——这是本设计里最容易漏的一致性约束(初稿只写了失败侧,独立审查发现成功侧缺口)。 +**并发与取消**: `effective_est_tokens()` 是无状态纯方法(只读 frozen dataclass 字段),并发安全、无锁、可重复调用。本次改动不新增 `await` 点、不改变任何 `try/finally` 结构,取消穿透路径与 in-flight 释放语义原样不动。#8 与 #9 合起来保证**成功侧与非 dead 瞬时失败侧**的预扣与结算恒取同一派生值(`delta == 0`)——这是本设计里最容易漏的一致性约束(初稿只写了失败侧,独立审查发现成功侧缺口)。**取消 / RequestRejected / ResultInvalid / SourceDead 四侧不在此列**:它们的 `actual` 停在 `retry.py:329` 的初值 0、全额退回,属 §3.3 声明不动的既有行为。 **降级方向**: 不改变任何后端的降级方向。遥测侧仍是静默降级(`telemetry.py:161` 的 warning 不冒泡);限流侧仍是 `GovernanceBackendError` 上抛而非放行;TPM 计量不因 usage 帧缺失而静默失效(#9)。 diff --git a/research-wiki/plans/2026-07-30-est-tokens-decoupling-plan.md b/research-wiki/plans/2026-07-30-est-tokens-decoupling-plan.md index 1315df9..0a64ab1 100644 --- a/research-wiki/plans/2026-07-30-est-tokens-decoupling-plan.md +++ b/research-wiki/plans/2026-07-30-est-tokens-decoupling-plan.md @@ -12,7 +12,7 @@ | 若单独先做 | 后果 | |---|---| | 先改 usage 兜底为 `(0, 0)`,结算点还没切派生值 | 成功调用 `actual = 0` 而入场押了 `est_tokens`,`delta` 为负 → **押金整笔退回**,TPM 闸退化成进门即放行 | -| 先解绑约束(允许 `est_tokens=0`),结算点还没切派生值 | 同上,`est_tokens=0` 的源入场押派生值、结算退 0 | +| 先切入场(`ratelimit.py:26`)+ 解绑约束,而结算点还没切 | `est_tokens=0` 的源入场押派生值、结算退 0 → 同样泄漏。**注意机制**:若**只**解绑约束而 `ratelimit.py` 一行未动,后果不是泄漏而是入场**完全不预扣**(仍传 `est_tokens=0`)——那是设计 §2.2 已否决的"不预扣"退化形态。两者都要避免,故 T4 必须在 T2 之后 | | 先改 `openai_compat.py:176` 而 `_merge` 还是二值逻辑 | embedding 的 `unavailable` 批被 `any(== "estimated")` 判 False 从而**误标 `measured`**,cost 照算 | 因此排序为:**先加能力(零行为变更)→ 再把所有调用点切到新能力(此时等价,因显式值优先)→ 再让三态生效 → 最后解绑约束**。Task 1-2 完成后行为逐字不变,Task 3 才是行为变更主体,Task 4 才让派生值真正启用。**不要合并或调换 Task 2 / 3 / 4 的顺序。** @@ -80,6 +80,10 @@ def effective_est_tokens(self) -> int: - `tpm=6000, est_tokens=4000` → `4000`(显式值优先于派生) - `USAGE_SOURCES` 恰为三元集合 +**值域封闭的两条实质断言**(设计 §6 要求;缺了它们 `USAGE_SOURCES` 会沦为零消费者的死常量,且 §3.1 的落点裁决无回归保护): +- **生产侧封闭**: 参数化覆盖库内全部 `usage_source` 生产点(`_resolve_usage`、`_resolve_embedding_usage`、`_merge`、`TelemetryEmitter.emit_*`),断言产出恒 ∈ `USAGE_SOURCES`。此断言在 T1 阶段即可写(此时产出仅 `measured`/`estimated`),T3 完成后自动覆盖 `unavailable` +- **不做运行时校验**: `LLMResponse(usage_source="garbage")` 构造**不抛异常**——锁定设计 §3.1 的裁决(公共 frozen dataclass 不加 `__post_init__` 值域校验,否则裸 `ValueError` 不属四分类、会逃出 `chat()`)。没有这条,后人很容易顺手补上校验而击穿 `chat()` + **验证**: `conda run --no-capture-output -n PolyGateway pytest tests/unit/test_types.py -v` → 全 PASS ### T2 — 五个调用点切到派生值(零行为变更) @@ -158,12 +162,14 @@ else: - [ ] **改** `src/polygateway/embedding.py:397` `_total_cost` — 存在 `unavailable` 批时整体返回 `None`(逐批求和会给出偏低却看似有效的金额) - [ ] **改** `src/polygateway/types.py:273` — 行内注释 `# measured | estimated` → 三态(内核里不留矛盾注释) +- [ ] **改** `src/polygateway/transports/openai_compat.py:142` 与 `:172` — 两个函数的中文 docstring 仍写着"缺失/非法按 `est_tokens` 保守兜底并标 `estimated`",改完不改就留下两句主动陈述旧行为的文档(与 `types.py:273` 同一把尺子) **验收标准**: 全库不再有任何位置把 `est_tokens` 写进遥测用量;`ocr.py` 一字未动。 **测试要求**(先失败后通过): -- `test_openai_compat.py`:`est_tokens=4000` + usage 帧缺失 → `(0, 0, "unavailable")`(**改前返回 `(0, 4000, "estimated")`,故先失败**) -- `test_openai_compat.py`:打捞 + usage 帧存在 → `estimated`;打捞 + usage 缺失 → `unavailable`(改前后者为 `estimated`) +- `test_openai_compat.py`:`est_tokens=4000` + usage 帧缺失 → `(0, 0, "unavailable")`(**改前返回 `(0, 4000, "estimated")`,故先失败**;现有用例 `test_usage_missing_falls_back_to_est` 需改写) +- **改写** `tests/unit/test_embedding.py:105 test_missing_usage_falls_back_estimated` — 现断言 `prompt_tokens == 7 and usage_source == "estimated"`(夹具 `est_tokens=7`),改 `openai_compat.py:176` 后必然变红,须改为 `(0, "unavailable")`。这是一条**位于 `test_embedding.py` 里的 transport 级用例**,容易在只盯 `test_openai_compat.py` 时漏掉 +- `test_openai_compat.py`:打捞 + usage 帧存在 → `estimated` **且 cost 非 None**;打捞 + usage 缺失 → `unavailable` **且 cost 为 None**。cost 配套断言不可省——#4 的真正目的就是防 `0/0` 被算成假的 `0.0`,只断言 `usage_source` 钉不住它 - `test_telemetry.py`:成功行 `usage_source="unavailable"` → `record_llm_call` 收到 `cost=None`(改前按 4000×输出单价算出 `0.032`) - `test_telemetry.py`:`cache_hit=True` 且 `unavailable` → cost 仍为 `0.0`(锁定分支次序) - `test_telemetry.py`:失败尝试与终态失败行 `usage_source == "unavailable"` @@ -183,15 +189,15 @@ if self.tpm > 0 and self.est_tokens <= 0: `_validate_gates` 的其余部分(`timeout_s > 0`、四个限额非负)**保留不动**。`est_tokens` 字段本身与 `{SCOPE}__{PROVIDER}__{N}__EST_TOKENS` 环境键保留不删不改名(迁移兼容硬约束);`config.py:40` 的键映射无需改动。 -**验收标准**: `tpm=6000, est_tokens=0` 可构造;该源入场预扣 100 且成功/失败/取消三侧结算均按 100 结算,TPM 窗口不出现负向偏差。 +**验收标准**: `tpm=6000, est_tokens=0` 可构造;该源入场预扣 100,**成功侧与非 dead 瞬时失败侧**按 100 结算(delta=0)。**取消 / RequestRejected / ResultInvalid / SourceDead 四侧维持既有的 `actual=0` 全额退回**——`retry.py:355-359` 的取消分支不给 `actual` 赋值、停在 `:329` 初值,这是设计 §3.3 声明不动的既有行为,**不要**为了凑"三侧一致"去改它。 **测试要求**(先失败后通过:改前构造即抛 `ValueError`): -- `test_types.py`:`tpm=6000, est_tokens=0` 构造成功 +- **改写** `tests/unit/test_types.py:94 test_tpm_requires_est_tokens` — 它现在断言 `_make_source(tpm=10000, est_tokens=0)` 抛 `ValueError`,删约束后必然变红。保留后半条正向断言(`est_tokens=800` 仍原样返回),把前半条改为"构造成功且 `effective_est_tokens()` 返回派生值" - `test_retry.py`:**成功侧**——未填 `est_tokens`、`tpm>0`、usage 帧缺失的成功调用后,TPM 窗口残留量等于派生预扣量而非 0(**这是设计中最易漏的一条**,回归 §3.2 #9;在 `test_retry.py:149` 的 `_src("a", tpm=1000, est_tokens=400)` 旁加 `est_tokens=0` 用例) -- `test_retry.py`:**失败侧**——同配置的瞬时失败调用后,窗口残留量同为派生预扣量(回归 §3.2 #8) -- `tests/contracts/test_limiter_contract.py`:上述两条在内存与 Redis 双后端均成立 +- `test_retry.py`:**失败侧**——同配置的非 dead 瞬时失败调用后,窗口残留量同为派生预扣量(回归 §3.2 #8) +- `tests/contracts/test_limiter_contract.py`:**只加后端级断言**——传入派生值时双后端的结算口径一致。**不要**在契约文件里写端到端用例:该文件直接驱动 limiter(形如 `limiter.try_acquire("s1", 0)`),不经 `QuotaGate`/`RetryMW`,照字面写会产出 `try_acquire(src.effective_est_tokens())` + `settle(同值)` 的退化用例——只测了后端算术,没测调用点是否真的切了派生值。上面两条端到端断言的载体是 `retry.py`,放 `test_retry.py`(内存后端) -**验证**: `conda run --no-capture-output -n PolyGateway make ci`(即 check + test,含 import-linter 契约)→ 全 PASS +**验证**: `make ci`(即 check + test,含 import-linter 契约)→ 全 PASS。**不要**在外层再套 `conda run`:`Makefile` 的 `check`/`test` 目标内部已各自 `conda run -n $(ENV)`,嵌套后外层的 `--no-capture-output` 也管不到内层缓冲 ### T5 — 权威文档与发布物同步 @@ -206,7 +212,7 @@ if self.tpm > 0 and self.est_tokens <= 0: **测试要求**: 纯文档,无行为测试。以 `grep` 输出为验收证据。 -**验证**: `conda run --no-capture-output -n PolyGateway make ci` → PASS;`grep -rn "EST_TOKENS 必填" . --exclude-dir=.git` → 无输出 +**验证**: `make ci` → PASS;`grep -rn "EST_TOKENS 必填" . --exclude-dir=.git` → 无输出 ## 5. 完成判定