superlocalmemory 3.6.8 → 3.6.10

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 (48) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +8 -4
  3. package/package.json +1 -1
  4. package/pyproject.toml +6 -1
  5. package/src/superlocalmemory/__init__.py +6 -2
  6. package/src/superlocalmemory/cli/compress_cmd.py +32 -70
  7. package/src/superlocalmemory/cli/daemon.py +25 -3
  8. package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
  9. package/src/superlocalmemory/cli/setup_wizard.py +49 -0
  10. package/src/superlocalmemory/core/config.py +28 -0
  11. package/src/superlocalmemory/core/engine.py +15 -4
  12. package/src/superlocalmemory/core/health_monitor.py +32 -9
  13. package/src/superlocalmemory/mcp/agent_context.py +111 -0
  14. package/src/superlocalmemory/mcp/tools_active.py +47 -12
  15. package/src/superlocalmemory/mcp/tools_core.py +22 -2
  16. package/src/superlocalmemory/mcp/tools_mesh.py +37 -38
  17. package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
  18. package/src/superlocalmemory/optimize/cache/exact.py +7 -4
  19. package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
  20. package/src/superlocalmemory/optimize/cache/manager.py +70 -8
  21. package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
  22. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
  23. package/src/superlocalmemory/optimize/compress/router.py +82 -87
  24. package/src/superlocalmemory/optimize/config/__init__.py +16 -0
  25. package/src/superlocalmemory/optimize/config/defaults.py +1 -6
  26. package/src/superlocalmemory/optimize/config/schema.py +2 -19
  27. package/src/superlocalmemory/optimize/config/store.py +15 -1
  28. package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
  29. package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
  30. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
  31. package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
  32. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
  33. package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
  34. package/src/superlocalmemory/optimize/proxy/server.py +29 -0
  35. package/src/superlocalmemory/optimize/storage/db.py +78 -11
  36. package/src/superlocalmemory/optimize/storage/schema.py +11 -0
  37. package/src/superlocalmemory/retrieval/spreading_activation.py +8 -3
  38. package/src/superlocalmemory/server/routes/optimize.py +6 -8
  39. package/src/superlocalmemory/server/unified_daemon.py +68 -11
  40. package/src/superlocalmemory/ui/index.html +18 -14
  41. package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
  42. package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
  43. package/src/superlocalmemory/ui/js/optimize.js +9 -9
  44. package/src/superlocalmemory.egg-info/PKG-INFO +10 -5
  45. package/src/superlocalmemory.egg-info/SOURCES.txt +2 -2
  46. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  47. package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
  48. package/src/superlocalmemory/optimize/compress/extractive_json.py +0 -72
@@ -358,6 +358,7 @@ async def _stream_and_cache_forward(
358
358
  body_bytes: bytes,
359
359
  upstream_url: str,
360
360
  on_complete: "Callable[[bytes], Any] | None" = None,
361
+ max_accumulate: int | None = None,
361
362
  ) -> Response | StreamingResponse:
