fix: close the failure modes review found in the new code

Three of them were the same shape as the bug this branch exists to fix:
something goes wrong, the library swallows it, and the caller is left
with a number that means the opposite of what happened.

The throttle key had no source in it. Five sources on one model is the
normal case here, so the first one to break would warn once and silence
the other four for the life of the process, and the message never said
which gateway to look at.

An unknown verdict in a cached entry threw away the whole response. The
rehydrator tolerates unknown fields but not unknown values of a known
field, so two library versions sharing a Redis would each invalidate
the other's entries: halved hit rate, and the only log line says the
cache rebuild failed. A purely observational field should not be able
to void a response whose content is intact.

Normalising for telemetry now degrades instead of raising, both for a
bare string and for a value outside the domain. Either one used to
reach the same except and cost the whole row, which is exactly how
1.3.0 lost nineteen calls without anyone noticing.
This commit is contained in:
2026-08-26 02:37:24 -04:00
parent c0b544d233
commit 1307a02b92
9 changed files with 270 additions and 30 deletions
+36 -5
View File
@@ -547,10 +547,11 @@ class TestThinkingObservationVerdict:
class TestThinkingReconciliation:
"""对账告警按 (model, direction) 节流(设计 §5)。
"""对账告警按 (source, model, direction) 节流(设计 §5)。
节流键必须含方向: 同一模型的开、关两档是两个独立的矛盾,合并键会让先出现
的那一档把另一档永久静音。
键的三段缺一不可,理由同源: 合并任意一段,都会让先出现的那一组把另一组
永久静音——同一模型的开/关两档是两个独立的矛盾,同一模型的两个源背后是
两个独立的账号/网关。
"""
def _handler(self, request):
@@ -563,9 +564,9 @@ class TestThinkingReconciliation:
# 开启档却零信号 → UNKNOWN,无法确认是否生效(M3 实测形态)
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
def _minimax(self, enable_thinking):
def _minimax(self, enable_thinking, name="mm"):
return _source(
name="mm", provider="minimax", model="MiniMax-M3", enable_thinking=enable_thinking
name=name, provider="minimax", model="MiniMax-M3", enable_thinking=enable_thinking
)
async def test_same_model_and_direction_warns_only_once(self):
@@ -581,6 +582,36 @@ class TestThinkingReconciliation:
hits = [m for m in messages if "MiniMax-M3" in m]
assert len(hits) == 1, f"同一 (model, direction) 应只告警一次,实得 {len(hits)}"
async def test_each_source_gets_its_own_warning(self):
"""多源多账号是本库的核心场景: 同一 model 跨 N 个源不得只喊第一个。
节流键漏掉源标识时,5 个共用同一模型的源里第一个出问题的喊完一次,其余
四个**永久静音**——而每个源背后是独立的账号/网关,它们的行为互不代表。
"""
transport = _transport_for(self._handler)
messages: list[str] = []
sink_id = logger.add(messages.append, level="WARNING")
try:
await _complete(transport, self._minimax(False, name="gw-a"))
await _complete(transport, self._minimax(False, name="gw-b"))
finally:
logger.remove(sink_id)
hits = [m for m in messages if "MiniMax-M3" in m]
assert len(hits) == 2, f"两个源各应告警一次,实得 {len(hits)}"
async def test_the_warning_names_the_source(self):
"""拿到告警的人得知道该查哪个网关: 只报模型名定位不到源。"""
transport = _transport_for(self._handler)
messages: list[str] = []
sink_id = logger.add(messages.append, level="WARNING")
try:
await _complete(transport, self._minimax(False, name="gw-a"))
finally:
logger.remove(sink_id)
hits = [m for m in messages if "MiniMax-M3" in m]
assert len(hits) == 1
assert "gw-a" in hits[0], f"告警未点名出问题的源: {hits[0]}"
async def test_switching_direction_earns_a_second_warning(self):
transport = _transport_for(self._handler)
messages: list[str] = []