superlocalmemory 3.6.9 → 3.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +112 -10
  2. package/README.md +67 -9
  3. package/package.json +1 -1
  4. package/pyproject.toml +6 -1
  5. package/skills/slm-optimize/README.md +55 -0
  6. package/skills/slm-optimize/SKILL.md +139 -0
  7. package/src/superlocalmemory/__init__.py +1 -1
  8. package/src/superlocalmemory/cli/compress_cmd.py +32 -70
  9. package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
  10. package/src/superlocalmemory/cli/setup_wizard.py +49 -0
  11. package/src/superlocalmemory/mcp/agent_context.py +111 -0
  12. package/src/superlocalmemory/mcp/server.py +4 -0
  13. package/src/superlocalmemory/mcp/tools_active.py +7 -8
  14. package/src/superlocalmemory/mcp/tools_core.py +16 -0
  15. package/src/superlocalmemory/mcp/tools_optimize.py +304 -0
  16. package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
  17. package/src/superlocalmemory/optimize/cache/exact.py +7 -4
  18. package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
  19. package/src/superlocalmemory/optimize/cache/manager.py +70 -8
  20. package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
  21. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
  22. package/src/superlocalmemory/optimize/compress/router.py +82 -87
  23. package/src/superlocalmemory/optimize/config/__init__.py +16 -0
  24. package/src/superlocalmemory/optimize/config/defaults.py +1 -6
  25. package/src/superlocalmemory/optimize/config/schema.py +2 -19
  26. package/src/superlocalmemory/optimize/config/store.py +15 -1
  27. package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
  28. package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
  29. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
  30. package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
  31. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
  32. package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
  33. package/src/superlocalmemory/optimize/proxy/server.py +29 -0
  34. package/src/superlocalmemory/optimize/storage/db.py +102 -11
  35. package/src/superlocalmemory/optimize/storage/schema.py +11 -0
  36. package/src/superlocalmemory/server/routes/optimize.py +6 -8
  37. package/src/superlocalmemory/server/unified_daemon.py +26 -5
  38. package/src/superlocalmemory/ui/index.html +18 -14
  39. package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
  40. package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
  41. package/src/superlocalmemory/ui/js/optimize.js +9 -9
  42. package/src/superlocalmemory.egg-info/PKG-INFO +69 -10
  43. package/src/superlocalmemory.egg-info/SOURCES.txt +3 -2
  44. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  45. package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
  46. package/src/superlocalmemory/optimize/compress/extractive_json.py +0 -72