362
363
  """Stream-forward with optional post-stream cache-store callback.
363
364
 
@@ -388,10 +389,12 @@ async def _stream_and_cache_forward(
388
389
  )
389
390
 
390
391
  acc: list[bytes] = []
392
+ acc_bytes = 0
393
+ acc_capped = False
391
394
  complete_called = False
392
395
 
393
396
  async def _generate() -> AsyncIterator[bytes]:
394
- nonlocal complete_called
397
+ nonlocal complete_called, acc_bytes, acc_capped
395
398
  stream_error = False
396
399
  try:
397
400
  async with proxy.http_client.stream(
@@ -399,7 +402,17 @@ async def _stream_and_cache_forward(
399
402
  ) as upstream_resp:
400
403
  async for chunk in upstream_resp.aiter_bytes():
401
404
  if chunk:
402
- acc.append(chunk)
405
+ # Always forward to the client; only bound what we hold
406
+ # in memory for the on_complete callback (CWE-400).
407
+ if max_accumulate is None or acc_bytes < max_accumulate:
408
+ acc.append(chunk)
409
+ acc_bytes += len(chunk)
410
+ elif not acc_capped:
411
+ acc_capped = True
412
+ logger.debug(
413
+ "[%s] stream accumulator capped at %d bytes",
414
+ request_id, max_accumulate,
415
+ )
403
416
  yield chunk
404
417
  except httpx.RemoteProtocolError as exc:
405
418
  stream_error = True
@@ -447,6 +460,91 @@ async def _stream_and_cache_forward(
447
460
  )
448
461
 
449
462
 
463
+ async def capture_passthrough_forward(
464
+ proxy: Any,
465
+ request: Request,
466
+ *,
467
+ provider: str,
468
+ upstream_url: str,
469
+ allowed_headers: frozenset,
470
+ request_id: str,
471
+ model_hint: str = "",
472
+ sse_parser: "Callable[[bytes], bytes | None] | None" = None,
473
+ is_stream: bool = False,
474
+ ) -> Response | StreamingResponse:
475
+ """Shadow-capture passthrough (v3.6.10, plan §7).
476
+
477
+ Pure passthrough to upstream + record the exchange to the capture corpus.
478
+ NO cache, NO compression — capture mode observes only authentic traffic.
479
+ Fail-open: a capture or forward error degrades to a normal forward/error
480
+ response; the user's request is never blocked by capture.
481
+ """
482
+ from superlocalmemory.optimize.proxy.capture import (
483
+ extract_usage,
484
+ record_exchange_async,
485
+ )
486
+
487
+ body_bytes = await request.body()
488
+ fwd_headers = _build_forward_headers(request, allowed_headers)
489
+ fwd_headers["content-length"] = str(len(body_bytes))
490
+
491
+ if is_stream:
492
+ async def _on_complete(acc: bytes) -> None:
493
+ parsed = sse_parser(acc) if sse_parser else None
494
+ payload = parsed if parsed is not None else acc
495
+ itok, otok, mdl = extract_usage(provider, parsed)
496
+ await record_exchange_async(
497
+ provider=provider,
498
+ model=mdl or model_hint,
499
+ request_body=body_bytes,
500
+ response_body=payload,
501
+ content_type="text/event-stream",
502
+ input_tokens=itok,
503
+ output_tokens=otok,
504
+ status_code=200,
505
+ stream=True,
506
+ )
507
+
508
+ # Bound the in-memory accumulator (CWE-400): the corpus only keeps the
509
+ # first 1 MB per side anyway, so cap accumulation there.
510
+ from superlocalmemory.optimize.proxy.capture import _MAX_CAPTURE_BODY_BYTES
511
+ return await _stream_and_cache_forward(
512
+ proxy, request_id, fwd_headers, body_bytes, upstream_url,
513
+ on_complete=_on_complete,
514
+ max_accumulate=_MAX_CAPTURE_BODY_BYTES,
515
+ )
516
+
517
+ if proxy.http_client is None:
518
+ return await _fail_open_forward(proxy, request, upstream_url)
519
+ try:
520
+ upstream_resp = await proxy.http_client.post(
521
+ upstream_url, content=body_bytes, headers=fwd_headers,
522
+ )
523
+ except Exception as exc:
524
+ logger.error("[%s] capture passthrough upstream error: %r", request_id, exc)
525
+ return await _fail_open_forward(proxy, request, upstream_url)
526
+
527
+ resp_bytes = upstream_resp.content
528
+ itok, otok, mdl = extract_usage(provider, resp_bytes)
529
+ await record_exchange_async(
530
+ provider=provider,
531
+ model=mdl or model_hint,
532
+ request_body=body_bytes,
533
+ response_body=resp_bytes,
534
+ content_type="application/json",
535
+ input_tokens=itok,
536
+ output_tokens=otok,
537
+ status_code=upstream_resp.status_code,
538
+ stream=False,
539
+ )
540
+ return Response(
541
+ content=resp_bytes,
542
+ status_code=upstream_resp.status_code,
543
+ media_type="application/json",
544
+ headers=_filter_response_headers(dict(upstream_resp.headers)),
545
+ )
546
+
547
+
450
548
  async def _safe_cache_check(hooks: HookChain, ctx: ProxyRequest) -> CachedResponse:
451
549
  try:
452
550
  result = hooks.cache.check(ctx)
@@ -25,7 +25,9 @@ from superlocalmemory.optimize.proxy._helpers import (
25
25
  _safe_compress,
26
26
  _stream_and_cache_forward,
27
27
  _stream_forward,
28
+ capture_passthrough_forward,
28
29
  )
30
+ from superlocalmemory.optimize.proxy.capture import capture_enabled
29
31
  from superlocalmemory.optimize.proxy.lifecycle import ProviderResponse, ProxyRequest
30
32
 
31
33
  logger = logging.getLogger("slm.optimize.proxy.anthropic")
@@ -192,6 +194,16 @@ async def handle_messages(proxy: object, request: Request) -> Response:
192
194
 
193
195
  stream = bool(body.get("stream", False))
194
196
  has_tools = _body_has_tools(body)
197
+
198
+ # v3.6.10 shadow-capture (plan §7): pure passthrough + corpus record.
199
+ if capture_enabled():
200
+ return await capture_passthrough_forward(
201
+ proxy, request, provider="anthropic", upstream_url=upstream_url,
202
+ allowed_headers=_ANTHROPIC_FORWARD_HEADERS, request_id=request_id,
203
+ model_hint=str(body.get("model", "")),
204
+ sse_parser=_parse_sse_to_json, is_stream=stream,
205
+ )
206
+
195
207
  ctx = ProxyRequest(
196
208
  provider="anthropic",
197
209
  method="POST",
@@ -0,0 +1,243 @@
1
+ """capture.py — Lossless shadow-capture of real proxy traffic (v3.6.10, plan §7).
2
+
3
+ Purpose: build a dogfood corpus of real {request, response, model, tokens,
4
+ content_type} pairs so the cache + compression benchmark (benchmarks/optimize/)
5
+ can be replayed against authentic traffic instead of only synthetic prompts.
6
+
7
+ Activation: set ``SLM_OPTIMIZE_CAPTURE=1`` in the daemon's environment. When on:
8
+ * the proxy runs in PURE PASSTHROUGH — cache + compression hooks are disabled
9
+ at load time (see server._load_hooks), so capture never observes a mutated
10
+ request or a cache hit; every line is a genuine upstream exchange.
11
+ * each completed exchange is appended as one JSON line to
12
+ ``~/.superlocalmemory/optimize_capture.jsonl`` (0600, gitignored).
13
+
14
+ ISOLATION GUARANTEE: this module writes ONLY to optimize_capture.jsonl. It never
15
+ opens memory.db, llmcache.db, or any SLM memory store. (Plan §9 hard rule.)
16
+
17
+ FAIL-OPEN: a capture failure (disk full, permission, encode error) is logged and
18
+ swallowed — it MUST NOT break the proxied request the user is waiting on.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import json
25
+ import logging
26
+ import os
27
+ import threading
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ logger = logging.getLogger("slm.optimize.proxy.capture")
32
+
33
+ _CAPTURE_DIRNAME = ".superlocalmemory"
34
+ _CAPTURE_FILENAME = "optimize_capture.jsonl"
35
+ _CAPTURE_ENV = "SLM_OPTIMIZE_CAPTURE"
36
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
37
+
38
+ # Cap a single captured body so a pathological 10 MB request can't bloat the
39
+ # corpus line beyond what the replay harness will read back. Bodies above this
40
+ # are recorded truncated with a marker (capture is for benchmarking, not audit).
41
+ _MAX_CAPTURE_BODY_BYTES = 1 * 1024 * 1024 # 1 MB per side
42
+
43
+ # Providers whose response bodies follow the OpenAI usage schema
44
+ # ({"usage": {"prompt_tokens", "completion_tokens"}, "model"}).
45
+ _OPENAI_FORMAT_PROVIDERS = frozenset({"openai", "gemini-openai-compat"})
46
+
47
+
48
+ def capture_enabled() -> bool:
49
+ """True iff ``SLM_OPTIMIZE_CAPTURE`` is set to a truthy value."""
50
+ return os.environ.get(_CAPTURE_ENV, "").strip().lower() in _TRUTHY
51
+
52
+
53
+ def _capture_path() -> Path:
54
+ return Path.home() / _CAPTURE_DIRNAME / _CAPTURE_FILENAME
55
+
56
+
57
+ class ShadowCapture:
58
+ """Thread-safe append-only JSONL writer for proxy exchanges (singleton)."""
59
+
60
+ _instance: "ShadowCapture | None" = None
61
+ _instance_lock = threading.Lock()
62
+
63
+ def __init__(self, path: Path | None = None) -> None:
64
+ self._path = path or _capture_path()
65
+ self._write_lock = threading.Lock()
66
+ self._count = 0
67
+
68
+ @classmethod
69
+ def get_instance(cls) -> "ShadowCapture":
70
+ # Double-checked locking: cheap fast-path after first construction.
71
+ if cls._instance is None:
72
+ with cls._instance_lock:
73
+ if cls._instance is None:
74
+ cls._instance = cls()
75
+ return cls._instance
76
+
77
+ @classmethod
78
+ def reset_instance(cls) -> None:
79
+ """Test hook — drop the singleton so a fresh path can be injected."""
80
+ with cls._instance_lock:
81
+ cls._instance = None
82
+
83
+ @property
84
+ def path(self) -> Path:
85
+ return self._path
86
+
87
+ @property
88
+ def count(self) -> int:
89
+ return self._count
90
+
91
+ def record(self, entry: dict[str, Any]) -> bool:
92
+ """Append one capture entry as a JSON line. Returns True on success.
93
+
94
+ Fail-open: any error is logged and False is returned; never raised.
95
+
96
+ Security: opens with a single ``os.open`` carrying ``O_CREAT |
97
+ O_APPEND | O_NOFOLLOW`` and mode ``0o600`` on EVERY write. O_NOFOLLOW
98
+ refuses a symlink pre-placed at the path (symlink-append attack), and
99
+ the unconditional 0600-on-create removes the stat/exists TOCTOU that
100
+ could otherwise drop the file to the process umask.
101
+ """
102
+ try:
103
+ line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
104
+ except (TypeError, ValueError) as exc:
105
+ logger.warning("capture: entry not JSON-serialisable, dropped: %r", exc)
106
+ return False
107
+
108
+ try:
109
+ with self._write_lock:
110
+ self._path.parent.mkdir(parents=True, exist_ok=True)
111
+ flags = os.O_CREAT | os.O_WRONLY | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0)
112
+ fd = os.open(self._path, flags, 0o600)
113
+ with os.fdopen(fd, "a", encoding="utf-8") as fh:
114
+ fh.write(line + "\n")
115
+ self._count += 1
116
+ return True
117
+ except OSError as exc:
118
+ # PermissionError / symlink-refusal (ELOOP) are security-relevant —
119
+ # surface the errno but still fail open so the request is never blocked.
120
+ logger.warning("capture: write failed (fail-open): %r", exc)
121
+ return False
122
+
123
+
124
+ def _truncate(raw: bytes) -> tuple[str, bool]:
125
+ """Decode bytes for storage; truncate beyond the per-side cap."""
126
+ truncated = len(raw) > _MAX_CAPTURE_BODY_BYTES
127
+ head = raw[:_MAX_CAPTURE_BODY_BYTES] if truncated else raw
128
+ return head.decode("utf-8", errors="replace"), truncated
129
+
130
+
131
+ def build_entry(
132
+ *,
133
+ provider: str,
134
+ model: str,
135
+ request_body: bytes,
136
+ response_body: bytes,
137
+ content_type: str,
138
+ input_tokens: int,
139
+ output_tokens: int,
140
+ status_code: int,
141
+ stream: bool,
142
+ ) -> dict[str, Any]:
143
+ """Construct a capture entry dict from a completed exchange.
144
+
145
+ No timestamp is stamped here (Date.now is intentionally avoided in some
146
+ runtimes); the replay harness keys on content, not time, and the file's own
147
+ line order preserves arrival sequence.
148
+ """
149
+ req_str, req_trunc = _truncate(request_body)
150
+ resp_str, resp_trunc = _truncate(response_body)
151
+ return {
152
+ "provider": provider,
153
+ "model": model,
154
+ "content_type": content_type,
155
+ "stream": stream,
156
+ "status_code": status_code,
157
+ "input_tokens": int(input_tokens),
158
+ "output_tokens": int(output_tokens),
159
+ "request": req_str,
160
+ "response": resp_str,
161
+ "request_truncated": req_trunc,
162
+ "response_truncated": resp_trunc,
163
+ }
164
+
165
+
166
+ def extract_usage(provider: str, body: bytes | None) -> tuple[int, int, str]:
167
+ """Best-effort (input_tokens, output_tokens, model) from a provider JSON body.
168
+
169
+ Works on the normalised JSON the SSE parsers emit AND on non-streaming
170
+ upstream JSON. Returns (0, 0, "") when the body is missing/unparseable —
171
+ capture must never fail because usage couldn't be read.
172
+ """
173
+ if not body:
174
+ return 0, 0, ""
175
+ try:
176
+ data = json.loads(body)
177
+ except (json.JSONDecodeError, ValueError, TypeError):
178
+ return 0, 0, ""
179
+ if not isinstance(data, dict):
180
+ return 0, 0, ""
181
+
182
+ if provider == "gemini":
183
+ usage = data.get("usageMetadata") or {}
184
+ return (
185
+ int(usage.get("promptTokenCount", 0) or 0),
186
+ int(usage.get("candidatesTokenCount", 0) or 0),
187
+ str(data.get("modelVersion", "") or ""),
188
+ )
189
+
190
+ usage = data.get("usage") or {}
191
+ if provider == "anthropic":
192
+ return (
193
+ int(usage.get("input_tokens", 0) or 0),
194
+ int(usage.get("output_tokens", 0) or 0),
195
+ str(data.get("model", "") or ""),
196
+ )
197
+ # OpenAI-format providers (explicit allowlist so a future provider variant
198
+ # is not silently parsed with the wrong schema — it warns + returns zeros).
199
+ if provider not in _OPENAI_FORMAT_PROVIDERS:
200
+ logger.warning(
201
+ "capture.extract_usage: unknown provider %r — recording zero tokens", provider
202
+ )
203
+ return 0, 0, str(data.get("model", "") or "")
204
+ return (
205
+ int(usage.get("prompt_tokens", 0) or 0),
206
+ int(usage.get("completion_tokens", 0) or 0),
207
+ str(data.get("model", "") or ""),
208
+ )
209
+
210
+
211
+ def record_exchange(
212
+ *,
213
+ provider: str,
214
+ model: str,
215
+ request_body: bytes,
216
+ response_body: bytes,
217
+ content_type: str = "application/json",
218
+ input_tokens: int = 0,
219
+ output_tokens: int = 0,
220
+ status_code: int = 200,
221
+ stream: bool = False,
222
+ ) -> bool:
223
+ """Build + append a capture entry. Fail-open. Returns True on success."""
224
+ entry = build_entry(
225
+ provider=provider,
226
+ model=model,
227
+ request_body=request_body,
228
+ response_body=response_body,
229
+ content_type=content_type,
230
+ input_tokens=input_tokens,
231
+ output_tokens=output_tokens,
232
+ status_code=status_code,
233
+ stream=stream,
234
+ )
235
+ return ShadowCapture.get_instance().record(entry)
236
+
237
+
238
+ async def record_exchange_async(**kwargs: Any) -> bool:
239
+ """Async wrapper for ``record_exchange`` that offloads the synchronous file
240
+ write to a worker thread so it never blocks the proxy event loop — relevant
241
+ when many streaming responses complete in the same loop iteration.
242
+ """
243
+ return await asyncio.to_thread(lambda: record_exchange(**kwargs))
@@ -47,7 +47,10 @@ from superlocalmemory.optimize.proxy._helpers import (
47
47
  _safe_compress,
48
48
  _stream_and_cache_forward,
49
49
  _stream_forward,
50
+ capture_passthrough_forward,
50
51
  )
