Files
PolyGateway/research-wiki/plans/2026-09-10-24-hedged-requests.md
T

34 KiB
Raw Blame History

type, node_id, title, date
type node_id title date
plan plan:2026-09-10-24-hedged-requests issue #24 长尾对冲请求与裸生成时间实施计划 2026-09-10

issue #24 长尾对冲请求与裸生成时间实施计划

设计:research-wiki/designs/2026-09-10-24-hedged-requests-design.md,人类于 2026-09-10 正式批准(§9 H1H7 及增补 H8 全数获批;H1 取方案 A 并发对冲,H3 取零新列档,H5 取 v1 只允许单路)。 计划审核门:Claude 自审 + 独立模型审查;plan 无人类门,审毕直接执行。 目标:① chat 链路可选对冲(挂起超阈值时并发向异源再发一次,先回者赢、输家取消);② CallStatshedges/generation_ms/hedge_won,三条链路 _attempt 加 transport 级计时;③ 默认关闭,缺省行为逐字等于 1.3.6。 方案:设计 §3 方案 A——对冲轮 = 一次"超级尝试",复用完整准入(QuotaGate + BreakerGate + pacer + 冷却备忘),输家落 1.3.6 取消 S3 格结算,零新错误分类。 技术:Python 3.12+、asyncio 任务组编排(asyncio.wait(FIRST_COMPLETED) + 显式收口)、frozen dataclass、pytest + 事件驱动假 transport + 真实 loop 钟(410× 余量)、ruff、import-linter。 基线 HEAD:166b286(main,1.3.6 已发布);分支 feature/24-hedged-requests

范围纪律: 不改四分类/熔断语义/429 分账/限流 Lua/deadline.py/Permit 端口;不做 embedding/OCR 对冲、分位数触发、同源对冲、per-call 对冲参数;不引入 shield/后台任务;不加遥测新列(hedge_cancelled 字符串 + CallStats 三字段经既有通道带出)。

1. 边界、授权与执行纪律

项目 固定边界
唯一 writer 一工作区一 writer;父会话负责前台委派与审核派发。1.3.X 合并/发布授权沿用;跨到 1.4 或新公共面变化须停下确认
公共面 只做设计 §9 已批准项:Transport.completefirst_token_event 必填 kw(H2)、两配置键 {SCOPE}__HEDGE__AFTER_S/{SCOPE}__HEDGE__MAX_EXTRA(H4)、GatewayClient.__init__ 两 keyword-only 参数、CallStats 三字段(H3+H8)、输家 hedge_cancelled 标签。不新增其它键/端口方法/遥测列/异常类
对冲边界 仅 chat(RetryMW);EmbeddingClient/OcrClient 不加对冲参数(非目标 A),但它们的 _attempt 照样加 generation 计时点(H8);v1 单路对冲(H5)
记账边界 输家取消走既有取消路径:settlement_known=Falsesettle(est) 保留预扣(retry.py:327-331);输家 record_failure、不喂熔断/健康分(挂起 ≠ 源死亡);两任务都失败才进重试且 fails += 1 只计一次(H6)
取消铁律 外部取消到达时两任务同消并穿透;不 shield、不留后台任务;收口 await 允许被再取消(同 136 清理纪律)
降级方向 限流/熔断后端不可用 → 照常冒泡(fail-closed);准入失败 → 静默放弃对冲,原请求继续等(不抛错、不硬等);遥测仍 warning 降级
时钟纪律 对冲触发只用相对时长 + 事件循环钟(asyncio.wait timeout),绝不读注入 now;generation_ms 计时用该链路既有注入钟(与 total_latency_ms 同钟,差值才有意义;生产即 time.monotonic)
证据纪律 不打印 .env/token/Authorization;不提交 .pi/tests/outputs/;测试事件驱动,禁 sleep 撞窗口;对冲阈值用 0.05s 级真实 loop 钟,断言容差 410×(tests/unit/test_streaming.py 既有范式,不标 slow)

Skill 纪律:T1T3 行为变更执行 test-driven-development(先红后绿证据落在本会话工具输出);每次提交执行 commit;T4 前执行 requesting-code-reviewverification-before-completion;异常先 systematic-debugging

保真校验: 本计划不涉及 reference/ 参考实现迁移(对冲编排为 D13 自研语义,蓝本即本库 1.3.6 的准入/结算/取消机制),保真校验不适用。

2. 文件职责与不变接缝

