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.
- package/CHANGELOG.md +112 -10
- package/README.md +67 -9
- package/package.json +1 -1
- package/pyproject.toml +6 -1
- package/skills/slm-optimize/README.md +55 -0
- package/skills/slm-optimize/SKILL.md +139 -0
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/compress_cmd.py +32 -70
- package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
- package/src/superlocalmemory/cli/setup_wizard.py +49 -0
- package/src/superlocalmemory/mcp/agent_context.py +111 -0
- package/src/superlocalmemory/mcp/server.py +4 -0
- package/src/superlocalmemory/mcp/tools_active.py +7 -8
- package/src/superlocalmemory/mcp/tools_core.py +16 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +304 -0
- package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
- package/src/superlocalmemory/optimize/cache/exact.py +7 -4
- package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
- package/src/superlocalmemory/optimize/cache/manager.py +70 -8
- package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
- package/src/superlocalmemory/optimize/compress/router.py +82 -87
- package/src/superlocalmemory/optimize/config/__init__.py +16 -0
- package/src/superlocalmemory/optimize/config/defaults.py +1 -6
- package/src/superlocalmemory/optimize/config/schema.py +2 -19
- package/src/superlocalmemory/optimize/config/store.py +15 -1
- package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
- package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/server.py +29 -0
- package/src/superlocalmemory/optimize/storage/db.py +102 -11
- package/src/superlocalmemory/optimize/storage/schema.py +11 -0
- package/src/superlocalmemory/server/routes/optimize.py +6 -8
- package/src/superlocalmemory/server/unified_daemon.py +26 -5
- package/src/superlocalmemory/ui/index.html +18 -14
- package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
- package/src/superlocalmemory/ui/js/optimize.js +9 -9
- package/src/superlocalmemory.egg-info/PKG-INFO +69 -10
- package/src/superlocalmemory.egg-info/SOURCES.txt +3 -2
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
- 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:
|
|
101
|
-
delta:
|
|
102
|
-
|
|
103
|
-
epsilon_grid:
|
|
104
|
-
|
|
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):
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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=
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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,
|
|
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={
|
|
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 =
|
|
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:
|