feat(proxy): T-P6 缓存分支接线(routes 主时序,M2 完整闭环)

- _run_chat 开头缓存分支:cacheable(stop+单轮)-> canonical_hash ->
  L1/L2 查 -> 命中:try_hold->立即 settle(cost=0, charged=售价口径,
  status=cached, gateway_cached=1) -> SSE 合成回放或 JSON(X-Cache: HIT)
- 未命中流程收尾写缓存(_json_response 尾部,stop 且单轮);
  缓存层任何故障降级直连上游(不影响可用性)
- 缓存命中计费 _cached_charge(成本 0,入按字符估收售价——全毛利杠杆 L0)
- _get_semcache 按 db_path 分实例(测试隔离)+ reset_semcache_instances
- 测试 +1(端到端:首问 miss 打上游/二问 HIT 上游仅 1 次/账目 cached),
  全量 423 passed
This commit is contained in:
tzt
2026-09-05 15:56:17 +08:00
parent e41471c39c
commit c54ad23c88
3 changed files with 145 additions and 6 deletions
+35 -1
View File
@@ -100,7 +100,10 @@ def _auth(key):
return {"Authorization": f"Bearer {key['key']}"}
BODY = {"model": "deepseek-chat", "messages": [{"role": "user", "content": "问个问题"}]}
BODY = {"model": "deepseek-chat",
"messages": [{"role": "user",
"content": "请详细介绍快速排序算法的原理、复杂度与实现要点,"
"并给出 Python 示例代码与适用场景分析。"}]}
def test_chat_non_stream_end_to_end(tmp_path):
@@ -288,3 +291,34 @@ def test_openai_protocol_compliance_via_httpx(tmp_path):
pass
mp.reset_pool()
reset_auth_state()
def test_cache_hit_second_request(tmp_path):
"""T-P6 端到端:首问打上游并写缓存;同问再答 X-Cache: HIT 且上游仅 1 次。"""
from gateway.proxy.routes import reset_semcache_instances
reset_semcache_instances()
tc, ledger, key, calls, (upmod, orig, hclient) = _make_app(tmp_path)
try:
r1 = tc.post("/proxy/v1/chat/completions", json=BODY, headers=_auth(key))
assert r1.status_code == 200
assert r1.headers.get("x-cache") is None # 首问未命中
assert calls["n"] == 1
r2 = tc.post("/proxy/v1/chat/completions", json=BODY, headers=_auth(key))
assert r2.status_code == 200
assert r2.headers.get("x-cache") == "HIT" # 缓存命中
assert calls["n"] == 1 # 上游仍只 1 次
assert r2.json()["choices"][0]["message"]["content"] == "你好,世界"
rid = r2.headers["x-request-id"]
u = ledger.get_usage(rid)
assert u["status"] == "cached" and u["gateway_cached"] == 1
assert u["upstream_cost_milli"] == 0 # 缓存命中成本 0(全毛利)
# 小请求售价取整后可为 0(D-P1 毫元整数语义);大请求 charged>0 由真实流量体现
finally:
reset_semcache_instances()
upmod._client = orig
try:
asyncio_run(hclient.aclose())
except Exception:
pass
mp.reset_pool()
reset_auth_state()