feat: add opt-in cross-source hedged requests for chat

This commit is contained in:
2026-09-10 14:00:50 -04:00
parent 463eca380d
commit adc069447a
7 changed files with 1018 additions and 13 deletions
+162
View File
@@ -1096,3 +1096,165 @@ class TestCallDeadlineConfig:
with pytest.raises(ValueError, match=r"GatewayClient\(call_deadline_s"):
_client(call_deadline_s=0)
class TestHedgeConfig:
"""`{SCOPE}__HEDGE__AFTER_S`/`{SCOPE}__HEDGE__MAX_EXTRA` 两键与装配守卫(issue #24 H4)。
对冲默认关闭: 键未设 = None/1,行为逐字等于 1.3.6。守卫四路覆盖
(env/直接构造/dataclasses.replace/client 直传),单一定义点是
`config.check_hedge_assembly`。
"""
def _two_source_env(self, **overrides):
"""双源 env(同 provider 避免注册表依赖): 隔离单源 warning 的干扰。"""
return _env(
**{
"LLM__QWEN__2__BASE_URL": "https://gw-b.example/v1",
"LLM__QWEN__2__API_KEY": "sk-b",
"LLM__QWEN__2__MODEL": "qwen-plus",
"LLM__QWEN__2__TIMEOUT_S": "90",
**overrides,
}
)
def test_hedge_keys_from_env_skip_source_loader(self):
"""两键为 3 段键,天然不被 `_load_sources` 当源字段;`HEDGE` 进保留段防 4 段撞名。"""
env = self._two_source_env(**{"LLM__HEDGE__AFTER_S": "8", "LLM__HEDGE__MAX_EXTRA": "2"})
with _captured_warnings(): # max_extra>1 的 v1 单路 warning, 本用例不断言它
s = GatewaySettings.from_env("LLM", env=env)
assert s.hedge_after_s == 8.0
assert s.hedge_max_extra == 2
assert {src.name for src in s.sources} == {"qwen_1", "qwen_2"} # HEDGE 键未造源
# `HEDGE` 在保留段: `LLM__HEDGE__1__*` 四段键不得造出一个名为 hedge_1 的源
env_collision = _env(
**{
"LLM__HEDGE__1__BASE_URL": "https://gw-c.example/v1",
"LLM__HEDGE__1__API_KEY": "sk-c",
"LLM__HEDGE__1__MODEL": "m-c",
"LLM__HEDGE__1__TIMEOUT_S": "60",
}
)
s2 = GatewaySettings.from_env("LLM", env=env_collision)
assert [src.name for src in s2.sources] == ["qwen_1"]
def test_hedge_keys_unset_mean_disabled(self):
"""默认关闭: 两键未设 = None/1,且不产生任何 warning。"""
with _captured_warnings() as warnings:
s = GatewaySettings.from_env("LLM", env=_env())
assert s.hedge_after_s is None and s.hedge_max_extra == 1
assert not warnings
def test_hedge_after_s_domain_four_paths(self):
"""非法值四条装配路全部当场 ValueError(消息须定位得到是哪个键/参数)。"""
from tests.unit.test_client import _client
# 路 1: env(origin 是实际命中的键名)
with pytest.raises(ValueError, match="LLM__HEDGE__AFTER_S"):
GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__AFTER_S": "0"}))
with pytest.raises(ValueError, match="LLM__HEDGE__AFTER_S"):
GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__AFTER_S": "abc"}))
base = GatewaySettings.from_env("LLM", env=_env())
# 路 2: 直接构造
fields = {f.name: getattr(base, f.name) for f in dataclasses.fields(base)}
with pytest.raises(ValueError, match="hedge_after_s"):
GatewaySettings(**{**fields, "hedge_after_s": float("nan")})
# 路 3: dataclasses.replace
with pytest.raises(ValueError, match="hedge_after_s"):
dataclasses.replace(base, hedge_after_s=-1)
# 路 4: client 直传(不经 settings 那道守卫)
with pytest.raises(ValueError, match=r"GatewayClient\(hedge_after_s"):
_client(hedge_after_s=0)
def test_hedge_guard_below_min_timeout_raises(self):
"""阈值 ≥ 最小源 timeout_s = 对冲永不可能触发,装配期炸掉(ValueError)。"""
env = self._two_source_env(**{"LLM__HEDGE__AFTER_S": "90"}) # min(timeout)=90
with pytest.raises(ValueError, match="timeout_s"):
GatewaySettings.from_env("LLM", env=env)
# 边界内侧合法(89 < 90)
s = GatewaySettings.from_env(
"LLM", env=self._two_source_env(**{"LLM__HEDGE__AFTER_S": "89"})
)
assert s.hedge_after_s == 89.0
def test_hedge_guard_ttft_warns(self):
"""阈值 ≥ 最小已设 ttft_timeout_s: 流式被看门狗先切,装配期 warning 而非 ValueError。"""
env = self._two_source_env(
**{
"LLM__QWEN__1__TTFT_TIMEOUT_S": "30",
"LLM__QWEN__1__INTER_TOKEN_TIMEOUT_S": "15",
"LLM__HEDGE__AFTER_S": "35", # ≥ ttft 30, < timeout 90
}
)
with _captured_warnings() as warnings:
s = GatewaySettings.from_env("LLM", env=env)
assert s.hedge_after_s == 35.0 # warning 不是拒绝: 非流式仍有效
assert any("ttft_timeout_s" in m for m in warnings)
# 阈值低于看门狗时不告警
with _captured_warnings() as warnings2:
GatewaySettings.from_env(
"LLM",
env=self._two_source_env(
**{
"LLM__QWEN__1__TTFT_TIMEOUT_S": "30",
"LLM__QWEN__1__INTER_TOKEN_TIMEOUT_S": "15",
"LLM__HEDGE__AFTER_S": "25",
}
),
)
assert not warnings2
def test_hedge_guard_single_source_warns(self):
"""单源 scope 设阈值: 装配期 warning 放行,运行期拿不到候选自然静默。"""
with _captured_warnings() as warnings:
s = GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__AFTER_S": "8"}))
assert s.hedge_after_s == 8.0
assert any("单源" in m for m in warnings)
def test_hedge_guard_deadline_conflict_raises(self):
"""阈值 ≥ call_deadline_s: 期限先于对冲触发,对冲形同虚设 → ValueError(§6)。"""
env = self._two_source_env(**{"LLM__HEDGE__AFTER_S": "35", "LLM__CALL_DEADLINE_S": "30"})
with pytest.raises(ValueError, match="call_deadline_s"):
GatewaySettings.from_env("LLM", env=env)
# 边界值(恰好相等)同样拒绝
with pytest.raises(ValueError, match="call_deadline_s"):
GatewaySettings.from_env(
"LLM",
env=self._two_source_env(
**{"LLM__HEDGE__AFTER_S": "30", "LLM__CALL_DEADLINE_S": "30"}
),
)
# 阈值 < 期限是合法组合
s = GatewaySettings.from_env(
"LLM",
env=self._two_source_env(**{"LLM__HEDGE__AFTER_S": "29", "LLM__CALL_DEADLINE_S": "30"}),
)
assert s.hedge_after_s == 29.0 and s.call_deadline_s == 30.0
def test_hedge_max_extra_v1_cap(self):
"""max_extra 值域 [1,3] 的四路校验;>1 已接受但 warning 声明 v1 仅单路生效(H5)。"""
# 域外值无条件拒绝(即使对冲未启用: 非法值没有"惰性"豁免)
with pytest.raises(ValueError, match="hedge_max_extra"):
GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__MAX_EXTRA": "0"}))
with pytest.raises(ValueError, match="LLM__HEDGE__MAX_EXTRA"):
GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__MAX_EXTRA": "x"}))
with pytest.raises(ValueError, match="hedge_max_extra"):
GatewaySettings.from_env("LLM", env=_env(**{"LLM__HEDGE__MAX_EXTRA": "4"}))
base = GatewaySettings.from_env("LLM", env=_env())
with pytest.raises(ValueError, match="hedge_max_extra"):
dataclasses.replace(base, hedge_max_extra=0)
# 2/3 接受 + warning: v1 运行期恒单路(对冲任务不再携带首 token 观测,
# 行为面由 test_hedge.py 的 ft_events 断言钉住),梯次追加为 H5 预留
env = self._two_source_env(**{"LLM__HEDGE__AFTER_S": "8", "LLM__HEDGE__MAX_EXTRA": "2"})
with _captured_warnings() as warnings:
s = GatewaySettings.from_env("LLM", env=env)
assert s.hedge_max_extra == 2
assert any("单路" in m for m in warnings)
def test_from_settings_propagates_hedge_to_client(self):
"""from_settings 透传: RetryMW 拿到归一化阈值;max_extra 不下传(v1 无消费者)。"""
env = self._two_source_env(**{"LLM__HEDGE__AFTER_S": "8"})
s = GatewaySettings.from_env("LLM", env=env)
client = GatewayClient.from_settings(s)
assert client._hedge_after_s == 8.0
assert client._terminal._hedge_after_s == 8.0