动作 精确路径 职责
修改 src/polygateway/ports.py Transport.complete(:51-60)加 first_token_event 必填 kw;顶部加 import asyncio(stdlib,不违 P7)
修改 src/polygateway/transports/openai_compat.py complete(:426-436)加参透传;_complete_stream(:552 起)首 token 处(:573-575)置位;_complete_once(:646 起)接收但永不置位(docstring 明写)
修改 src/polygateway/middleware/retry.py _attempt(:270-353)加 first_token_event/generation_sink 私有 kw + transport 级计时;__call__(:224-267)单轮尝试 → 任务组编排;_past_hedge_window/_attempt_hedged/_combine_failures 新方法;CancelledError 分支(:327-334)hedge_cancelled 标签;__init__hedge_after_s
修改 src/polygateway/middleware/admission.py pick(:171-213)加 keyword-only exclude: frozenset[str] | None = None
修改 src/polygateway/types.py CallStats(:296-318)增三字段(全带默认值,追加在 total_latency_ms 后);_CallContext(:321-360)docstring 修订 + 三个计数 + record_generation/register_hedge;snapshot(:348-353)填三字段
修改 src/polygateway/config.py _RESERVED_SEGMENTS(:57)加 "HEDGE";GatewaySettings 字段(:190 后)加 hedge_after_s/hedge_max_extra;__post_init__(:201 后)加 _validate_hedge();新增 _load_hedge(:695 _load_call_deadline 之后)与模块级 check_hedge_assembly 守卫;from_env(:396 同列)透传
修改 src/polygateway/client.py __init__(:239 后)加两 keyword-only 参数,入口即校(复用 check_hedge_assembly);RetryMW 构造(:254-273)传 hedge_after_s;from_settings(:511 同列)透传
修改 src/polygateway/embedding.py _attempt(:351 起,transport 调用 :373)两侧计时 + 成功分支 record_generation(accumulate=True)
修改 src/polygateway/ocr.py _attempt(:376 起,_invoke 调用 :397)两侧计时 + 成功分支 record_generation(accumulate=False)
新建 tests/unit/test_hedge.py 对冲编排全部用例(批次 G)
修改 tests/unit/test_openai_compat.py 批次 A(端口加参);_complete helper(:82-90)同步签名
修改 tests/unit/test_retry.py test_types.py test_embedding.py test_ocr_client.py test_client.py test_config.py 批次 BF、H;test_retry.py:82 FakeTransport 签名同步
修改 tests/unit/test_backpressure.py:213 tests/unit/test_ports.py:75 tests/unit/test_client.py:1741 tests/integration/test_redis_cross_connection.py:78 tests/e2e/conftest.py:284-303 fake/包装 transport 签名同步(e2e 包装转发 first_token_event)
修改 tests/unit/test_live_evidence.py:270-279(_complete helper)+:1258(直调 ObservedTransport(...).complete(...));tests/unit/test_usage_source_domain.py:135(直调真实 OpenAICompatTransport.complete) 调用方同步(独立审 B1): helper 加 first_token_event=None 转发、两直调传 None;不传则必填 kw 报 TypeError,unit 门必红
修改 CHANGELOG.mdREADME.md.env.example 新键、三字段、对外承诺措辞(§4 T4)
新建 research-wiki/findings/2026-09-10-24-hedged-requests-validation.md 红绿、命令、豁免索引,≤300 行

