Files
PolyGateway/research-wiki/findings/2026-09-10-136-call-deadline-validation.md
T
iomgaa b1bc06e2b1 docs: document the optional call deadline and cancellation settlement
The deadline governs waiting, not the moment a call returns: cleanup
still runs in finally, so the return time is the deadline plus the
cleanup cost (5-7x the deadline in the measured fixture). An expiry
therefore does not mean nothing was produced or nothing was billed.
Leaving the key unset keeps 1.3.5 semantics verbatim, which also keeps
its two long waits: a pure 429 sequence can still wait for a long time,
and a large finite Retry-After is still slept in full because the
library deliberately does not clamp the hint with backoff_max_s.

- CHANGELOG unreleased section states those three sentences, records the
  cancellation settlement change (an attempt cancelled after the port
  started but before the settlement is known keeps its reservation at
  the source estimate: over-charge rather than refund something the
  upstream may already have billed; known settlements and unclassified
  escapes are untouched) and warns that except GatewayUnavailableError
  does not catch CallDeadlineExceeded
- README in four places: the capability table, the exception handling
  example, the "which exceptions reach the caller" table and the error
  model section, which now spells out the remaining large-but-finite
  Retry-After wait
- .env.example documents LLM__CALL_DEADLINE_S as commented out
- new findings file indexes the red/green evidence, the commands and
  their exit codes, what was not run and who covers it, and repeats the
  three residual risks

No version bump, no tag, no release: those belong to the release
checklist.
2026-09-10 04:33:55 -04:00

9.6 KiB
Raw Blame History

type, node_id, title, date
type node_id title date
finding finding:2026-09-10-136-call-deadline-validation 1.3.6 可选调用期限与取消结算验证记录 2026-09-10

1.3.6 可选调用期限与取消结算验证记录

范围:分支 feature/1.3.6-call-budgets 上的 T1T3 三个行为提交(1ff83bb / 9474c76 / da77b12)与 T4 文档提交。设计 research-wiki/designs/2026-09-09-136-call-budgets-design.md(人类已批准),计划 research-wiki/plans/2026-09-10-136-call-deadline.md。 本文件是证据索引:原始输出在 tests/outputs/136/(按纪律不提交),此处只记路径、命令、退出码与结论。 环境:conda 环境 PolyGatewayPython 3.12.13conda run -n PolyGateway python -V 实测)。所有 pytest/lint 命令均不接管道,退出码直取。 版本号未 bump、未 tag、未发布——发布清单(CLAUDE.md §4.4.1)不在本轮范围。

1. 红绿证据索引

任务 阶段 证据文件 结果
T1 取消结算 红/绿 未落盘(见 §1.1 诚实说明) 见 §1.1
T1 真实 Redis 绿(本轮在 HEAD da77b12 上复跑) tests/outputs/136/t4/redis_cross_connection.txt 8 passedexit=0
T2 期限 红(值域+形态) tests/outputs/136/t2/red_deadline.txt 收集期 1 errordeadline.py 缺席),exit=2
T2 期限 绿(test_deadline.py tests/outputs/136/t2/green_deadline.txt 22 passedexit=0
T2 接线中途 绿 tests/outputs/136/t2/unit_contracts_midway.txtunit_contracts_after_wiring.txt 1560 passed, 17 skipped
T2 入口冒烟 绿 tests/outputs/136/t2/entry_smoke.txt 四个方法签名含 call_deadline_s;到期异常 has retry_after_s: Falseexit=0
T2 回归门 绿 tests/outputs/136/t2/final_unit_contracts.txt 1572 passed, 17 skippedpytest_exit=0
T2 lint 红→绿 tests/outputs/136/t2/lint.txtFound 3 errorslint_exit=2)→ lint_final.txtAll checks passed! + Contracts: 1 kept, 0 broken.lint_exit=0 修后绿
T2c 补测 红 pass1 tests/outputs/136/t2c/red_pass1_import_absent.txt 3 errors in 0.29s(三个模块收集期 ImportError
T2c 补测 红 pass2 tests/outputs/136/t2c/red_pass2_real_reasons.txt + red_method_and_reasons.txt 32 failed;分布见 §1.2
T2c 补测 绿 green_unit_after_hardening.txt1530 passed)、green_client_recheck.txt110 passedEXIT=0)、green_recheck_client_embedding.txt165 passed)、green_unit_contracts.txt1592 passed, 17 skipped)、final_unit_contracts.txt1606 passed, 17 skipped 全绿
T2c lint 绿 tests/outputs/136/t2c/lint.txt All checks passed! + Contracts: 1 kept, 0 broken.
T3 Retry-After tests/outputs/136/t3/red.txt 6 failed, 8 passed, 129 deselected
T3 Retry-After 绿 green_file.txt143 passed)、green_unit_contracts.txt1606 passed, 17 skipped 全绿
T3 lint 绿 tests/outputs/136/t3/lint.txt All checks passed! + Contracts: 1 kept, 0 broken.

tests/outputs/136/t2/lsp_noise_refutation.txtt2c/lsp_noise_refutation.txt 记录编辑器 LSP 报的 import/属性告警属环境噪声(pydantic 在 conda 环境可解析),不是代码缺陷。

1.1 T1 的证据形态(诚实说明)

T11ff83bb)的先红后通过证据产生于当时的会话工具输出,未落盘为 tests/outputs/136/t1/ 文件。本文件不追认那次输出,只登记两项当下可复核的替代证据:

替代证据 内容
提交 1ff83bb 的 diff 三个源文件 + 四个测试文件共 213 插入;测试侧含 S3/S7(取消结算按 est)、S5-dead(仍 0)、S8RuntimeError 逃逸仍 0)、真实 usage 恰为 0 的成功仍 0 四组断言
本轮在 HEAD da77b12 上重跑真实 Redis pytest tests/integration/test_redis_cross_connection.py -q8 passedexit=0tests/outputs/136/t4/redis_cross_connection.txt