52
+ from superlocalmemory.optimize.proxy.capture import capture_enabled
53
+ from superlocalmemory.optimize.proxy.openai_surface import _parse_openai_sse_to_json
51
54
  from superlocalmemory.optimize.proxy.lifecycle import ProviderResponse, ProxyRequest
52
55
 
53
56
  logger = logging.getLogger("slm.optimize.proxy.gemini")
@@ -238,6 +241,24 @@ async def handle_gemini_native(
238
241
  stream = "streamGenerateContent" in model_and_method
239
242
  has_tools = _body_has_tools(body)
240
243
 
244
+ # v3.6.10 shadow-capture (plan §7): pure passthrough + corpus record.
245
+ if capture_enabled():
246
+ cap_model = model_and_method.split(":", 1)[0].replace("models/", "")
247
+ cap_url = upstream_url
248
+ if stream:
249
+ _allowed = {
250
+ k: v for k, v in request.query_params.items()
251
+ if k.lower() in _GEMINI_ALLOWED_QUERY_PARAMS
252
+ }
253
+ _allowed["alt"] = "sse"
254
+ cap_url = f"{upstream_url}?{urllib.parse.urlencode(_allowed)}"
255
+ return await capture_passthrough_forward(
256
+ proxy, request, provider="gemini", upstream_url=cap_url,
257
+ allowed_headers=_GEMINI_NATIVE_FORWARD_HEADERS, request_id=request_id,
258
+ model_hint=cap_model,
259
+ sse_parser=_parse_gemini_sse_to_json, is_stream=stream,
260
+ )
261
+
241
262
  ctx = ProxyRequest(
242
263
  provider="gemini",
243
264
  method="POST",
@@ -407,6 +428,16 @@ async def handle_gemini_openai_compat(proxy: object, request: Request) -> Respon
407
428
  has_tools = _body_has_tools(body)
408
429
  stream = bool(body.get("stream", False))
409
430
 
431
+ # v3.6.10 shadow-capture (plan §7): pure passthrough + corpus record.
432
+ if capture_enabled():
433
+ return await capture_passthrough_forward(
434
+ proxy, request, provider="gemini-openai-compat",
435
+ upstream_url=upstream_url,
436
+ allowed_headers=_GEMINI_OPENAI_COMPAT_FORWARD_HEADERS,
437
+ request_id=request_id, model_hint=str(body.get("model", "")),
438
+ sse_parser=_parse_openai_sse_to_json, is_stream=stream,
439
+ )
440
+
410
441
  ctx = ProxyRequest(
411
442
  provider="gemini-openai-compat",
412
443
  method="POST",
@@ -33,7 +33,9 @@ from superlocalmemory.optimize.proxy._helpers import (
33
33
  _safe_compress,
34
34
  _stream_and_cache_forward,
35
35
  _stream_forward,
36
+ capture_passthrough_forward,
36
37
  )
38
+ from superlocalmemory.optimize.proxy.capture import capture_enabled
37
39
  from superlocalmemory.optimize.proxy.lifecycle import ProviderResponse, ProxyRequest
38
40
 
39
41
  logger = logging.getLogger("slm.optimize.proxy.openai")
@@ -316,6 +318,16 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
316
318
 
317
319
  stream = bool(body.get("stream", False))
318
320
  has_tools = _body_has_tools(body)
321
+
322
+ # v3.6.10 shadow-capture (plan §7): pure passthrough + corpus record.
323
+ if capture_enabled():
324
+ return await capture_passthrough_forward(
325
+ proxy, request, provider="openai", upstream_url=upstream_url,
326
+ allowed_headers=_OPENAI_FORWARD_HEADERS, request_id=request_id,
327
+ model_hint=str(body.get("model", "")),
328
+ sse_parser=_parse_openai_sse_to_json, is_stream=stream,
329
+ )
330
+
319
331
  ctx = ProxyRequest(
320
332
  provider="openai", method="POST", path="/v1/chat/completions",
321
333
  headers=_redact_headers(dict(request.headers)),
@@ -75,6 +75,24 @@ class ProxyApp:
75
75
  self._request_counter += 1
76
76
  return f"slm_{int(time.monotonic() * 1000)}_{self._request_counter:06d}"
77
77
 
78
+ def reload_from_config(self, config: OptimizeConfig) -> None:
79
+ """Hot-swap cache/compress behavior when optimize.json changes (v3.6.10).
80
+
81
+ Called by the ConfigStore change-callback (UI save → immediate; external
82
+ file/CLI edit → within the 2s watchdog poll). Rebuilds the HookChain so
83
+ ``cache_enabled`` and ``compress_enabled`` can be toggled INDEPENDENTLY
84
+ at runtime with no daemon restart. Note: ``proxy_enabled`` (whether the
85
+ proxy claims /v1/* at all) is a startup decision and is NOT changed here.
86
+ """
87
+ self.config = config
88
+ self.hooks = _load_hooks(config)
89
+ logger.info(
90
+ "slm.optimize.proxy reloaded (config v%s): cache=%s compress=%s",
91
+ getattr(config, "config_version", "?"),
92
+ type(self.hooks.cache).__name__ if self.hooks.cache else "None",
93
+ type(self.hooks.compress).__name__ if self.hooks.compress else "None",
94
+ )
95
+
78
96
 
79
97
  def build_proxy_router(proxy: ProxyApp) -> APIRouter:
80
98
  """Build and return the FastAPI router for all proxy surfaces."""
@@ -132,6 +150,17 @@ def build_proxy_router(proxy: ProxyApp) -> APIRouter:
132
150
 
133
151
 
134
152
  def _load_hooks(config: OptimizeConfig) -> HookChain:
153
+ # v3.6.10 shadow-capture (plan §7): capture mode is PURE passthrough — no
154
+ # cache, no compression — so the corpus records only authentic upstream
155
+ # exchanges. This is defense-in-depth alongside the per-surface guard.
156
+ from superlocalmemory.optimize.proxy.capture import capture_enabled
157
+ if capture_enabled():
158
+ logger.info(
159
+ "slm.optimize.proxy: SLM_OPTIMIZE_CAPTURE on — cache/compress "
160
+ "DISABLED, recording exchanges to optimize_capture.jsonl"
161
+ )
162
+ return HookChain.empty()
163
+
135
164
  cache_hook = None
136
165
  compress_hook = None
137
166