- normalizer.canonical_hash 增加可选 route_sig 段:键 = bucket|doc_version|sig|sha256 (旧三段格式向后兼容,旧条目随 TTL 自然淘汰) - ProxyConfig 新增 semcache.route_sig_scope:capabilities(默认,vision/tools 需求 签名)/ model(按模型隔离)/ none(旧行为);非法值回落 capabilities - semcache:签名升级为条目属性并分区 L2 语义扫描(仅键分桶不够——语义层仍会 跨签名命中);签名从缓存键第四段解析,重启重建零 schema 变更; 晋升别名键携带签名段;route_sig=None 的旧调用零过滤完全兼容 - routes:lookup/put 共用同一 norm_hash(消除 put 侧重复哈希),签名贯穿两层 - 新增 tests/test_route_sig.py 6 项(键格式/键空间分割/scope 三态/两层隔离/ 重建存活/旧调用兼容)
88 lines
4.3 KiB
Python
88 lines
4.3 KiB
Python
"""语义缓存路由签名分桶(T-X10,采纳 cortiq 路由签名设计)。"""
|
|
from gateway.proxy.config import build_proxy_config
|
|
from gateway.proxy.normalizer import canonical_hash
|
|
from gateway.proxy.routes import _route_sig
|
|
|
|
_BODY = {"model": "m1", "messages": [{"role": "user", "content": "什么是递归"}]}
|
|
|
|
|
|
def test_canonical_hash_legacy_format_without_sig():
|
|
"""不带签名保持旧三段格式(旧调用/旧条目兼容)。"""
|
|
h = canonical_hash("default", 1, _BODY)
|
|
assert h.count("|") == 2
|
|
h2 = canonical_hash("default", 1, _BODY, "")
|
|
assert h == h2
|
|
|
|
|
|
def test_canonical_hash_with_sig_divides_keyspace():
|
|
"""签名不同 -> 键不同:vision 请求与纯文本请求不再共用缓存条目。"""
|
|
plain = canonical_hash("default", 1, _BODY)
|
|
vision = canonical_hash("default", 1, _BODY, "v1t0")
|
|
tools = canonical_hash("default", 1, _BODY, "v0t1")
|
|
assert len({plain, vision, tools}) == 3
|
|
|
|
|
|
def test_route_sig_scopes():
|
|
"""三种 scope 的签名形态:capabilities 默认 / model 精确 / none 旧格式。"""
|
|
cfg = build_proxy_config({"proxy": {"enabled": True}})
|
|
needs = {"vision": True, "tools": False, "min_context_tokens": 10}
|
|
assert _route_sig(needs, "m1", cfg) == "v1t0"
|
|
|
|
cfg_model = build_proxy_config({"proxy": {"semcache": {"route_sig_scope": "model"}}})
|
|
assert _route_sig(needs, "m1", cfg_model) == "m:m1"
|
|
|
|
cfg_none = build_proxy_config({"proxy": {"semcache": {"route_sig_scope": "none"}}})
|
|
assert _route_sig(needs, "m1", cfg_none) == ""
|
|
|
|
# 非法 scope 回落 capabilities
|
|
cfg_bad = build_proxy_config({"proxy": {"semcache": {"route_sig_scope": "bogus"}}})
|
|
assert _route_sig(needs, "m1", cfg_bad) == "v1t0"
|
|
|
|
|
|
def test_sig_isolation_end_to_end_via_semcache(tmp_path):
|
|
"""经 SemanticCache 验证:签名分区 L1 精确层与 L2 语义扫描两层。"""
|
|
from gateway.proxy.ledger import Ledger
|
|
from gateway.proxy.semcache import SemanticCache
|
|
|
|
led = Ledger.init_db(tmp_path / "l.sqlite3")
|
|
cache = SemanticCache(led, max_entries=100, sim_threshold=0.5,
|
|
promote_frequency=5)
|
|
k_text = canonical_hash("default", 1, _BODY, "v0t0")
|
|
k_vision = canonical_hash("default", 1, _BODY, "v1t0")
|
|
cache.put(k_text, "什么是递归", "纯文本答案", "m1", doc_version=1,
|
|
route_sig="v0t0")
|
|
# 相同问题、vision 签名:L1 键不同必不中;L2 语义扫描按签名分区也不中
|
|
assert cache.lookup(k_vision, "什么是递归", doc_version=1,
|
|
route_sig="v1t0") is None
|
|
# 同签名:L1 精确命中
|
|
hit = cache.lookup(k_text, "什么是递归", doc_version=1, route_sig="v0t0")
|
|
assert hit is not None and hit["level"] == "exact"
|
|
|
|
|
|
def test_sig_partition_survives_rebuild(tmp_path):
|
|
"""重启重建(semcache 表无签名列):签名从缓存键解析恢复,分区仍有效。"""
|
|
from gateway.proxy.ledger import Ledger
|
|
from gateway.proxy.semcache import SemanticCache
|
|
|
|
led = Ledger.init_db(tmp_path / "l.sqlite3")
|
|
c1 = SemanticCache(led, max_entries=100, sim_threshold=0.5, promote_frequency=5)
|
|
k = canonical_hash("default", 1, _BODY, "v1t0")
|
|
c1.put(k, "什么是递归", "多模态答案", "m1", doc_version=1, route_sig="v1t0")
|
|
c2 = SemanticCache(led, max_entries=100, sim_threshold=0.5, promote_frequency=5)
|
|
assert c2.lookup(k, "什么是递归", doc_version=1, route_sig="v1t0") is not None
|
|
assert c2.lookup(canonical_hash("default", 1, _BODY, "v0t0"),
|
|
"什么是递归", doc_version=1, route_sig="v0t0") is None
|
|
|
|
|
|
def test_legacy_calls_without_sig_unchanged(tmp_path):
|
|
"""不传签名(route_sig=None)的旧调用零过滤:既有测试行为不变。"""
|
|
from gateway.proxy.ledger import Ledger
|
|
from gateway.proxy.semcache import SemanticCache
|
|
|
|
led = Ledger.init_db(tmp_path / "l.sqlite3")
|
|
cache = SemanticCache(led, max_entries=100, sim_threshold=0.5, promote_frequency=5)
|
|
cache.put("k1", "请解释一下递归函数的概念", "答案", "m1", doc_version=1)
|
|
assert cache.lookup("k1", "请解释一下递归函数的概念", doc_version=1) is not None
|
|
# 语义路径同样不受影响(近似文本跨键命中)
|
|
assert cache.lookup("k2", "请解释一下递归函数的概念呢", doc_version=1) is not None
|