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:
@@ -28,6 +28,31 @@ _KEY_PREFIX = "pgw:cache:"
|
||||
_RESPONSE_FIELDS = {f.name for f in dataclasses.fields(LLMResponse)}
|
||||
|
||||
|
||||
def _coerce_observation(raw: Any) -> ThinkingObservation:
|
||||
"""缓存里的三态取值 → 枚举;域外取值降级为 `UNKNOWN`,**不作废整条缓存**。
|
||||
|
||||
方向选择的理由: `_rehydrate` 对 JSON 里的**新字段**已经是宽容的(先按
|
||||
`_RESPONSE_FIELDS` 过滤),对同一字段的**新取值**却不该是致命的。真实场景是
|
||||
多个项目共用一个 Redis,先升级的那个写入了本版没有的取值,未升级的项目若把
|
||||
这些条目判成未命中,就会每次真打网关、随后覆写回旧值,两个版本互相打对方的
|
||||
缓存(表现是命中率莫名腰斩,而通用的"重建失败"文案给不出任何线索)。一个纯
|
||||
可观测性字段不该有能力废掉内容完好的缓存响应——"整条作废"留给真正破坏内容
|
||||
完整性的失败(JSON 坏了、结构化重建不过)。
|
||||
|
||||
降级到 `UNKNOWN` 而不是别的态: 它的语义恰好就是"本次判不出来",对一个本库
|
||||
读不懂的取值,这是唯一诚实的说法。
|
||||
"""
|
||||
try:
|
||||
return ThinkingObservation(raw)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"缓存条目的 thinking_observation 取值 {!r} 不在本版取值域内(多半由更新版本的"
|
||||
"进程写入),已降级为 UNKNOWN;响应内容照常复活——可观测性字段不作废缓存",
|
||||
raw,
|
||||
)
|
||||
return ThinkingObservation.UNKNOWN
|
||||
|
||||
|
||||
def digest_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""多模态 content part 先各自 sha256 摘要再参与序列化;文本原文参与。
|
||||
|
||||
@@ -134,10 +159,9 @@ class CacheMW:
|
||||
structured_data = self._rebuild_structured(fields.get("content", ""), request)
|
||||
# JSON 里存的是 StrEnum 的字符串值,不转就复活成裸 str,与字段注解分叉
|
||||
# (下游 `is ThinkingObservation.OBSERVED` 会在命中路径上静默为 False);
|
||||
# 键缺失即升级前写入的旧条目,交给 dataclass 默认值。域外值抛
|
||||
# ValueError,由下方 except 吞成"按未命中回源"——降级方向正确。
|
||||
# 键缺失即升级前写入的旧条目,交给 dataclass 默认值
|
||||
if "thinking_observation" in fields:
|
||||
fields["thinking_observation"] = ThinkingObservation(fields["thinking_observation"])
|
||||
fields["thinking_observation"] = _coerce_observation(fields["thinking_observation"])
|
||||
fields.update(
|
||||
cache_hit=True,
|
||||
latency_ms=0,
|
||||
|
||||
Reference in New Issue
Block a user