fix: address independent verification findings

Classify empty completions as transient per human ruling (fixes flaky
real-gateway smoke and prevents caching empty responses), rename the
factory injection parameter gate to breaker per the frozen design,
rewrite the probe-entry cleanup without except BaseException, declare
python-dotenv explicitly, add a mid-backoff cancellation test, and
record all implementation errata in the design and architecture docs.
This commit is contained in:
2026-07-20 22:01:26 -04:00
parent 0f4ae5ee8b
commit 0b8460b6d5
12 changed files with 101 additions and 16 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ def _full_client(handler, *, clock=None, telemetry=None, cache=None):
global_limits=GlobalLimits(0, 0, 0),
now=clock,
),
gate=InMemoryGate(config=_BREAKER, now=clock),
breaker=InMemoryGate(config=_BREAKER, now=clock),
transport=OpenAICompatTransport(
client_factory=lambda s: httpx.AsyncClient(transport=httpx.MockTransport(handler))
),
+1 -1
View File
@@ -155,7 +155,7 @@ class TestClientWithRealRedis:
limiter=InMemoryLimiter(
scope="llm", sources={src.name: src}, global_limits=GlobalLimits(0, 0, 0)
),
gate=InMemoryGate(config=BreakerConfig(5, 60.0, 120.0)),
breaker=InMemoryGate(config=BreakerConfig(5, 60.0, 120.0)),
transport=OpenAICompatTransport(
client_factory=lambda s: httpx.AsyncClient(
transport=httpx.MockTransport(lambda req: _sse_response())
+1 -1
View File
@@ -81,7 +81,7 @@ def _client(sources=None, handler=None, *, limiter=None, quota_full="wait", **ov
sources={s.name: s for s in sources},
global_limits=GlobalLimits(0, 0, 0),
),
"gate": InMemoryGate(config=BreakerConfig(5, 60.0, 120.0)),
"breaker": InMemoryGate(config=BreakerConfig(5, 60.0, 120.0)),
"transport": transport,
"retry": RetryPolicy(3, 2.0, 30.0),
"backpressure": BackpressurePolicy(300.0, 0.01),
+27
View File
@@ -161,6 +161,33 @@ class TestMissingDoneSemantics:
await _complete(_transport_for(handler), _source(missing_done="salvage"))
class TestEmptyCompletion:
"""空补全 → TransientError(2026-07-20 人类裁决;MiniMax 间歇形态,绝不缓存)。"""
async def test_stream_zero_content_with_done_is_transient(self):
def handler(request):
return _sse_stream(_chunk(reasoning="only thinking"), _chunk(usage=_USAGE))
with pytest.raises(TransientError, match="empty_completion"):
await _complete(_transport_for(handler), _source())
async def test_non_stream_empty_content_is_transient(self):
def handler(request):
return httpx.Response(
200, json={"choices": [{"message": {"content": ""}}], "usage": _USAGE}
)
with pytest.raises(TransientError, match="empty_completion"):
await _complete(_transport_for(handler), _source(), stream=False)
async def test_think_only_content_after_strip_is_transient(self):
def handler(request):
return _sse_stream(_chunk(content="<think>hmm</think>"), _chunk(usage=_USAGE))
with pytest.raises(TransientError, match="empty_completion"):
await _complete(_transport_for(handler), _source())
class TestNonStreamFastPath:
async def test_non_stream_parses_message(self):
def handler(request):
+33
View File
@@ -298,6 +298,39 @@ class TestCancellation:
await task
assert (await limiter.source_stats("a")).inflight == 0 # finally 释放
async def test_cancel_mid_backoff_propagates_with_no_held_permit(self):
"""退避 sleep 中取消: CancelledError 穿透,且 permit 早已在 finally 释放。"""
clock = FakeClock()
src = _src("a", max_concurrency=1)
limiter = InMemoryLimiter(
scope="llm",
sources={"a": src},
global_limits=_NO_GLOBAL,
lease_ttl_s=100.0,
now=clock,
)
mw = RetryMW(
scope="llm",
sources=[src],
selector=RoundRobinSelector(),
limiter=limiter,
gate=InMemoryGate(config=_BREAKER, now=clock),
transport=FakeTransport([TransientError("x"), _ok()]),
retry=RetryPolicy(max_attempts=3, backoff_base_s=30.0, backoff_max_s=60.0),
backpressure=BackpressurePolicy(300.0, 0.01),
cooldown_memo=SourceCooldownMemo(now=clock),
emitter=None,
now=clock,
sleep=asyncio.sleep,
rng=lambda: 0.5,
)
task = asyncio.ensure_future(mw(_REQ))
await asyncio.sleep(0.05) # 第一次失败后进入 30s 真实退避
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert (await limiter.source_stats("a")).inflight == 0 # 退避期不占并发槽
async def test_cancel_probe_releases_probe_lease(self):
clock = FakeClock()
mw, _, gate, _, _, _ = _harness(