@@ -0,0 +1,304 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SLM v3.6.11 — Surface B: MCP Optimize Tools.
6
+
7
+ Five proxy-free tools exposing compression (reversible via CCR) and
8
+ routed-result caching WITHOUT touching ANTHROPIC_BASE_URL, so the full
9
+ 1M context window is preserved on any Claude subscription.
10
+
11
+ Primary Claude conversation turns CANNOT be cached without a proxy.
12
+ These tools cache results the agent explicitly routes through SLM.
13
+
14
+ Fail-open: every tool body is wrapped in try/except Exception.
15
+ Any internal error returns the input unchanged with ok:False — never raises.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import logging
22
+ import threading
23
+ import time
24
+
25
+ from mcp.types import ToolAnnotations
26
+
27
+ from superlocalmemory.mcp.agent_context import get_current_agent_id
28
+ from superlocalmemory.optimize.compress.ccr import CCRStore, _UUID4_RE
29
+ from superlocalmemory.optimize.compress.router import CompressRouter
30
+ from superlocalmemory.optimize.storage.db import CacheDB, _normalize_tenant_id
31
+
32
+ logger = logging.getLogger("slm.mcp.tools_optimize")
33
+
34
+ # ─── Size caps (CWE-400 guards) ───────────────────────────────────────────────
35
+
36
+ _MAX_COMPRESS_BYTES: int = 1_000_000
37
+ _MAX_KV_VALUE_BYTES: int = 1_000_000
38
+ _MAX_KV_KEY_CHARS: int = 512
39
+
40
+ # ─── Exported tool name list (used by server.py + tests) ─────────────────────
41
+
42
+ _OPTIMIZE_TOOL_NAMES = (
43
+ "slm_compress",
44
+ "slm_retrieve",
45
+ "slm_cache_set",
46
+ "slm_cache_get",
47
+ "slm_optimize_stats",
48
+ )
49
+
50
+ # ─── In-module KV counters (thread-safe; MetricsCollector is process-scoped) ─
51
+
52
+ _kv_lock = threading.Lock()
53
+ _kv_hits: int = 0
54
+ _kv_misses: int = 0
55
+
56
+
57
+ def _tenant() -> str:
58
+ # get_current_agent_id() never returns ""; "mcp_client" is its stdio sentinel.
59
+ return get_current_agent_id()
60
+
61
+
62
+ # ─── Tool registration ────────────────────────────────────────────────────────
63
+
64
+
65
+ def register_optimize_tools(server) -> None:
66
+ """Register the 5 Surface B optimize tools on *server*.
67
+
68
+ *server* is duck-typed: must support @server.tool() decorator pattern.
69
+ Compatible with FastMCP, _FilteredServer, and test _MockServer.
70
+ """
71
+
72
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
73
+ async def slm_compress(
74
+ content: str,
75
+ mode: str = "auto",
76
+ reversible: bool = True,
77
+ ttl_seconds: int = 86400,
78
+ ) -> dict:
79
+ """Compress text or tool output to reduce context window usage.
80
+
81
+ Returns compressed text. If lossy and reversible=True, also returns a
82
+ ccr_id — pass it to slm_retrieve to recover the exact original.
83
+
84
+ Args:
85
+ content: Text to compress (max 1MB).
86
+ mode: "normalize" (lossless whitespace) | "auto" | "aggressive".
87
+ reversible: Store original in CCR for later retrieval.
88
+ ttl_seconds: CCR lifetime in seconds (default 24h).
89
+ """
90
+ try:
91
+ if not isinstance(content, str) or not content:
92
+ return {
93
+ "ok": False, "compressed": content or "",
94
+ "strategy": "none", "tokens_before": 0, "tokens_after": 0,
95
+ "ratio": 1.0, "lossy": False, "ccr_id": None,
96
+ "note": "empty input",
97
+ }
98
+
99
+ note_parts: list[str] = []
100
+ if len(content.encode("utf-8")) > _MAX_COMPRESS_BYTES:
101
+ reversible = False
102
+ note_parts.append("content over 1MB: ccr skipped")
103
+
104
+ if mode == "normalize":
105
+ # @staticmethod — lossless whitespace collapse, no config/daemon dep.
106
+ normalized = CompressRouter._normalize_whitespace(content)
107
+ tb = len(content.split())
108
+ ta = len(normalized.split())
109
+ ratio = round(ta / tb, 4) if tb else 1.0
110
+ return {
111
+ "ok": True, "compressed": normalized, "strategy": "normalize",
112
+ "tokens_before": tb, "tokens_after": ta, "ratio": ratio,
113
+ "lossy": False, "ccr_id": None,
114
+ "note": " | ".join(note_parts) or None,
115
+ }
116
+
117
+ if mode == "aggressive":
118
+ note_parts.append(
119
+ "aggressive mode requires daemon compress_mode=aggressive in config"
120
+ )
121
+
122
+ res = CompressRouter.get_instance().compress_text(content)
123
+
124
+ ccr_id = None
125
+ if res.lossy and reversible:
126
+ stored = CCRStore.get_instance().store(
127
+ content.encode("utf-8"),
128
+ tenant_id=_tenant(),
129
+ ttl_seconds=ttl_seconds,
130
+ )
131
+ ccr_id = stored or None
132
+ if ccr_id:
133
+ note_parts.append("reversible: call slm_retrieve with this ccr_id")
134
+
135
+ ratio = (
136
+ round(res.tokens_after / res.tokens_before, 4)
137
+ if res.tokens_before else 1.0
138
+ )
139
+ return {
140
+ "ok": True, "compressed": res.compressed_text, "strategy": res.strategy,
141
+ "tokens_before": res.tokens_before, "tokens_after": res.tokens_after,
142
+ "ratio": ratio, "lossy": res.lossy, "ccr_id": ccr_id,
143
+ "note": " | ".join(note_parts) or None,
144
+ }
145
+
146
+ except Exception as exc:
147
+ logger.error("slm_compress failed (fail-open): %s", exc)
148
+ t = len(content.split()) if isinstance(content, str) else 0
149
+ return {
150
+ "ok": False,
151
+ "compressed": content if isinstance(content, str) else "",
152
+ "strategy": "none", "tokens_before": t, "tokens_after": t,
153
+ "ratio": 1.0, "lossy": False, "ccr_id": None,
154
+ "note": f"internal error: {exc}",
155
+ }
156
+
157
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
158
+ async def slm_retrieve(ccr_id: str) -> dict:
159
+ """Retrieve original text stored during a lossy slm_compress call.
160
+
161
+ Do not log or share ccr_ids — they are unguessable session tokens, but
162
+ if exposed they allow retrieval by anyone with the daemon's decryption key.
163
+
164
+ Args:
165
+ ccr_id: UUID4 returned by slm_compress when reversible=True.
166
+ """
167
+ try:
168
+ if not ccr_id or not _UUID4_RE.match(ccr_id):
169
+ return {
170
+ "ok": False, "content": None, "size_bytes": 0,
171
+ "error": "ccr_id must be a UUID4",
172
+ }
173
+ original = CCRStore.get_instance().retrieve(ccr_id)
174
+ if original is None:
175
+ return {
176
+ "ok": False, "content": None, "size_bytes": 0,
177
+ "error": "not found (expired / never stored / wrong id)",
178
+ }
179
+ size = len(original)
180
+ try:
181
+ text = original.decode("utf-8")
182
+ except UnicodeDecodeError:
183
+ text = original.decode("latin-1")
184
+ return {"ok": True, "content": text, "size_bytes": size, "error": None}
185
+
186
+ except Exception as exc:
187
+ logger.error("slm_retrieve failed (fail-open): %s", exc)
188
+ return {
189
+ "ok": False, "content": None, "size_bytes": 0,
190
+ "error": f"internal error: {exc}",
191
+ }
192
+
193
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
194
+ async def slm_cache_set(key: str, value: str, ttl_seconds: int = 86400) -> dict:
195
+ """Cache a result you want to reuse (tool output, file read, search result).
196
+
197
+ This caches results the agent explicitly routes through SLM — NOT the
198
+ Claude conversation turn (impossible without a proxy).
199
+
200
+ Do not cache secrets, credentials, or ccr_ids via this tool.
201
+
202
+ Args:
203
+ key: Cache key (max 512 chars). Namespaced per agent automatically.
204
+ value: Value to store as string (max 1MB).
205
+ ttl_seconds: Time-to-live in seconds (default 24h).
206
+ """
207
+ try:
208
+ if not key or len(key) > _MAX_KV_KEY_CHARS:
209
+ return {
210
+ "ok": False, "stored": False,
211
+ "note": f"key must be 1–{_MAX_KV_KEY_CHARS} chars",
212
+ }
213
+ value_bytes = value.encode("utf-8")
214
+ if len(value_bytes) > _MAX_KV_VALUE_BYTES:
215
+ return {"ok": False, "stored": False, "note": "value exceeds 1MB limit"}
216
+
217
+ tenant = _tenant()
218
+ cache_key = hashlib.sha256(f"mcpkv:{tenant}:{key}".encode()).hexdigest()
219
+ norm_tid = _normalize_tenant_id(tenant)
220
+ ttl_exp = time.time() + ttl_seconds
221
+
222
+ CacheDB.get_default().set(
223
+ cache_key, norm_tid, value_bytes,
224
+ model="mcp-kv", ttl_expires=ttl_exp, tags=["mcp-kv"],
225
+ )
226
+ return {"ok": True, "stored": True, "note": None}
227
+
228
+ except Exception as exc:
229
+ logger.error("slm_cache_set failed (fail-open): %s", exc)
230
+ return {"ok": False, "stored": False, "note": f"internal error: {exc}"}
231
+
232
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
233
+ async def slm_cache_get(key: str) -> dict:
234
+ """Retrieve a previously cached result.
235
+
236
+ Returns hit:True + value if the key exists and has not expired.
237
+ Returns hit:False (never raises) on miss, expiry, or any error.
238
+
239
+ Args:
240
+ key: Cache key used in slm_cache_set.
241
+ """
242
+ global _kv_hits, _kv_misses
243
+ try:
244
+ if not key or len(key) > _MAX_KV_KEY_CHARS:
245
+ return {
246
+ "ok": False, "hit": False, "value": None,
247
+ "note": f"key must be 1–{_MAX_KV_KEY_CHARS} chars",
248
+ }
249
+ tenant = _tenant()
250
+ cache_key = hashlib.sha256(f"mcpkv:{tenant}:{key}".encode()).hexdigest()
251
+ norm_tid = _normalize_tenant_id(tenant)
252
+
253
+ blob = CacheDB.get_default().get_value(cache_key, norm_tid)
254
+ if blob is None:
255
+ with _kv_lock:
256
+ _kv_misses += 1
257
+ return {"ok": True, "hit": False, "value": None, "note": None}
258
+ with _kv_lock:
259
+ _kv_hits += 1
260
+ return {"ok": True, "hit": True, "value": blob.decode("utf-8"), "note": None}
261
+
262
+ except Exception as exc:
263
+ logger.error("slm_cache_get failed (fail-open): %s", exc)
264
+ return {
265
+ "ok": False, "hit": False, "value": None,
266
+ "note": f"internal error: {exc}",
267
+ }
268
+
269
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
270
+ async def slm_optimize_stats() -> dict:
271
+ """Return compression and cache statistics.
272
+
273
+ Proxy/compress stats are daemon-persisted (accurate across restarts).
274
+ KV stats are in-module counters for this MCP process session only.
275
+ """
276
+ try:
277
+ snap = CacheDB.get_default().metrics_load()
278
+ with _kv_lock:
279
+ kv_h = _kv_hits
280
+ kv_m = _kv_misses
281
+ return {
282
+ "ok": True,
283
+ "compress_runs": snap.compress_runs,
284
+ "tokens_saved_compress": snap.tokens_saved_compress,
285
+ "cache_proxy_hits": snap.hits,
286
+ "cache_proxy_misses": snap.misses,
287
+ "cache_kv_hits": kv_h,
288
+ "cache_kv_misses": kv_m,
289
+ "ccr_note": (
290
+ "CCR entry count not tracked per-session; "
291
+ "see daemon /api/v1/metrics"
292
+ ),
293
+ "note": "proxy stats are daemon-persisted; kv stats are this session only",
294
+ }
295
+ except Exception as exc:
296
+ logger.error("slm_optimize_stats failed (fail-open): %s", exc)
297
+ return {
298
+ "ok": False,
299
+ "compress_runs": 0, "tokens_saved_compress": 0,
300
+ "cache_proxy_hits": 0, "cache_proxy_misses": 0,
301
+ "cache_kv_hits": 0, "cache_kv_misses": 0,
302
+ "ccr_note": None,
303
+ "note": f"internal error: {exc}",
304
+ }
@@ -93,19 +93,23 @@ class PerItemBoundaryRecord:
93
93
  query_sim: float,
