feat: harden cache reuse semantics and expand protocol regressions
Stabilize cross-protocol ask-mode/streaming behavior and reduce session-reuse branch collisions, then add focused docs/tests for multimodal normalization and pool/stats/config paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ class SessionEntry:
|
||||
def hash_user_context(messages: list[dict]) -> str:
|
||||
"""Hash the user/system/developer turns of a message list.
|
||||
|
||||
We deliberately skip `assistant`/`tool` messages because:
|
||||
We deliberately skip `assistant`/`tool` messages here because:
|
||||
- Clients may subtly reformat or trim assistant replies between turns,
|
||||
breaking exact-match keying.
|
||||
- Only the *inputs* are stable, and they're sufficient to identify a
|
||||
@@ -43,6 +43,28 @@ def hash_user_context(messages: list[dict]) -> str:
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def hash_branch_context(messages: list[dict]) -> str:
|
||||
"""Hash assistant/tool turns to reduce branch collisions."""
|
||||
h = hashlib.sha1()
|
||||
for m in messages:
|
||||
role = m.get("role", "")
|
||||
if role not in ("assistant", "tool"):
|
||||
continue
|
||||
content = m.get("content")
|
||||
text = content if isinstance(content, str) else flatten_content(content)
|
||||
tool_calls = m.get("tool_calls")
|
||||
if tool_calls is not None:
|
||||
try:
|
||||
tool_calls_text = json.dumps(tool_calls, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
except Exception:
|
||||
tool_calls_text = str(tool_calls)
|
||||
else:
|
||||
tool_calls_text = ""
|
||||
tool_call_id = m.get("tool_call_id") or ""
|
||||
h.update(f"{role}\x1f{text or ''}\x1f{tool_calls_text}\x1f{tool_call_id}\x1e".encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _tool_fingerprint(tool_config: dict | None) -> str:
|
||||
if not isinstance(tool_config, dict):
|
||||
return "-"
|
||||
@@ -90,11 +112,21 @@ class SessionCache:
|
||||
def enabled(self) -> bool:
|
||||
return self.max > 0
|
||||
|
||||
def build_key(self, api_key: str, messages: list[dict], *, tool_config: dict | None = None) -> str:
|
||||
def build_key(
|
||||
self,
|
||||
api_key: str,
|
||||
messages: list[dict],
|
||||
*,
|
||||
tool_config: dict | None = None,
|
||||
branch_context: str | None = None,
|
||||
) -> str:
|
||||
# API key scoping prevents cross-tenant session leakage even when
|
||||
# different clients happen to produce identical histories.
|
||||
key_scope = hashlib.sha1((api_key or "-").encode("utf-8")).hexdigest()[:12]
|
||||
return f"{key_scope}:{hash_user_context(messages)}:{_tool_fingerprint(tool_config)}"
|
||||
base = f"{key_scope}:{hash_user_context(messages)}:{_tool_fingerprint(tool_config)}"
|
||||
if not branch_context:
|
||||
return base
|
||||
return f"{base}:{branch_context}"
|
||||
|
||||
async def get(self, key: str) -> SessionEntry | None:
|
||||
if not self.enabled:
|
||||
|
||||
Reference in New Issue
Block a user