不改:errors.py(零新异常)/deadline.py/telemetry/schema.py(零新列)/middleware/{ratelimit,breaker,structured,cache,telemetry}.py/backends/**(含全部 Lua)/tests/contracts/**;embedding.py/ocr.py 除计时点外一字不动;StallClock/backoff_delay/settle_and_release 逐字不动。若必须突破本清单,先说明最小原因交父会话核定。

3. 跨任务接口(可执行定义,禁止占位)

3.1 T1:Transport.complete 加首 token 事件(H2)

ports.py:51-60 签名改为(顺序追加在 reasoning_effort 后,必填、不设默认值,与端口既有约定同款):

async def complete(
    self, *, messages: list[dict[str, Any]], source: SourceConfig, stream: bool,
    overlay: dict[str, Any], call_id: str, reasoning_effort: Effort | None,
    first_token_event: asyncio.Event | None,
) -> TransportResult: ...

docstring 补两句:None = 调用方不观测首 token(未启用对冲);非流式实现永不置位(物理上无中途信号,事件自然退化为纯时间阈值)。OpenAICompatTransport.complete(:426-436)加同款必填 kw 并透传两条路径;_complete_stream 在 :573-575 if ttft_ms is None: 块内加:

if first_token_event is not None:
    first_token_event.set()

_complete_once(:646)接收该参数但永不置位,docstring 明写"非流式无中途信号"。retry.py:291-300 调用处 T1 先传字面 first_token_event=None(必填参数不传即全库 TypeError;T3 换成真事件)。

假 transport 同步纪律(test_retry.py:71-73 既有注释的同款):签名加 first_token_event,不给默认值;除 test_hedge.py 外所有 fake 忽略该参数即可。e2e 包装(tests/e2e/conftest.py:284-303)必须转发给被包 transport。

3.2 T2:CallStats 三字段与 _CallContext 计数(H3+H8)

types.py CallStats(:296-318)在 total_latency_ms 后追加:

hedges: int = 0
"""本次逻辑调用实际并发发出的对冲路数(触发但准入失败静默不计);1.3.6 及以前恒 0。"""
generation_ms: int = 0
"""裸生成时间: 赢家/成功那次 transport 调用的墙钟时长(口径见设计 §4.5 H8)。"""
hedge_won: bool = False
"""赢家是否为对冲路;无对冲恒 False。"""

_CallContext(:321-360):docstring "每调用一个实例的单任务对象" 修订为 "每逻辑调用一个实例,可多任务并发登记(对冲);全部方法无 await,事件循环内任务安全";__slots____init___generation_ms: int/_hedges: int/_hedge_won: bool;新增两个同步方法:

def record_generation(self, elapsed_ms: int, *, accumulate: bool) -> None:
    """chat/OCR 覆盖(结构化重问最后一轮为准);embedding 分批累加。"""
    self._generation_ms = self._generation_ms + elapsed_ms if accumulate else elapsed_ms

def register_hedge(self, *, hedge_won: bool) -> None:
    """对冲路实际发出即计数;赢家裁定后一次性登记。"""
    self._hedges += 1
    self._hedge_won = hedge_won

snapshot(:348-353)按字段名填 hedges=self._hedges, generation_ms=self._generation_ms, hedge_won=self._hedge_won

3.3 T2:三条链路 _attempt 的 transport 级计时点

统一形态:计时只包 transport 调用本身,用该链路既有注入钟 self._now(生产 = time.monotonic);起点紧贴调用前、终点在返回后首句,中间无 await(取消落进来时 transport 未返回,本就不计)。

链路 计时点 记录点
chat(retry.py:291-300) gen_started = self._now() 紧贴 await self._transport.complete(...) 前;返回后首句 generation_sink.append(int((self._now() - gen_started) * 1000)) 不在 _attempt 内记录——对冲赢家归属由编排裁定。_attempt 加私有 kw generation_sink: list[int];__call__(:248-256)每轮建 sink,outcomeLLMResponsecall_context 非 None 时 record_generation(sink[0], accumulate=False)(与 register_attempt 同款 None 守卫)
embedding(embedding.py:373) 同款两侧包 await self._transport.embed(...) 成功分支(:386 settlement_known = True 之后)context.record_generation(gen_ms, accumulate=True)——分批累加
ocr(ocr.py:397) 同款两侧包 await self._invoke(...) 成功分支 context.record_generation(gen_ms, accumulate=False)

缓存命中/空输入不产生 transport 调用 → generation_ms 恒 0(0 是实测,不违 types.py "None 表未知" 惯例——本字段语义是时长不是用量)。

3.4 T3:对冲编排(retry.py,设计 §4.6 的唯一实现形态)

RetryMW.__init__ 加 keyword-only hedge_after_s: float | None = None(存 self._hedge_after_s;hedge_max_extra 不下传——v1 编排固定单路,H5;配置面值域与 v1 生效口径由 §3.6 守卫负责)。模块级:

_HEDGE_LOSER_ATTR = "_polygateway_hedge_loser"
"""编排在 cancel() 之前给输家任务置位的标记;_attempt 读它选遥测标签。"""

__call__(:244-256)循环体内:self._hedge_after_s is None逐字旧路径(_attemptfirst_token_event=None,单任务,默认关闭回归门据此成立);否则 outcome = await self._attempt_hedged(request, picked, reasons, attempt_fails),其后的 _is_rate_limited/refund/fails 计数机制一字不动。

_attempt_hedged 编排(Phase 注释组织;_attempt 相应加 first_token_event kw 取代 T1 的字面 None):

# Phase 1 启动原路: 事件与 sink 每轮新建(局部状态,严禁实例属性)
source, permit, entry = picked
first_token: asyncio.Event = asyncio.Event()
sink_p: list[int] = []
primary = asyncio.create_task(
    self._attempt(request, source, permit, entry, reasons, attempt_fails,
                  first_token_event=first_token, generation_sink=sink_p)
)
# Phase 2 触发窗: 只认"阈值到 + 首 token 未至 + 原路在途"(loop 相对时长,不读注入 now)
if not await self._past_hedge_window(primary, first_token):
    return await primary  # 原路已了结/首 token 已至: 等价于未配置对冲
# Phase 3 异源准入(完整 pick 路径, 无旁路): 拿不到候选 = 静默等原路
hedge_picked, _ = await self._admission.pick(
    reasons, attempt_fails, exclude=frozenset({source.name})
)
if hedge_picked is None:
    return await primary

Phase 4-5(赢家裁定与收口)规则:

sink_h: list[int] = []
hedge = asyncio.create_task(
    self._attempt(request, *hedge_picked, reasons, attempt_fails,
                  first_token_event=None, generation_sink=sink_h)  # v1 单路: 对冲路不再触发梯次
)
done, pending = await asyncio.wait({primary, hedge}, return_when=asyncio.FIRST_COMPLETED)
  • 裁定:done 中有成功(LLMResponse)即赢家;两路同时成功(竞速)→ 原路优先(hedge_won=False,保守不弃原路成果);done 全是失败且 pending 非空 → 等 pending 了结后再裁定。
  • 收口:赢家产生后,对 pending 中的输家先 setattr(task, _HEDGE_LOSER_ATTR, True)task.cancel(),然后 await asyncio.gather(*pending, return_exceptions=True)——输家 finally 的结算/遥测跑完才返回(快照含输家,attempts==2;client.py:442 快照在返回后,天然在收口之后)。两路同时完成的竞速落选者不置标记(它没被取消,attempt 行是正常成功/失败行)。
  • 登记:对冲路实际发出(Phase 3 之后)即计一次;赢家裁定后 context.record_generation(赢家 sink[0], accumulate=False) + context.register_hedge(hedge_won=winner is hedge);两败轮次(无赢家)同样照登 register_hedge(hedge_won=False)——设计 §4.5 已批准口径是"实际并发发出即计,触发但准入失败静默不计";call_context is None 时跳过(同 register_attempt 守卫)。
  • 两败汇合(H6,只计一次预算;deterministic):
def _combine_failures(primary_f: _Failed, hedge_f: _Failed) -> _Failed:
    """任一非 429 优先(计预算);两路皆 429 才按 429 免预算退还 stall 账;同类取原路。"""
    if _failure_reason(primary_f.exc) == "rate_limited" != _failure_reason(hedge_f.exc):
        return hedge_f
    return primary_f
  • 取消穿透:Phase 2-5 全程包 except BaseException(含 CancelledError 与准入冒泡的 GovernanceBackendError)→ 两任务(存在者)cancel() + gather(return_exceptions=True) 尽力收口后 raise 原异常;收口 await 允许被再取消,不 shield。

_past_hedge_window 实现红线(waiter 任务必须收口,且不得吞外部取消):

async def _past_hedge_window(self, primary: asyncio.Task, first_token: asyncio.Event) -> bool:
    waiter = asyncio.create_task(first_token.wait())
    try:
        await asyncio.wait({primary, waiter}, timeout=self._hedge_after_s,
                           return_when=asyncio.FIRST_COMPLETED)
    finally:
        waiter.cancel()
        try:
            await waiter
        except asyncio.CancelledError:
            if asyncio.current_task().cancelling():  # 外部取消,穿透
                raise
    return not primary.done() and not first_token.is_set()

输家标签:_attempt 的 CancelledError 分支(:327-334)把 error="cancelled" 换成按标记选择——label = "hedge_cancelled" if getattr(asyncio.current_task(), _HEDGE_LOSER_ATTR, False) else "cancelled"。竞速误贴(外部取消与对冲取消同时到达)记账方向一致(est 保留),属设计 §4.5 已批准的可接受残留。

3.5 T3:admission.pick 私有排除参数(设计 §4.2)

pick(:171-173)签名加 keyword-only exclude: frozenset[str] | None = None;候选循环首部加:

if exclude and cand.name in exclude:
    continue  # 被排除不是源的拒绝: 不计 gate_rejections、不写 reasons

理由:写进 reasons/gate_rejections 会污染 on_no_runnable(:224)的分派判据与 per_source_reasons 对账。对冲调用方拿到 None 的处置是静默等原路(§3.4 Phase 3),严禁对它调 on_no_runnable(那会按 quota/circuit 策略抛错或睡觉,语义全错)。三条既有调用方(retry.py:244embedding.py:332ocr.py:348 所在循环)不传该参数,行为逐字不变。

3.6 T3:配置两键 + 两守卫 + client 透传(H4)

config.py 改动(单一定义点纪律,值域/交叉守卫只写一份):

精确定义
保留段 :57 _RESERVED_SEGMENTS"HEDGE"(防 provider 段撞名);两键均 3 段,:405len(parts) != 4 判据天然跳过 _load_sources
loader 新增 _load_hedge(scope, env)(:695 _load_call_deadline 之后):AFTER_S_first + _cast(..., "float", ...) + ensure_call_deadline(origin 传实际命中键名,同 _load_call_deadline 纪律);MAX_EXTRA_first + _cast(..., "int", ...),未设 = 1;返回 {"hedge_after_s": ..., "hedge_max_extra": ...}
字段 GatewaySettings :190 后追加 hedge_after_s: float | None = Nonehedge_max_extra: int = 1
守卫 新增模块级 check_hedge_assembly(*, hedge_after_s, hedge_max_extra, sources, call_deadline_s, origin),返回归一化后的 hedge_after_s;GatewaySettings.__post_init__(:201 后)加 _validate_hedge() 调它并 object.__setattr__ 写回归一化值(同 _validate_call_deadline 形态);GatewayClient.__init__ 调同一份(client.py:22 已 import config,合法)
from_env :396 同列加 **_load_hedge(scope_u, env)

check_hedge_assembly 守卫全表(设计 §5;hedge_after_s is None 时值域归一化后直接返回,交叉守卫不查):

守卫 判定
值域 ensure_call_deadline(hedge_after_s, origin)(None 或有限正数,复用 deadline.py 同款校验)
hedge_after_s ≥ min(源 timeout_s) ValueError(对冲永不可能触发,配置即错误)
hedge_after_s ≥ call_deadline_s(两者皆设) ValueError(期限先于对冲触发)
hedge_after_s ≥ min(已设 ttft_timeout_s) 装配期 warning(流式档被 TTFT 看门狗先行切断;非流式仍有效,不升 ValueError)
单源 scope 设了阈值 装配期 warning,允许(运行期拿不到候选自然静默)
hedge_max_extra 非 int/bool 或不在 [1,3] → ValueError;>1 → warning"v1 仅单路对冲生效,梯次追加为 H5 预留"(值域按设计 §5 表放到 3,运行期 H5 只允许 1,warning 保 fail-loud 不静默)

client.py:__init__ :239 后加 keyword-only hedge_after_s: float | None = None, hedge_max_extra: int = 1,在 :245-247 期限校验同列调 check_hedge_assembly(hedge_after_s=..., hedge_max_extra=..., sources=sources, call_deadline_s=self._call_deadline_s, origin="GatewayClient(...)");RetryMW 构造(:254-273)传 hedge_after_s= 归一化值(max_extra 不下传,§3.4);from_settings :511 同列传 settings.hedge_after_s/settings.hedge_max_extraEmbeddingClient/OcrClient 不加对冲参数;它们的 settings 嵌 GatewaySettings 故守卫照常跑(文档明写对冲键只对 chat 生效)。

4. 任务与提交点(4 个原子提交)

T0:设计增补并入与本计划(本任务,无代码)

产出:设计文档 §4.5/§7/§8/§9 增补(已完成)+ 本计划。不提交代码。

T1 → 提交 1 feat: add a required first-token event to the transport port

  1. 先红:批次 A(tests/unit/test_openai_compat.py 四用例),确认失败为 TypeError(签名无此 kw)而非断言值不符。
  2. 按 §3.1 改 ports.pyopenai_compat.pyretry.py:291-300 传 None;同步六处 fake/包装(§2 表)+ test_live_evidence.py/test_usage_source_domain.py 三处调用点(§2 表末行);_complete helper(:82-90)加 first_token_event=None 默认转发(测试设施,与生产端口的"必填无默认"约定不冲突——生产端口不变)。
  3. 后绿:批次 A 通过;pytest tests/unit -q 全绿且不改一行既有断言。
  4. 暂存:src/polygateway/ports.pysrc/polygateway/transports/openai_compat.pysrc/polygateway/middleware/retry.py、六个测试文件。

T2 → 提交 2 feat: expose bare generation time and hedge flags in CallStats

  1. 先红:批次 B(test_types.py 三用例,字段不存在 → TypeError/AttributeError)+ C-F(各链路计时断言,字段恒 0 → 断言失败)。
  2. 按 §3.2 改 types.py;按 §3.3 改三条 _attemptretry.py::__call__ sink 接线;对冲计数本提交保持 0/False(T3 才登记)。
  3. 后绿:批次 BF 通过;pytest tests/unit tests/contracts -q 全绿不改既有断言。
  4. 暂存:src/polygateway/types.pymiddleware/retry.pyembedding.pyocr.py、五个测试文件。

T3 → 提交 3 feat: add opt-in cross-source hedged requests for chat

  1. 先红:批次 G(test_hedge.py,对冲未实现 → 挂起用例超时或 hedges==0 断言失败)+ H(test_config.py,键未识 → ValueError/None 断言失败)。
  2. 按 §3.5 改 admission.py → §3.6 改 config.py/client.py → §3.4 改 retry.py 编排。
  3. 后绿:批次 G/H 通过;默认关闭回归门:pytest tests/unit tests/contracts -q 全绿且不改一行既有断言(批次 I);make lint 通过。
  4. 暂存:src/polygateway/middleware/{retry,admission}.pysrc/polygateway/{config,client}.pytests/unit/test_hedge.pytests/unit/test_config.py

T4 → 提交 4 docs: document hedged requests and bare generation time

  1. CHANGELOG.md 未发布段:对冲三句强制措辞——默认关闭,开启即用配额换延迟(挂起窗口内 in-flight 翻倍);输家可能已被上游计费(est 保留只是闸内保守记账,非上游计量);对冲只对 chat 生效,embedding/OCR 仅获得 generation_ms 计时。另记 CallStats 三字段口径(generation_mstotal_latency_ms 差值 = 波动开销)与 hedge_cancelled 标签的遥测 join 用法。
  2. README.md:能力表加"长尾对冲(可选)"一行(三句措辞同上);配置键清单加两键与值域/守卫;CallStats 说明处加三字段。
  3. .env.example:LLM__CALL_DEADLINE_S 注释行后加 # LLM__HEDGE__AFTER_S=# LLM__HEDGE__MAX_EXTRA=1(缺省关闭,说明触发语义与成本含义)。
  4. research-wiki/findings/2026-09-10-24-hedged-requests-validation.md:红绿证据、命令与退出码、豁免索引(含 H5 的 v1 单路口径与竞速误贴残留)。
  5. wiki 注册本计划与 findings(add_entity/add_edge/rebuild_index);独立验证(全新上下文 verifier)与整分支审查在本提交前完成;版本 bump/发布不在本计划内

5. 测试矩阵 → 任务映射

设施复用核对(动手前必读):tests/unit/test_retry.py:120-127FakeSleep 只记录不推进时钟——退避推进须用例自带 async def sleep(s): clock.advance(s) 闭包;tests/unit/test_embedding.py:217 已有 _ClockAdvancingEmbedTransport(尝试内推进时钟),批次 D 直接复用;tests/unit/test_config.py:31-33 已有 loguru WARNING 捕获 fixture,warning 断言用它(caplog 抓不到 loguru);tests/unit/test_client.py:134-139 已有 InMemoryCache 命中回路,批次 F 复用;对冲编排用例(test_hedge.py)用真实 loop 钟(不注入 FakeClock),阈值 0.05s、断言容差 4–10×;取消窗口用 entered Event 范式(test_retry.py:79-89 既有),禁 sleep 撞窗口。

批次 断言(→ 任务) 落点(精确测试名)
A 端口加参 流式首 token 置位事件;非流式永不置位;None 不观测行为不变;漏传 → TypeError(钉住必填)(→T1) test_openai_compat.py::test_stream_sets_first_token_eventtest_non_stream_never_sets_first_token_eventtest_none_first_token_event_keeps_behaviortest_first_token_event_is_required_keyword
B 三字段 三字段默认值(0/0/False);仅旧三参数构造 CallStats(...) 不炸;record_generation 覆盖/累加语义;register_hedge 计数;snapshot 带出三字段(→T2) test_types.py::test_callstats_hedge_fields_defaulttest_callcontext_record_generation_overwrite_and_accumulatetest_callcontext_register_hedge_countstest_snapshot_includes_hedge_fields
C chat 计时 脚本 [Transient, ok]:退避推进时钟 5s、成功次 transport 推进 0.2s → generation_ms == 200total_latency_ms ≥ 5200(证明排除 backoff);无对冲时 hedges == 0/hedge_won is False(→T2) test_retry.py::test_generation_ms_excludes_backoff_and_admissiontest_generation_ms_zero_hedge_flags_without_hedging(配 _GenClockTransport 薄包装:委托 FakeTransport 并在返回前 clock.advance(delta))
C2 重问覆盖 结构化首轮坏 JSON(transport 推进 1s)、重问轮好 JSON(推进 0.2s)→ generation_ms == 200(最后一轮覆盖,非累加)(→T2) test_client.py::test_generation_ms_structured_last_round_wins(复用 _client(structured_strategy=...) 与 :129-132 范式;若 _client 未暴露 now 注入,按其既有模式补 keyword 参数——测试设施非公共面)
D embedding 计时 两批各推进 0.3s → generation_ms == 600(批次和)(→T2) test_embedding.py::test_generation_ms_sums_batch_transports(复用 _ClockAdvancingEmbedTransport)
E ocr 计时 单次 transport 推进 0.4s → generation_ms == 400(→T2) test_ocr_client.py::test_generation_ms_single_transport_call(同款薄包装,推进 test_ocr_client.py:344 那份本地 FakeClock——历史坑:不是 contracts 那份)
F 缓存命中 第二次同 key 调用 generation_ms == 0attempts == 0cache_hit is True(→T2) test_client.py::test_cache_hit_generation_ms_zero(复用 :134-139 回路)
G 对冲编排 见下表(→T3) tests/unit/test_hedge.py(新建)
H 配置守卫 两键 env 解析(3 段键不被当源字段、HEDGE 在保留段);非法值四路(env/直接构造/dataclasses.replace/client 直传)→ ValueError;after_s ≥ min(timeout_s) → ValueError;after_s ≥ min(ttft) → warning;单源 → warning;after_s ≥ call_deadline_s → ValueError;max_extra 0/"x" → ValueError、2/3 → warning 且生效 1(→T3) test_config.py::test_hedge_keys_from_env_skip_source_loadertest_hedge_after_s_domain_four_pathstest_hedge_guard_below_min_timeout_raisestest_hedge_guard_ttft_warnstest_hedge_guard_single_source_warnstest_hedge_guard_deadline_conflict_raisestest_hedge_max_extra_v1_cap;client 直传入口校验 test_client.py::test_client_hedge_params_entry_validation
I 默认关闭回归 不配对冲键时 tests/unit + tests/contracts 全绿,不改一行既有断言(→T3 门) 全套件

批次 G(test_hedge.py)用例全表——设施:两源 scope(s1/s2),event-driven 假 transport(每源一对 entered/release Event + 可脚本化"先置 first_token 再挂起"),真实 loop 钟,hedge_after_s=0.05:

测试名 断言(设计 §8 矩阵编号)
test_non_stream_triggers_hedge_and_fast_leg_wins s1 挂起、s2 即时成功:总时长 < 10× 阈值(①);hedges==1hedge_won is True;generation_ms < total_latency_ms 且 ≥ s2 实际 transport 耗时下界(⑩:不含触发前等待)
test_stream_triggers_only_when_first_token_absent 两例:首 token 未至 → 触发;假 transport 先 first_token_event.set() 再挂起 → 不触发,attempts==1(①)
test_hedge_goes_to_other_source 对冲请求落在 s2(transport.calls 断言);logical_call_id 两行一致(②⑤)
test_hedge_silent_when_no_candidate s2 permit 预占满 → 不对冲:hedges==0attempts==1、原请求放行后正常成功(③)
test_hedge_silent_when_single_source 单源 scope:运行期自然静默,行为与不配阈值逐字相同(②)
test_winner_settles_actual_loser_keeps_est memory limiter:赢家源 tpm_used == 真实 usage,输家源 tpm_used == est(S3 格);调用结束后两源 inflight == 0(④)
test_loser_row_labelled_hedge_cancelled 假 emitter:输家 attempt 行 error=="hedge_cancelled",赢家行无 error;两行 logical_call_id 相同;无 terminal_failure 行(④⑤)
test_loser_does_not_feed_breaker memory gate:挂起源 failure_count 不变、健康喂数无 ok=False;赢家照常 record_success(④,§3 关键判断)
test_attempts_two_and_no_task_leak call_stats.attempts == 2;返回后 asyncio.all_tasks() 无本调用残留任务(⑤)
test_external_cancel_cancels_both_legs 两路均挂起,entered 双置位后 task.cancel():CancelledError 上抛;两行 attempt 均 "cancelled"(标记只在赢家产生后置,外部取消无 hedge_cancelled);两 permit 释放(⑦)
test_deadline_cuts_hedged_tree client 级 call_deadline_s=0.2 + 两路挂起 → CallDeadlineExceeded;两 permit 释放(⑧)
test_primary_late_success_wins_back s1 挂 0.3s 后成功、s2 对冲路挂起:对冲已触发但原路先完成 → hedge_won is Falsegeneration_ms 为原路时长、s2 行 hedge_cancelled(⑩ 取快者)
test_both_fail_counts_budget_once 两路 Transient:max_attempts=2 时恰进第二轮(两败只计一次);最终 retry_exhausted 在第二轮两败后(H6)
test_both_429_refund_no_budget 两路 429:不耗预算(max_attempts=1 不抛 retry_exhausted),stall 账退还——小 stall_window_s 下终局 reason=="stalled" 而非 "retry_exhausted"
test_mixed_429_and_failure_counts_budget 一路 429 一路 Transient → 计一次预算、不退还 stall 账(§3.4 _combine_failures)

命令(全部 conda run -n PolyGateway,禁接管道):pytest tests/unit/test_openai_compat.py -qpytest tests/unit -qpytest tests/unit/test_hedge.py -qpytest tests/unit tests/contracts -qmake lint。真实 Redis/网关 slow 用例本计划不新增、不跑,由发布清单第 4 步按 diff 交集选子集(本 diff 触及 retry/限流结算路径,Redis 时间语义变体届时在交集内)。

6. 阻塞矩阵与交接

触发条件 处置
需要新增本计划外的公共键/端口方法/遥测列/异常类 停下上报(设计 §9 边界之外即未批准)
asyncio.current_task().cancelling() 在目标 Python 版本语义不符 3.12 语义同 136 探针已验证的取消计数;若实测不符,改用"取消标志位置于 _attempt_hedged 局部"方案并记入 findings,不得吞取消
两路同时成功的竞速在测试中无法确定性构造 用双 Event 栅栏(两 transport 都等同一放行事件)构造;仍不可得则记入 findings 豁免索引,不得删"原路优先"断言
批次 G 计时断言在 CI 机器抖动 只断言下界与相对比较(generation_ms < total_latency_ms、总时长 < 10× 阈值),不断言精确值;精确值断言只在注入钟批次(C–F)
想顺手让对冲路再触发梯次对冲 不做(H5:v1 单路;hedge 任务恒传 first_token_event=None)
想把输家记进熔断/健康分 不记(§3 关键判断:挂起 ≠ 源死亡);运维面靠 hedge_cancelled 遥测行统计
hedge_max_extra > 1 应 warning 还是 ValueError 存疑 本计划取 warning(§5 值域 [1,3] 与 H5 "v1 只允许 1" 的并存解);若人类审定应 ValueError,改 check_hedge_assembly 一处 + 批次 H 一条断言
发现 embedding/OCR 也想加对冲参数 不加(非目标 A,H7);登记为后续 issue

交接物:4 个提交、1 份 findings、CHANGELOG 未发布段。版本号 bump、tag、构建、上传 registry 与 wiki 同步不在本计划内,按 CLAUDE.md §4.4.1 另行执行。

7. 自审

检查 结论
路径/行号/签名是否可执行无 TBD 是——接入点均现读:ports.py:51-60openai_compat.py:426/552/573-575/646retry.py:244/248-256/270/291-300/327-334admission.py:171-213/224types.py:296-318/321-360config.py:57/190/201/396/405/695client.py:239/245-247/254-273/511embedding.py:373/386ocr.py:397、六个 fake transport 精确行号
是否复用而非重造 是——取消结算 S3 格、准入全链路、settle_and_releaseasyncio.wait 范式、ensure_call_deadline 值域校验、_ClockAdvancingEmbedTransport/loguru 捕获 fixture/InMemoryCache 回路全部复用;新增仅 1 测试文件 + 2 配置键 + 3 字段
先失败后通过证据点 是——T1 批次 A(TypeError)、T2 批次 BF(字段缺失/恒 0)、T3 批次 G/H(未实现/键未识)均先红
取消与降级铁律 取消穿透路径显式收口不吞没;准入失败静默(设计 §4.3 批准);后端不可用照常冒泡;无 shield/后台任务
反 gold-plating 四分类/熔断语义/429 分账/Lua/deadline.py/遥测列/embedding-OCR 对冲/分位数/同源/per-call 参数一律不碰;hedge_max_extra 不下传 RetryMW(v1 无消费者)
跨任务签名一致 first_token_event(T1 端口 → T3 接线)、generation_sink/record_generation(T2 定义,T3 编排消费)、check_hedge_assembly(config 定义,client 消费)三处接缝均在 §3 写出实际代码
残余诚实标注 输家 est 保留 ≠ 上游真实计费计量;非流式误对冲慢生物理不可分;竞速误贴标签可接受(设计 §4.5);hedge_max_extra v1 生效口径取 warning(§6 阻塞矩阵已列复核点)