94
94
  delta: float = 0.05,
95
95
  epsilon_grid: tuple[float, ...] = (0.01, 0.02, 0.05, 0.10),
96
+ return_threshold: float = 1.0,
96
97
  ) -> float:
97
98
  """Compute τ̂ — the vCache exploration probability (Eq. 11).
98
99
 
99
100
  Args:
100
- query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
101
- delta: δ — user-defined maximum error rate.
102
- Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
103
- epsilon_grid: ε values for the Eq. 11 min sweep.
104
- Distinct from δ; controls CI conservativeness.
101
+ query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
102
+ delta: δ — user-defined maximum error rate.
103
+ Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
104
+ epsilon_grid: ε values for the Eq. 11 min sweep.
105
+ Distinct from δ; controls CI conservativeness.
106
+ return_threshold: Semantic return threshold from config (semantic_return_threshold).
107
+ C-03 fix: during cold start, exploit directly when
108
+ query_sim >= return_threshold instead of always exploring.
105
109
 
106
110
  Returns:
107
111
  τ̂ ∈ [0.0, 1.0]. Lower = more exploitation.
108
- Cold start (n < 3): returns 1.0 (always explore).
112
+ Cold start (n < 3): 0.0 if query_sim >= return_threshold, else 1.0.
109
113
 
110
114
  Eq. 11 derivation (from the paper):
111
115
  1. I_tt = Σ γ̂² · p_i(1 - p_i) [Fisher info diagonal]
@@ -117,7 +121,12 @@ class PerItemBoundaryRecord:
117
121
  """
