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
@@ -252,6 +252,7 @@ class OpenAICompatTransport:
(content_parts if is_content else thinking_parts).append(text)
salvaged = self._check_done(sink, content_parts, thinking_parts, source)
content, thinking = self._finalize_text(content_parts, thinking_parts, profile)
self._reject_empty_completion(content, source)
prompt, completion, usage_source = _resolve_usage(sink.get("usage") or {}, source)
if salvaged:
usage_source = "estimated" # 打捞路径强制 estimated(设计 §6)
@@ -283,6 +284,20 @@ class OpenAICompatTransport:
raise TransientError(f"{source.name} SSE missing_done: 截断且无 [DONE]", **ctx)
return True
def _reject_empty_completion(self, content: str, source: SourceConfig) -> None:
"""空补全 → 瞬时错误(2026-07-20 人类裁决,M1 验证发现)。
服务 200 且流程完整([DONE]/usage 正常)但 content 为空——MiniMax 等
网关的间歇异常形态。视为服务抖动: 退避重试/换源,**绝不缓存空响应**;
承 CHS "VLM 零 content"归瞬时的先例(invokers.py:309)。
"""
if not content.strip():
raise TransientError(
f"{source.name} 空补全(empty_completion): 流程完整但零内容",
source_name=source.name,
operation="chat",
)
def _finalize_text(
self, content_parts: list[str], thinking_parts: list[str], profile: ProviderProfile
) -> tuple[str, str]:
@@ -321,6 +336,7 @@ class OpenAICompatTransport:
content, thinking = self._finalize_text(
[message.get("content") or ""], [message.get("reasoning_content") or ""], profile
)
self._reject_empty_completion(content, source)
prompt, completion, usage_source = _resolve_usage(body.get("usage") or {}, source)
return TransportResult(
content=content,