feat: add opt-in mid-stream stall hedging for chat
HEDGE__STALL_AFTER_S triggers a cross-source hedge when the primary leg stalls mid-stream (independent of HEDGE__AFTER_S); two-phase hedge window with event recheck discipline; watchdog-killed primary converges on the surviving hedge leg. STREAM__STALL_THRESHOLD_MS (1000ms default) adjustable via env/settings/constructor. Assembly guards: ValueError on never-opening windows, embedding/OCR scope warning (B4). Batch D 14 tests red-to-green, 1688 unit+contracts.
This commit is contained in:
@@ -239,6 +239,8 @@ class GatewayClient:
|
||||
call_deadline_s: float | None = None,
|
||||
hedge_after_s: float | None = None,
|
||||
hedge_max_extra: int = 1,
|
||||
hedge_stall_after_s: float | None = None,
|
||||
stall_threshold_ms: int | None = None,
|
||||
now: Any = time.monotonic,
|
||||
sleep: Any = asyncio.sleep,
|
||||
rng: Any = random.random,
|
||||
@@ -247,15 +249,25 @@ class GatewayClient:
|
||||
self._call_deadline_s = ensure_call_deadline(
|
||||
call_deadline_s, "GatewayClient(call_deadline_s=...)"
|
||||
)
|
||||
# 对冲守卫与 GatewaySettings 共用同一份(issue #24 H4): 直接构造这条路
|
||||
# 不经过 settings,值域/交叉守卫若只挂在 settings 上就会被它绕过
|
||||
self._hedge_after_s = check_hedge_assembly(
|
||||
# 对冲守卫与 GatewaySettings 共用同一份(issue #24 H4 / 1.3.8 #28): 直接构造这条路
|
||||
# 不经过 settings,值域/交叉守卫若只挂在 settings 上就会被它绕过。
|
||||
# 返回契约(1.3.8 审查 B2): tuple(归一化 after_s, 归一化 stall_after_s)
|
||||
self._hedge_after_s, self._hedge_stall_after_s = check_hedge_assembly(
|
||||
hedge_after_s=hedge_after_s,
|
||||
hedge_max_extra=hedge_max_extra,
|
||||
sources=sources,
|
||||
call_deadline_s=self._call_deadline_s,
|
||||
origin="GatewayClient(hedge_after_s=...)",
|
||||
stall_after_s=hedge_stall_after_s,
|
||||
stall_origin="GatewayClient(hedge_stall_after_s=...)",
|
||||
)
|
||||
# 沉默计量阈值分发(1.3.8 设计 §6): None = 保留各 SourceConfig 自带值
|
||||
# (不 stomp 直构造调用方的 per-source 设置);非 None 经 replace 写进每个源
|
||||
# (replace 触发 SourceConfig.__post_init__ 重跑,值域校验复用同一处)
|
||||
if stall_threshold_ms is not None:
|
||||
sources = [
|
||||
dataclasses.replace(s, stall_threshold_ms=stall_threshold_ms) for s in sources
|
||||
]
|
||||
emitter = (
|
||||
TelemetryEmitter(telemetry, scope=scope, pricing=pricing, text_cap=text_cap)
|
||||
if telemetry is not None
|
||||
@@ -280,6 +292,8 @@ class GatewayClient:
|
||||
emitter=emitter,
|
||||
# max_extra 不下传(issue #24 H5): v1 编排固定单路对冲,无消费者
|
||||
hedge_after_s=self._hedge_after_s,
|
||||
# 中段沉默对冲阈值(1.3.8 #28): 独立于 hedge_after_s 生效,守卫归一化值
|
||||
stall_after_s=self._hedge_stall_after_s,
|
||||
now=now,
|
||||
sleep=sleep,
|
||||
rng=rng,
|
||||
@@ -524,6 +538,9 @@ class GatewayClient:
|
||||
call_deadline_s=settings.call_deadline_s,
|
||||
hedge_after_s=settings.hedge_after_s,
|
||||
hedge_max_extra=settings.hedge_max_extra,
|
||||
hedge_stall_after_s=settings.hedge_stall_after_s,
|
||||
# settings 值为权威,恒分发(1.3.8 §3.5): 含缺省 1000 在内
|
||||
stall_threshold_ms=settings.stall_threshold_ms,
|
||||
)
|
||||
_mark_owned_components(client, limiter=limiter, breaker=breaker, telemetry=telemetry)
|
||||
client._owns_cache = cache is None # 缓存后端可以是 None(backend=none),helper 会跳过
|
||||
|
||||
Reference in New Issue
Block a user