118
122
  n = len(self.samples)
119
123
  if n < 3:
120
- return 1.0 # cold startalways explore
124
+ # ARCH-02 note: this function serves dual purpose (a) warm-phase vCache
125
+ # Eq. 11 tau computation and (b) cold-start similarity gate. The cold-start
126
+ # branch (n < 3) is intentionally simple: if the query is already above the
127
+ # return threshold, serve it (tau=0.0 → exploit); otherwise explore (tau=1.0).
128
+ # C-03: honor return_threshold — avoids 100% miss until 3 samples are accumulated.
129
+ return 0.0 if query_sim >= return_threshold else 1.0
121
130
 
122
131
  # Step 1: Fisher-information SE
123
132
  i_tt = 0.0
@@ -144,12 +153,17 @@ class PerItemBoundaryRecord:
144
153
 
145
154
  return best_tau
146
155
 
147
- def should_explore(self, query_sim: float, delta: float = 0.05) -> bool:
156
+ def should_explore(
157
+ self,
158
+ query_sim: float,
159
+ delta: float = 0.05,
160
+ return_threshold: float = 1.0,
161
+ ) -> bool:
148
162
  """Return True (explore = LLM call) or False (exploit = serve cache).
149
163
 
150
164
  Source: vCache Algorithm 2: draw u ~ Uniform(0, 1); explore iff u ≤ τ̂.
151
165
  """