结论口径:T1 的“红”只有会话内证据、无归档文件;T1 的“绿”在当前 HEAD 上已被真实 Redis 复现证实。

1.2 T2c 两趟红证据的方法说明(诚实说明)

T2c 的红证据是在**基线 1ff83bbT2 之前)**的 git worktree --detach 检出上取的,用 PYTHONPATH=<worktree>/src 覆盖 editable .pth(已实测 polygateway 加载自 worktree 且 deadline.py 缺席):

趟次 做法 结果
pass1 用例原样跑 三个测试模块收集期 ImportErrorCallDeadlineExceeded 不存在)→ 3 errors。只证明符号缺席,没有执行到函数体
pass2 仅把缺失的导入符号替换成本地占位异常类shim),让函数体真正跑起来 32 failed27 条“参数/属性不存在”chat() 9、embed() 4、GatewaySettings.__init__() 3、GatewaySettings.call_deadline_s 属性 3、recognize_text() 2、GatewayClient.__init__() 2、parse_layout()OcrClient.__init__()EmbeddingClient.__init__()GatewayClient._call_deadline_s 各 1)+ 5 条 Failed: DID NOT RAISE ValueError

该 shim 是一次性本地脚手架,未提交、不在任何分支上;它只替换导入符号,不改被测源码。故 pass2 的红是针对预 T2 源码的真实失败原因分布,而非构造错误——但读者需知这份红无法从仓库检出复现,只能从上表与 red_method_and_reasons.txt 复核。

2. 命令与退出码

命令(前缀均为 conda run -n PolyGateway python -mlint 为 make lint 何时 结果
pytest tests/unit/test_deadline.py -q T2 红 exit=2collection error,符合预期)
pytest tests/unit/test_deadline.py -q T2 绿 22 passedexit=0
pytest tests/unit tests/contracts -q T2 门 1572 passed, 17 skippedexit=0
pytest tests/unit/test_client.py tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_config.py -q -rf -k "…deadline…" T2c 红 32 failed(基线 worktree,见 §1.2
pytest tests/unit tests/contracts -q T2c 门 1606 passed, 17 skippedexit=0
pytest tests/unit/test_openai_compat.py -q -rf -k RetryAfterNonFinite T3 红 6 failed, 8 passed, 129 deselected
pytest tests/unit/test_openai_compat.py -q T3 绿 143 passed
pytest tests/unit tests/contracts -q T3 门 1606 passed, 17 skippedexit=0
pytest tests/integration/test_redis_cross_connection.py -q T1T4 复跑 8 passedexit=0
make lintruff + import-linter T2T2cT3 收尾 All checks passed!Contracts: 1 kept, 0 broken.(新 polygateway.deadline 层在内)

T4(本次文档提交)不含行为变更,故未跑测试;仅新增上表最后一行的真实 Redis 复跑作为 §1.1 的替代证据。

3. 豁免索引(哪些门没跑,为什么,谁来兜)

未执行项 原因 兜底责任
pytest -m slow(真实网关 e2e、Redis 时间语义变体) 成败取决于外部服务当下状态,默认被 addopts = "-m 'not slow'" 排除;计划 §5 明确本轮不跑 发布清单(CLAUDE.md §4.4.1)第 4 步,合并 main 后统一执行
tests/e2e/ 四个文件 同上,本版零付费调用 同上
真实网关的期限行为实测 期限用例用真实事件循环时钟+假 transport 构造,余量 4–10 倍,不依赖网关 发布清单第 4 步的 e2e 顺带覆盖;本轮无真实网关证据
模型能力矩阵复验 本版未触碰推理/能力表 不适用
跨 Python 版本验证 见 §4 残余三 未兜底,登记为残余

真实 Redis 不在豁免之列:T1 已证、本轮在 HEAD 上复跑(8 passed),未以 memory 后端冒充。

4. 残余风险(三条,逐条复述设计 §12)

# 残余 诚实口径
1 取消结算按 est 保留预扣 这是保守选择,不是“上游已计费”的证明。库无法知道端口已开始的那次调用是否真的产生了计费用量;方向定为宁多扣不空退(多扣只损失本窗口一点额度,空退会让已计费用量绕过闸门)。真实 usage 已知(含恰为 0)与已判 SourceDead0 不被覆写;未分类异常逃逸仍按 0,属已知残留,本版不动
2 清理期自抛 TimeoutError无终态遥测行 与 1.3.5 的裸 TimeoutError 穿透同一口径(这条路径一直存在、一直没有终态行),本版没有让它变坏;但期限把这条路径常态化了——启用期限后触发清理的频率上升,其可达性随之上升。with_call_deadline 的局部变量身份比较保证这种 TimeoutError 不会被误标成 CallDeadlineExceededtest_deadline.py 有断言钉住)
3 跨 Python 版本仅 3.12.13 有实证 asyncio.timeoutcm.expired()uncancel() 行为与清理期异常传播是探针在 3.12.13 单一版本上实测的;3.13+的行为未验证。库声明 3.12+,故这是真实的验证缺口,不是理论担忧

5. 未在本轮做的事

  • 不实现 issue #24(长尾对冲):未获批准,代码与文档均无 hedge 机制,本版不缓解 #24
  • 不修 ResultInvalidRequestRejected 已计费坏结果仍退全款:属另一族记账语义,未批准,登记待立 issue。
  • 不 bump 版本号、不打 tag、不构建、不上传 registry、不同步 wiki——全部留给发布清单。