152
- tau = self.compute_tau(query_sim, delta=delta)
166
+ tau = self.compute_tau(query_sim, delta=delta, return_threshold=return_threshold)
153
167
  return _RNG.random() <= tau
154
168
 
155
169
  def add_sample(
@@ -3,11 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import time
7
8
  from typing import Any
8
9
 
9
10
  from superlocalmemory.optimize.cache.key_builder import CacheConfig
10
11
 
12
+ logger = logging.getLogger(__name__)
11
13
 
12
14
  _NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
13
15
  "tool_use",
@@ -20,17 +22,21 @@ _NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
20
22
  def _is_cacheable_response(response: dict) -> bool:
21
23
  finish = response.get("stop_reason") or response.get("finish_reason") or ""
22
24
  if finish in _NON_CACHEABLE_FINISH_REASONS:
25
+ logger.debug("exact: skip cache (finish_reason=%r)", finish)
23
26
  return False
24
27
  choices = response.get("choices") or []
25
28
  for choice in choices:
26
29
  fr = (choice.get("finish_reason") or "")
27
30
  if fr in _NON_CACHEABLE_FINISH_REASONS:
31
+ logger.debug("exact: skip cache (choice.finish_reason=%r)", fr)
28
32
  return False
29
33
  msg = choice.get("message") or {}
30
34
  if msg.get("tool_calls"):
35
+ logger.debug("exact: skip cache (choice.message.tool_calls present)")
31
36
  return False
32
37
  for block in response.get("content") or []:
33
38
  if isinstance(block, dict) and block.get("type") == "tool_use":
39
+ logger.debug("exact: skip cache (tool_use content block)")
34
40
  return False
35
41
  return True
36
42
 
@@ -46,9 +52,6 @@ class ExactCache:
46
52
  row = self._db.get(key, tenant_id)
47
53
  if row is None:
48
54
  return None
49
- if row.ttl_expires is not None and row.ttl_expires < time.time():
50
- self._db.delete(key, tenant_id)
51
- return None
52
55
  return json.loads(row.value.decode("utf-8"))
53
56
 
54
57
  def set(
@@ -74,7 +77,7 @@ class ExactCache:
74
77
  value=encoded,
75
78
  model=model,
76
79
  ttl_expires=expires_at,
77
- tags=[],
80
+ tags=tags,
78
81
  )
79
82
  return True
80
83
 
@@ -4,10 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import json
7
+ import logging
7
8
  import re
8
9
  from dataclasses import dataclass, field
9
10
  from typing import Any
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
11
14
  DETERMINISTIC_PARAMS: frozenset[str] = frozenset({
12
15
  "max_tokens", "stop", "stop_sequences", "top_p", "top_k",
13
16
  "response_format", "tools", "tool_choice",
@@ -62,6 +65,16 @@ class KeyBuilder:
62
65
  temperature = 0.0
63
66
 
64
67
  if temperature != 0 and not self._config.allow_nonzero_temperature_cache:
68
+ logger.debug(
69
+ "cache skip: temperature=%.2f allow_nonzero=%s",
70
+ temperature,
71
+ self._config.allow_nonzero_temperature_cache,
72
+ )
73
+ try:
74
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector
75
+ MetricsCollector.get_instance().increment_skipped_temperature()
76
+ except Exception:
77
+ pass
65
78
  return None
66
79
 
67
80
  deterministic_params: dict[str, Any] = {}
@@ -2,12 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib as _hashlib
6
+ import json as _json_mod
5
7
  import logging
6
8
  import threading
7
9
  import time
8
10
  from abc import ABC, abstractmethod
9
11
  from typing import Any, Callable
10
12
 
13
+ # C-08: precomputed hash for the common "default" tenant — avoids sha256 on every request
14
+ _DEFAULT_TENANT_HASH: str = _hashlib.sha256(b"default").hexdigest()
15
+
11
16
  from superlocalmemory.optimize.cache.exact import ExactCache
12
17
  from superlocalmemory.optimize.cache.invalidation import InvalidationEngine
13
18
  from superlocalmemory.optimize.cache.key_builder import CacheConfig, KeyBuilder
@@ -230,6 +235,26 @@ class CacheManager:
230
235
  ttl_seconds=0,
231
236
  )
232
237
  self._metrics.exact_misses += 1
238
+
239
+ # C-01: semantic fallback on exact miss (only when explicitly enabled)
240
+ if self._semantic.is_enabled():
241
+ try:
242
+ sem_result = self._semantic.lookup(req, tenant_id, None)
243
+ if sem_result is not None:
244
+ self._metrics.semantic_hits += 1
245
+ if isinstance(sem_result, CachedResponse):
246
+ return sem_result
247
+ # VCacheSemantic may return a response dict — wrap it
248
+ return CachedResponse(
249
+ hit=True,
250
+ data=_json_mod.dumps(sem_result, ensure_ascii=False).encode(),
251
+ cache_key=key,
252
+ ttl_seconds=0,
253
+ )
254
+ except Exception as exc:
255
+ logger.warning("SemanticTier.lookup raised (fail-open): %s", exc)
256
+ self._metrics.semantic_misses += 1
257
+
233
258
  # Return miss WITH the key so callers can use it for cache storage.
234
259
  return CachedResponse(hit=False, data=None, cache_key=key, ttl_seconds=0)
235
260
 
@@ -258,6 +283,13 @@ class CacheManager:
258
283
  self._invalidation.register(key, tenant_id, tags)
259
284
  self._metrics.sets += 1
260
285
 
286
+ # C-02: index in semantic tier after exact write (fail-open)
287
+ if self._semantic.is_enabled():
288
+ try:
289
+ self._semantic.index_entry(req, tenant_id, None, response_dict)
290
+ except Exception as exc:
291
+ logger.warning("SemanticTier.index_entry raised (fail-open): %s", exc)
292
+
261
293
  # ---- CacheHook protocol implementation (INTERFACE-CONTRACT §3) ----
262
294
 
263
295
  def check(self, req: ProxyRequest) -> "CachedResponse | None":
@@ -269,7 +301,7 @@ class CacheManager:
269
301
  cache-miss result.
270
302
  """
271
303
  try:
272
- result = self.get(req, tenant_id="default")
304
+ result = self.get(req, tenant_id=_DEFAULT_TENANT_HASH)
273
305
  if result is not None and not result.hit:
274
306
  MetricsCollector.get_instance().on_miss()
275
307
  return result
@@ -280,21 +312,51 @@ class CacheManager:
280
312
  def store(self, req: ProxyRequest, resp: ProviderResponse) -> None:
281
313
  """CacheHook.store() — persist response; fail-open on error."""
282
314
  try:
283
- self.set(req, resp, tenant_id="default")
315
+ self.set(req, resp, tenant_id=_DEFAULT_TENANT_HASH)
284
316
  except Exception as exc:
285
317
  logger.warning("CacheManager.store raised (fail-open): %s", exc)
286
318
 
287
319
  def on_hit(self, req: ProxyRequest, resp: bytes, tokens_saved: int) -> None:
288
320
  """CacheHook.on_hit() — forward token savings to MetricsCollector.
289
-
290
- Contract §7: cache skip saves BOTH input+output tokens (whole call avoided).
291
- tokens_saved = input tokens saved (from request).
292
- Output tokens estimated from response body size (~4 bytes per token).
321
+
322
+ M-01: compute input tokens from request body when caller passes 0.
323
+ M-02: parse real output tokens from cached response usage field.
324
+ All counts are estimates for display not billing-accurate.
293
325
  """
326
+ import json as _json
327
+
328
+ # M-01: estimate input tokens from message content
329
+ if tokens_saved == 0 and isinstance(req, ProxyRequest):
330
+ try:
331
+ body = req.body or {}
332
+ total_chars = 0
333
+ for m in (body.get("messages") or []):
334
+ c = m.get("content", "")
335
+ if isinstance(c, str):
336
+ total_chars += len(c)
337
+ elif isinstance(c, list):
338
+ for blk in c:
339
+ if isinstance(blk, dict):
340
+ total_chars += len(blk.get("text", "") or "")
341
+ total_chars += len(body.get("system", "") or "")
342
+ tokens_saved = max(0, total_chars // 4)
343
+ except Exception:
344
+ pass
345
+
346
+ # M-02: parse real output tokens from stored response
294
347
  output_tokens = 0
295
348
  if resp:
296
- # Rough estimate: 4 bytes per token for English text
297
- output_tokens = len(resp) // 4
349
+ try:
350
+ data = _json.loads(resp)
351
+ usage = data.get("usage") or {}
352
+ output_tokens = (
353
+ usage.get("output_tokens")
354
+ or usage.get("completion_tokens")
355
+ or 0
356
+ )
357
+ except Exception:
358
+ output_tokens = len(resp) // 4 # fallback byte-estimate
359
+
298
360
  MetricsCollector.get_instance().on_hit(
299
361
  tokens_saved_input=tokens_saved,
300
362
  tokens_saved_output=output_tokens,
@@ -294,7 +294,8 @@ class VCacheSemantic(SemanticTier):
294
294
  # Step 5: vCache exploit/explore (MLE τ̂ via record_outcome updates)
295
295
  record = self._boundary_store.get(best_entry_id)
296
296
  delta = float(getattr(cfg, "semantic_error_target", _DEFAULT_ERROR_TARGET))
297
- if record.should_explore(best_score, delta=delta):
297
+ sem_return_threshold = float(getattr(cfg, "semantic_return_threshold", _DEFAULT_RETURN_THRESHOLD))
298
+ if record.should_explore(best_score, delta=delta, return_threshold=sem_return_threshold):
298
299
  logger.debug(
299
300
  "VCacheSemantic: explore (score=%.4f entry=%s t_hat=%.4f)",
300
301
  best_score, best_entry_id, record.t_hat,
@@ -363,11 +364,11 @@ class VCacheSemantic(SemanticTier):
363
364
  self._index[tenant_id] = []
364
365
  return
365
366
  entries: list[tuple[str, str, np.ndarray]] = []
366
- for entry_id, blob in rows:
367
+ for entry_id, blob, ctx_fp in rows: # C-10: unpack persisted context_fp
367
368
  try:
368
369
  v = np.frombuffer(blob, dtype=np.float32).copy()
369
370
  if v.shape[0] == _EMBED_DIM:
370
- entries.append((entry_id, "", v))
371
+ entries.append((entry_id, ctx_fp, v))
371
372
  except Exception:
372
373
  continue
373
374
  with self._index_lock:
@@ -422,13 +423,17 @@ class VCacheSemantic(SemanticTier):
422
423
  )
423
424
  return
424
425
 
425
- # Store vector in DB
426
+ # Store vector in DB — C-10: persist context_fp alongside the vector
426
427
  vec_bytes = vec.tobytes()
427
428
  self._db.vec_add(
428
429
  entry_id=entry_id,
429
430
  tenant_id=tenant_id,
430
431
  vector=vec_bytes,
431
- meta={"model": "nomic-ai/nomic-embed-text-v1.5", "dim": _EMBED_DIM},
432
+ meta={
433
+ "model": "nomic-ai/nomic-embed-text-v1.5",
434
+ "dim": _EMBED_DIM,
435
+ "context_fp": context_fp,
436
+ },
432
437
  )
433
438
 
434
439
  # Initialize boundary record if new
@@ -32,16 +32,10 @@ class LLMLinguaCompressor:
32
32
 
33
33
  def __init__(
34
34
  self,
35
- model_name: str = _MODEL_BERT,
35
+ model_name: str = _MODEL_XLM,
36
36
  device_map: str = "cpu",
37
37
  rate: float = _DEFAULT_RATE,
38
38
  ) -> None:
39
- import os
40
- if os.environ.get("SLM_DISABLE_HF_DOWNLOAD", "0") == "1":
41
- raise ImportError(
42
- "LLMLingua-2 model download blocked: SLM_DISABLE_HF_DOWNLOAD=1. "
43
- "Set compress_llmlingua_allow_download=true in optimize.json."
44
- )
45
39
  try:
46
40
  from llmlingua import PromptCompressor # type: ignore[import]
47
41
  except ImportError as e: