superlocalmemory 3.5.8 → 3.6.0

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 (72) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +142 -35
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/src/superlocalmemory/__init__.py +1 -1
  7. package/src/superlocalmemory/cli/cache_cmd.py +198 -0
  8. package/src/superlocalmemory/cli/commands.py +80 -2
  9. package/src/superlocalmemory/cli/compress_cmd.py +179 -0
  10. package/src/superlocalmemory/cli/help_cmd.py +197 -0
  11. package/src/superlocalmemory/cli/main.py +122 -0
  12. package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
  13. package/src/superlocalmemory/cli/optimize_constants.py +31 -0
  14. package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
  15. package/src/superlocalmemory/core/config.py +5 -0
  16. package/src/superlocalmemory/core/engine.py +23 -0
  17. package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
  18. package/src/superlocalmemory/llm/backbone.py +10 -4
  19. package/src/superlocalmemory/mcp/server.py +34 -0
  20. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  21. package/src/superlocalmemory/optimize/NOTICE +11 -0
  22. package/src/superlocalmemory/optimize/__init__.py +0 -0
  23. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  24. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  25. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  26. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  27. package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
  28. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  29. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  30. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  31. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  32. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  33. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  34. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  35. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  36. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  37. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  38. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  39. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  40. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  43. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  44. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  45. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  46. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  47. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  48. package/src/superlocalmemory/optimize/config/store.py +209 -0
  49. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  50. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  51. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  52. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  53. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  54. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  55. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  56. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  57. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  58. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  59. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  60. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  61. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  62. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  63. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  64. package/src/superlocalmemory/server/routes/optimize.py +166 -0
  65. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  66. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  67. package/src/superlocalmemory/ui/index.html +98 -0
  68. package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
  69. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  70. package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
  71. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  72. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,184 @@
1
+ """DDL for llmcache.db — the Optimize module's dedicated SQLite database.
2
+
3
+ ISOLATION GUARANTEE: This schema MUST NEVER reference memory.db tables.
4
+ No FKs to profiles, atomic_facts, memories, or any SLM core table.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sqlite3
10
+ from typing import Final
11
+
12
+ CACHE_SCHEMA_VERSION: Final[int] = 1
13
+
14
+ LLMCACHE_DB_FILENAME: Final[str] = "llmcache.db"
15
+
16
+
17
+ _DDL_STATEMENTS: tuple[str, ...] = (
18
+ """
19
+ CREATE TABLE IF NOT EXISTS llmcache_schema_version (
20
+ version INTEGER NOT NULL,
21
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
22
+ description TEXT NOT NULL DEFAULT ''
23
+ )
24
+ """,
25
+ """
26
+ CREATE TABLE IF NOT EXISTS llmcache_entries (
27
+ entry_id TEXT PRIMARY KEY,
28
+ cache_key TEXT NOT NULL,
29
+ tenant_id TEXT NOT NULL DEFAULT 'default',
30
+ model TEXT NOT NULL DEFAULT '',
31
+ provider TEXT NOT NULL DEFAULT '',
32
+ value_blob BLOB NOT NULL,
33
+ compressed INTEGER NOT NULL DEFAULT 0,
34
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
35
+ last_hit_at TEXT,
36
+ ttl_expires REAL,
37
+ hit_count INTEGER NOT NULL DEFAULT 0,
38
+ byte_size INTEGER NOT NULL DEFAULT 0,
39
+ tag_json TEXT NOT NULL DEFAULT '[]',
40
+ cache_tier TEXT NOT NULL DEFAULT 'exact'
41
+ CHECK (cache_tier IN ('exact', 'semantic'))
42
+ )
43
+ """,
44
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_llmcache_key_tenant ON llmcache_entries (cache_key, tenant_id)",
45
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_ttl ON llmcache_entries (ttl_expires) WHERE ttl_expires IS NOT NULL",
46
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_tenant ON llmcache_entries (tenant_id)",
47
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_provider ON llmcache_entries (tenant_id, provider)",
48
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_lru ON llmcache_entries (last_hit_at)",
49
+ """
50
+ CREATE TABLE IF NOT EXISTS llmcache_semantic_vectors (
51
+ entry_id TEXT PRIMARY KEY,
52
+ tenant_id TEXT NOT NULL DEFAULT 'default',
53
+ vector_blob BLOB NOT NULL,
54
+ vector_dim INTEGER NOT NULL DEFAULT 768,
55
+ model_name TEXT NOT NULL DEFAULT 'nomic-ai/nomic-embed-text-v1.5',
56
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
57
+ )
58
+ """,
59
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_vec_tenant ON llmcache_semantic_vectors (tenant_id)",
60
+ """
61
+ CREATE TABLE IF NOT EXISTS llmcache_ccr_originals (
62
+ ccr_id TEXT PRIMARY KEY,
63
+ tenant_id TEXT NOT NULL DEFAULT 'default',
64
+ original_blob BLOB NOT NULL,
65
+ compressed_hash TEXT NOT NULL,
66
+ byte_size_orig INTEGER NOT NULL DEFAULT 0,
67
+ byte_size_comp INTEGER NOT NULL DEFAULT 0,
68
+ model TEXT NOT NULL DEFAULT '',
69
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
70
+ ttl_expires REAL
71
+ )
72
+ """,
73
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_ccr_hash ON llmcache_ccr_originals (compressed_hash)",
74
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_ccr_tenant ON llmcache_ccr_originals (tenant_id)",
75
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_ccr_ttl ON llmcache_ccr_originals (ttl_expires) WHERE ttl_expires IS NOT NULL",
76
+ """
77
+ CREATE TABLE IF NOT EXISTS llmcache_tags (
78
+ tag TEXT NOT NULL,
79
+ cache_key TEXT NOT NULL,
80
+ tenant_id TEXT NOT NULL DEFAULT 'default',
81
+ PRIMARY KEY (tag, cache_key, tenant_id)
82
+ )
83
+ """,
84
+ "CREATE INDEX IF NOT EXISTS idx_llmcache_tags_tag ON llmcache_tags (tag)",
85
+ """
86
+ CREATE TABLE IF NOT EXISTS llmcache_boundaries (
87
+ entry_id TEXT PRIMARY KEY,
88
+ logistic_t REAL NOT NULL DEFAULT 0.95,
89
+ logistic_gamma REAL NOT NULL DEFAULT 10.0,
90
+ sample_count INTEGER NOT NULL DEFAULT 0,
91
+ updated_at REAL NOT NULL DEFAULT 0
92
+ )
93
+ """,
94
+ """
95
+ CREATE TABLE IF NOT EXISTS llmcache_centroids (
96
+ tenant_id TEXT PRIMARY KEY,
97
+ centroid_blob BLOB NOT NULL,
98
+ n INTEGER NOT NULL DEFAULT 0,
99
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
100
+ )
101
+ """,
102
+ """
103
+ CREATE TABLE IF NOT EXISTS llmcache_metrics (
104
+ id INTEGER PRIMARY KEY CHECK (id = 1),
105
+ hits INTEGER NOT NULL DEFAULT 0,
106
+ misses INTEGER NOT NULL DEFAULT 0,
107
+ calls_skipped INTEGER NOT NULL DEFAULT 0,
108
+ tokens_saved_input INTEGER NOT NULL DEFAULT 0,
109
+ tokens_saved_output INTEGER NOT NULL DEFAULT 0,
110
+ tokens_saved_compress INTEGER NOT NULL DEFAULT 0,
111
+ evictions INTEGER NOT NULL DEFAULT 0,
112
+ latency_overhead_ms_sum REAL NOT NULL DEFAULT 0,
113
+ latency_samples INTEGER NOT NULL DEFAULT 0,
114
+ compress_runs INTEGER NOT NULL DEFAULT 0,
115
+ compress_bytes_original INTEGER NOT NULL DEFAULT 0,
116
+ compress_bytes_after INTEGER NOT NULL DEFAULT 0,
117
+ cache_size_bytes INTEGER NOT NULL DEFAULT 0,
118
+ cache_entry_count INTEGER NOT NULL DEFAULT 0,
119
+ updated_at REAL NOT NULL DEFAULT 0
120
+ )
121
+ """,
122
+ )
123
+
124
+
125
+ def create_all_tables(conn: sqlite3.Connection) -> None:
126
+ """Create all llmcache_* tables, indexes, and seed schema_version.
127
+
128
+ Safe to call repeatedly — all DDL uses IF NOT EXISTS.
129
+ Also seeds the single llmcache_metrics row (id=1) via INSERT OR IGNORE.
130
+ """
131
+ for stmt in _DDL_STATEMENTS:
132
+ conn.execute(stmt)
133
+ conn.execute("INSERT OR IGNORE INTO llmcache_metrics(id) VALUES (1)")
134
+ row = conn.execute(
135
+ "SELECT 1 FROM llmcache_schema_version WHERE version = ?",
136
+ (CACHE_SCHEMA_VERSION,),
137
+ ).fetchone()
138
+ if row is None:
139
+ conn.execute(
140
+ "INSERT INTO llmcache_schema_version (version, description) VALUES (?, ?)",
141
+ (CACHE_SCHEMA_VERSION, ""),
142
+ )
143
+
144
+
145
+ def drop_all_tables(conn: sqlite3.Connection) -> None:
146
+ """Drop all llmcache_* tables. Testing only."""
147
+ for name in get_table_names():
148
+ conn.execute(f"DROP TABLE IF EXISTS {name}")
149
+
150
+
151
+ def get_table_names() -> tuple[str, ...]:
152
+ """Return all table names in creation order."""
153
+ return (
154
+ "llmcache_schema_version",
155
+ "llmcache_entries",
156
+ "llmcache_semantic_vectors",
157
+ "llmcache_ccr_originals",
158
+ "llmcache_tags",
159
+ "llmcache_boundaries",
160
+ "llmcache_centroids",
161
+ "llmcache_metrics",
162
+ )
163
+
164
+
165
+ def assert_no_memory_db_tables(conn: sqlite3.Connection) -> None:
166
+ """Assert that no memory.db tables are present. Raises RuntimeError on violation.
167
+
168
+ Use this after opening a connection to confirm we are NOT talking to memory.db.
169
+ """
170
+ rows = conn.execute(
171
+ "SELECT name FROM sqlite_master WHERE type='table'"
172
+ ).fetchall()
173
+ table_names = {r[0] for r in rows}
174
+ forbidden = {
175
+ "memories", "atomic_facts", "profiles", "canonical_entities",
176
+ "entity_aliases", "consolidation_log", "trust_scores", "bm25_tokens",
177
+ "fact_retention", "core_memory_blocks", "ccq_consolidated_blocks",
178
+ }
179
+ found = forbidden & table_names
180
+ if found:
181
+ raise RuntimeError(
182
+ f"ISOLATION VIOLATION: llmcache.db contains memory.db tables: {sorted(found)}. "
183
+ "This means the wrong database file was opened. Aborting."
184
+ )
@@ -0,0 +1,166 @@
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
+ """Optimize API routes — single canonical route file (INTERFACE-CONTRACT v2 §5 R2-01).
6
+
7
+ All optimize endpoints live here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any, Literal
14
+
15
+ from fastapi import APIRouter, HTTPException
16
+ from pydantic import BaseModel
17
+
18
+ logger = logging.getLogger("superlocalmemory.server.routes.optimize")
19
+
20
+ router = APIRouter(prefix="/api/optimize", tags=["optimize"])
21
+
22
+ # F-03 fix: module-level singleton — constructed once, shared by /savings + CLI
23
+ from superlocalmemory.optimize.metrics.estimator import SavingsEstimator as _SavingsEstimator # noqa: E402
24
+ _savings_estimator = _SavingsEstimator()
25
+
26
+
27
+ class ConfigUpdateRequest(BaseModel):
28
+ """Partial config update — only provided fields are changed."""
29
+ enabled: bool | None = None
30
+ cache_enabled: bool | None = None
31
+ semantic_enabled: bool | None = None
32
+ compress_enabled: bool | None = None
33
+ compress_mode: Literal['safe', 'aggressive'] | None = None
34
+ compress_code: bool | None = None
35
+ compress_prose: bool | None = None
36
+ compress_ccr: bool | None = None
37
+
38
+
39
+ @router.get("/config")
40
+ async def get_config() -> dict[str, Any]:
41
+ """Return current optimize config as JSON."""
42
+ try:
43
+ from superlocalmemory.optimize.config.store import ConfigStore
44
+ cfg = ConfigStore().get()
45
+ return cfg.as_dict()
46
+ except Exception as exc:
47
+ logger.warning("GET /api/optimize/config failed: %s", exc)
48
+ raise HTTPException(status_code=500, detail=str(exc))
49
+
50
+
51
+ @router.put("/config")
52
+ async def put_config(body: ConfigUpdateRequest) -> dict[str, Any]:
53
+ """Update optimize config (partial). Daemon hot-reloads within 2s."""
54
+ try:
55
+ import dataclasses
56
+ from superlocalmemory.optimize.config.store import ConfigStore
57
+ store = ConfigStore()
58
+ cfg = store.get()
59
+ updates: dict[str, Any] = {}
60
+ for field_name in ConfigUpdateRequest.model_fields:
61
+ val = getattr(body, field_name, None)
62
+ if val is not None:
63
+ updates[field_name] = val
64
+ if updates:
65
+ cfg = dataclasses.replace(cfg, **updates)
66
+ store.save(cfg)
67
+ return {"status": "ok", "updated": list(updates.keys())}
68
+ except ValueError as exc:
69
+ raise HTTPException(status_code=400, detail=str(exc))
70
+ except Exception as exc:
71
+ logger.warning("PUT /api/optimize/config failed: %s", exc)
72
+ raise HTTPException(status_code=500, detail=str(exc))
73
+
74
+
75
+ @router.get("/savings")
76
+ async def get_savings() -> dict[str, Any]:
77
+ """Return savings estimates from live metrics + CacheDB.
78
+
79
+ Field names match INTERFACE-CONTRACT §5 exactly.
80
+ """
81
+ try:
82
+ from superlocalmemory.optimize.config.store import ConfigStore
83
+ from superlocalmemory.optimize.metrics.counters import get_metrics
84
+ from superlocalmemory.optimize.storage.db import CacheDB
85
+
86
+ cfg = ConfigStore().get()
87
+ collector = get_metrics()
88
+ db = CacheDB()
89
+ snap = collector.snapshot(
90
+ cache_size_bytes=db.db_size_bytes(),
91
+ cache_entry_count=db.entry_count(),
92
+ )
93
+
94
+ # F-03 fix: use module-level singleton instead of per-request instantiation
95
+ active_model = getattr(cfg, "active_model", "anthropic")
96
+ # Determine provider from model
97
+ provider = "anthropic"
98
+ for key in ("anthropic", "openai", "gemini"):
99
+ if key in active_model.lower():
100
+ provider = key
101
+ break
102
+ est = _savings_estimator.estimate(snap, provider=provider)
103
+
104
+ # compress_ratio from snap (byte-based, survives restart)
105
+ if snap.compress_bytes_original > 0:
106
+ compress_ratio = snap.compress_bytes_after / snap.compress_bytes_original
107
+ else:
108
+ compress_ratio = None
109
+
110
+ total = snap.hits + snap.misses
111
+ hit_rate = snap.hits / total if total > 0 else 0.0
112
+
113
+ # F-02 fix: response keys match INTERFACE-CONTRACT §5 exactly
114
+ return {
115
+ "tokens_saved_input": snap.tokens_saved_input,
116
+ "tokens_saved_output": snap.tokens_saved_output,
117
+ "calls_skipped": snap.calls_skipped,
118
+ "compress_ratio": round(compress_ratio, 4) if compress_ratio is not None else None,
119
+ "cost_saved": {"usd": est["usd"], "inr": est["inr"]},
120
+ "hit_rate": round(hit_rate, 4),
121
+ "cache_bytes": snap.cache_size_bytes,
122
+ "entries": snap.cache_entry_count,
123
+ # diagnostic fields — non-conflicting extras
124
+ "hits": snap.hits,
125
+ "misses": snap.misses,
126
+ "tokens_saved_compress": snap.tokens_saved_compress,
127
+ "is_stale": est["is_stale"],
128
+ "pricing_date": est["pricing_date"],
129
+ }
130
+ except Exception as exc:
131
+ logger.warning("GET /api/optimize/savings failed: %s", exc)
132
+ raise HTTPException(status_code=500, detail=str(exc))
133
+
134
+
135
+ @router.get("/stats")
136
+ async def get_stats() -> dict[str, Any]:
137
+ """Return raw metrics snapshot as JSON."""
138
+ try:
139
+ from superlocalmemory.optimize.metrics.counters import get_metrics
140
+ from superlocalmemory.optimize.storage.db import CacheDB
141
+
142
+ collector = get_metrics()
143
+ db = CacheDB()
144
+ snap = collector.snapshot(
145
+ cache_size_bytes=db.db_size_bytes(),
146
+ cache_entry_count=db.entry_count(),
147
+ )
148
+ # Return all 16 fields as dict
149
+ import dataclasses
150
+ return dataclasses.asdict(snap)
151
+ except Exception as exc:
152
+ logger.warning("GET /api/optimize/stats failed: %s", exc)
153
+ raise HTTPException(status_code=500, detail=str(exc))
154
+
155
+
156
+ @router.delete("/cache/clear")
157
+ async def delete_cache_clear(tenant: str = "default") -> dict[str, Any]:
158
+ """Delete all cache entries for a tenant."""
159
+ try:
160
+ from superlocalmemory.optimize.storage.db import CacheDB
161
+ db = CacheDB()
162
+ deleted = db.clear_tenant(tenant)
163
+ return {"success": True, "deleted": deleted}
164
+ except Exception as exc:
165
+ logger.warning("DELETE /api/optimize/cache/clear failed: %s", exc)
166
+ raise HTTPException(status_code=500, detail=str(exc))
@@ -365,6 +365,51 @@ async def test_embedding_endpoint(request: Request):
365
365
  return {"success": False, "error": type(e).__name__}
366
366
 
367
367
 
368
+ @router.get("/embed/ping")
369
+ async def embed_ping():
370
+ """V3.5.9: Liveness probe for McpEmbedderProxy. Returns 200 when daemon
371
+ embedder is ready so the proxy knows the daemon is reachable."""
372
+ try:
373
+ from .helpers import get_engine_lazy
374
+ # We just need to confirm the route is alive — engine check is optional
375
+ return {"ok": True}
376
+ except Exception as e:
377
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=503)
378
+
379
+
380
+ @router.post("/embed")
381
+ async def embed_texts(request: Request):
382
+ """V3.5.9: Embed texts via daemon's FULL engine for McpEmbedderProxy.
383
+
384
+ MCP processes run LIGHT engines (no ONNX worker). This endpoint lets them
385
+ delegate embed_batch() to the daemon's single real embedder — one ONNX
386
+ worker total across all sessions (fixes PR #30 NULL embedder bug).
387
+ """
388
+ import asyncio
389
+ try:
390
+ body = await request.json()
391
+ texts = body.get("texts", [])
392
+ if not texts:
393
+ return {"embeddings": []}
394
+
395
+ from .helpers import get_engine_lazy
396
+ engine = get_engine_lazy(request.app.state)
397
+ if engine is None or engine._embedder is None:
398
+ return JSONResponse(
399
+ {"error": "Embedder not available in daemon"},
400
+ status_code=503,
401
+ )
402
+
403
+ loop = asyncio.get_event_loop()
404
+ embeddings = await loop.run_in_executor(
405
+ None,
406
+ lambda: engine._embedder.embed_batch(texts),
407
+ )
408
+ return {"embeddings": embeddings}
409
+ except Exception as e:
410
+ return JSONResponse({"error": str(e)}, status_code=500)
411
+
412
+
368
413
  @router.post("/provider/test")
369
414
  async def test_provider(request: Request):
370
415
  """Test connectivity to an LLM provider."""
@@ -400,8 +445,25 @@ async def test_provider(request: Request):
400
445
  return {"success": True, "message": "OpenRouter connected, key valid"}
401
446
 
402
447
  if provider == "openai":
448
+ # V3.5.9: custom/local endpoint — api_key is optional (llama.cpp, LM Studio etc.)
449
+ custom_endpoint = body.get("base_url", "").strip() or body.get("endpoint", "").strip()
450
+ if custom_endpoint:
451
+ headers_test = {"Content-Type": "application/json"}
452
+ if api_key:
453
+ headers_test["Authorization"] = f"Bearer {api_key}"
454
+ base = custom_endpoint.rstrip("/")
455
+ if not base.endswith("chat/completions"):
456
+ base = f"{base}/chat/completions"
457
+ with httpx.Client(timeout=httpx.Timeout(10.0)) as c:
458
+ # Probe with a minimal chat request (models list not universal on local servers)
459
+ probe = {"model": body.get("model", "test"), "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}
460
+ resp = c.post(base, headers=headers_test, json=probe)
461
+ if resp.status_code in (200, 400, 422):
462
+ return {"success": True, "message": f"Custom endpoint reachable (HTTP {resp.status_code})"}
463
+ resp.raise_for_status()
464
+ return {"success": True, "message": "Custom OpenAI-compatible endpoint connected"}
403
465
  if not api_key:
404
- return {"success": False, "error": "API key required"}
466
+ return {"success": False, "error": "API key required for official OpenAI endpoint"}
405
467
  with httpx.Client(timeout=httpx.Timeout(10.0)) as c:
406
468
  resp = c.get("https://api.openai.com/v1/models", headers={"Authorization": f"Bearer {api_key}"})
407
469
  resp.raise_for_status()
@@ -824,8 +824,76 @@ async def lifespan(application: FastAPI):
824
824
  )
825
825
  logger.info(_ready_msg)
826
826
 
827
+ # Start optimize proxy httpx client if mounted
828
+ try:
829
+ _opt_proxy = getattr(application.state, "optimize_proxy", None)
830
+ if _opt_proxy is not None:
831
+ await _opt_proxy.startup()
832
+ except Exception as _exc: # pragma: no cover — defensive
833
+ logger.warning("optimize_proxy startup failed (non-fatal): %s", _exc)
834
+
835
+ # V3.6: Mount optimize API routes + restore persisted metrics + start flush loop
836
+ try:
837
+ from superlocalmemory.server.routes.optimize import router as optimize_router
838
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector
839
+ from superlocalmemory.optimize.metrics.persistence import MetricsPersistence
840
+ from superlocalmemory.optimize.storage.db import CacheDB
841
+ application.include_router(optimize_router)
842
+
843
+ # Restore persisted metrics counters on startup
844
+ MetricsPersistence().load(MetricsCollector.get_instance(), CacheDB())
845
+
846
+ # Periodic flush — every 60s (OPT-005: guard + task ref for shutdown)
847
+ _metrics_flush_task = getattr(application.state, "_optimize_flush_task", None)
848
+ if _metrics_flush_task is None or _metrics_flush_task.done():
849
+ async def _metrics_flush_loop():
850
+ while True:
851
+ await asyncio.sleep(60)
852
+ try:
853
+ MetricsPersistence().flush(
854
+ MetricsCollector.get_instance(), CacheDB()
855
+ )
856
+ except Exception as e:
857
+ logger.warning("metrics flush error: %s", e)
858
+ application.state._optimize_flush_task = asyncio.create_task(
859
+ _metrics_flush_loop()
860
+ )
861
+ except Exception as e:
862
+ logger.warning("optimize module not available: %s", e)
863
+
827
864
  yield
828
865
 
866
+ # Cancel optimize metrics flush loop + run final flush before shutdown
867
+ try:
868
+ _flush_task = getattr(application.state, "_optimize_flush_task", None)
869
+ if _flush_task is not None and not _flush_task.done():
870
+ _flush_task.cancel()
871
+ try:
872
+ await _flush_task
873
+ except asyncio.CancelledError:
874
+ pass
875
+ # Final flush to persist the last window (H-04: use singleton)
876
+ try:
877
+ from superlocalmemory.optimize.metrics.persistence import MetricsPersistence
878
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector
879
+ from superlocalmemory.optimize.storage.db import CacheDB as _FinalCacheDB
880
+ MetricsPersistence().flush(
881
+ MetricsCollector.get_instance(),
882
+ _FinalCacheDB.get_default(),
883
+ )
884
+ except Exception:
885
+ pass
886
+ except Exception: # pragma: no cover — defensive
887
+ pass
888
+
889
+ # Shutdown optimize proxy httpx client (symmetric with startup)
890
+ try:
891
+ _opt_proxy = getattr(application.state, "optimize_proxy", None)
892
+ if _opt_proxy is not None:
893
+ await _opt_proxy.shutdown()
894
+ except Exception as _exc: # pragma: no cover — defensive
895
+ logger.warning("optimize_proxy shutdown failed (non-fatal): %s", _exc)
896
+
829
897
  # S9-W4 C2: symmetric shutdown. Prior version only flushed the
830
898
  # observe-buffer + signal_worker + engine. The following long-lived
831
899
  # subsystems lived on ``application.state`` but were never
@@ -1062,6 +1130,33 @@ def create_app() -> FastAPI:
1062
1130
  except ImportError as exc: # pragma: no cover — defensive wiring
1063
1131
  logger.warning("token router not wired: %s", exc)
1064
1132
 
1133
+ # ── Optimize proxy (optional, fail-open) ──────────────────────────────
1134
+ # Mounts on the existing daemon port 8765. Proxy routes carry provider
1135
+ # API keys (x-api-key, Authorization), NOT the SLM API key. Auth-exempt
1136
+ # path prefixes are configured below in the auth_middleware block.
1137
+ try:
1138
+ from superlocalmemory.optimize.config.store import ConfigStore
1139
+ from superlocalmemory.optimize.proxy.server import ProxyApp, build_proxy_router
1140
+
1141
+ _opt_cfg = ConfigStore().get()
1142
+ if _opt_cfg.proxy_enabled:
1143
+ _proxy = ProxyApp(config=_opt_cfg)
1144
+ application.state.optimize_proxy = _proxy
1145
+ _proxy_router = build_proxy_router(_proxy)
1146
+ # prefix="" — proxy claims /v1/*, /v1beta/* directly.
1147
+ application.include_router(_proxy_router, prefix="")
1148
+ logger.info(
1149
+ "optimize.proxy mounted on /v1/*, /v1beta/* port=8765"
1150
+ )
1151
+ else:
1152
+ application.state.optimize_proxy = None
1153
+ except ImportError:
1154
+ application.state.optimize_proxy = None
1155
+ logger.debug("optimize.proxy not installed — skipping")
1156
+ except Exception as _exc: # pragma: no cover — defensive
1157
+ application.state.optimize_proxy = None
1158
+ logger.warning("optimize.proxy mount failed (non-fatal): %s", _exc)
1159
+
1065
1160
  # -- Daemon-specific routes --
1066
1161
  _register_daemon_routes(application)
1067
1162
 
@@ -1113,8 +1208,18 @@ def _register_dashboard_routes(application: FastAPI) -> None:
1113
1208
  try:
1114
1209
  from superlocalmemory.infra.auth_middleware import check_api_key
1115
1210
 
1211
+ # Auth-exempt path prefixes — proxy routes carry provider API keys
1212
+ # (x-api-key for Anthropic, Authorization: Bearer for OpenAI, x-goog-api-key
1213
+ # for Gemini), never X-SLM-API-Key. Verified: auth_middleware.py:50-82
1214
+ # returns False for POST when api_key file exists and X-SLM-API-Key
1215
+ # is absent.
1216
+ _AUTH_EXEMPT_PREFIXES = ("/v1/", "/v1beta/")
1217
+
1116
1218
  @application.middleware("http")
1117
1219
  async def auth_middleware(request, call_next):
1220
+ # Exempt proxy paths — LLM clients carry provider keys, not SLM keys.
1221
+ if request.url.path.startswith(_AUTH_EXEMPT_PREFIXES):
1222
+ return await call_next(request)
1118
1223
  is_write = request.method in ("POST", "PUT", "DELETE", "PATCH")
1119
1224
  headers = dict(request.headers)
1120
1225
  if not check_api_key(headers, is_write=is_write):
@@ -1233,6 +1233,77 @@
1233
1233
  </div>
1234
1234
  </div>
1235
1235
  </div>
1236
+
1237
+ <!-- Optimize Tab -->
1238
+ <div class="tab-pane fade" id="optimize-pane">
1239
+ <div class="card mb-3">
1240
+ <div class="card-header bg-success text-white">
1241
+ <h6 class="mb-0"><i class="bi bi-speedometer"></i> Optimize Module</h6>
1242
+ </div>
1243
+ <div class="card-body">
1244
+ <div class="row g-3">
1245
+ <div class="col-md-6">
1246
+ <h6>Controls</h6>
1247
+ <div class="form-check form-switch mb-2">
1248
+ <input class="form-check-input" type="checkbox" id="opt-enabled">
1249
+ <label class="form-check-label" for="opt-enabled">Optimize Enabled</label>
1250
+ </div>
1251
+ <div class="form-check form-switch mb-2">
1252
+ <input class="form-check-input" type="checkbox" id="opt-cache-enabled">
1253
+ <label class="form-check-label" for="opt-cache-enabled">Cache Enabled</label>
1254
+ </div>
1255
+ <div class="form-check form-switch mb-2">
1256
+ <input class="form-check-input" type="checkbox" id="opt-semantic-enabled">
1257
+ <label class="form-check-label" for="opt-semantic-enabled">Semantic Cache</label>
1258
+ </div>
1259
+ <div class="form-check form-switch mb-2">
1260
+ <input class="form-check-input" type="checkbox" id="opt-compress-enabled">
1261
+ <label class="form-check-label" for="opt-compress-enabled">Compression</label>
1262
+ </div>
1263
+ <div class="mb-2">
1264
+ <label for="opt-compress-mode" class="form-label">Compression Mode</label>
1265
+ <select class="form-select form-select-sm" id="opt-compress-mode">
1266
+ <option value="safe">Safe</option>
1267
+ <option value="aggressive">Aggressive</option>
1268
+ </select>
1269
+ </div>
1270
+ <div class="form-check form-switch mb-2">
1271
+ <input class="form-check-input" type="checkbox" id="opt-compress-code">
1272
+ <label class="form-check-label" for="opt-compress-code">Code Compression</label>
1273
+ </div>
1274
+ <div class="form-check form-switch mb-2">
1275
+ <input class="form-check-input" type="checkbox" id="opt-compress-prose">
1276
+ <label class="form-check-label" for="opt-compress-prose">Prose Compression</label>
1277
+ </div>
1278
+ <div class="form-check form-switch mb-2">
1279
+ <input class="form-check-input" type="checkbox" id="opt-compress-ccr">
1280
+ <label class="form-check-label" for="opt-compress-ccr">CCR</label>
1281
+ </div>
1282
+ <div class="form-check form-switch mb-2">
1283
+ <input class="form-check-input" type="checkbox" id="opt-compress-align">
1284
+ <label class="form-check-label" for="opt-compress-align">Alignment Compression</label>
1285
+ </div>
1286
+ </div>
1287
+ <div class="col-md-6">
1288
+ <h6>Savings</h6>
1289
+ <table class="table table-sm">
1290
+ <tr><td>Tokens Saved</td><td id="opt-tokens-saved">-</td></tr>
1291
+ <tr><td>USD Saved</td><td id="opt-usd-saved">-</td></tr>
1292
+ <tr><td>INR Saved</td><td id="opt-inr-saved">-</td></tr>
1293
+ <tr><td>Hit Rate</td><td id="opt-hit-rate">-</td></tr>
1294
+ <tr><td>Cache Entries</td><td id="opt-cache-entries">-</td></tr>
1295
+ <tr><td>Cache Size</td><td id="opt-cache-size">-</td></tr>
1296
+ <tr><td>Compression Ratio</td><td id="opt-compression-ratio">-</td></tr>
1297
+ <tr><td>Pricing Date</td><td id="opt-pricing-date">-</td></tr>
1298
+ </table>
1299
+ <div class="small text-muted">Config version: <span id="opt-config-version">-</span></div>
1300
+ <div id="opt-stale-warning" class="text-warning small mt-1"></div>
1301
+ <button class="btn btn-sm btn-outline-primary mt-2" id="opt-copy-url">Copy URL</button>
1302
+ </div>
1303
+ </div>
1304
+ </div>
1305
+ </div>
1306
+ </div>
1236
1307
  </div>
1237
1308
  </div>
1238
1309
 
@@ -1329,6 +1400,33 @@
1329
1400
  <script src="static/js/ng-skills.js?v=3411"></script>
1330
1401
  <script src="static/js/ng-mesh.js?v=345"></script>
1331
1402
  <script src="static/js/ng-shell.js?v=34421g"></script>
1403
+ <!-- Optimize tab (v3.6.0) — runtime config for cache + compress + proxy -->
1404
+ <script src="static/js/optimize.js?v=360"></script>
1405
+
1406
+ <!-- Aggressive compression warning modal (v3.6.0 M-03) -->
1407
+ <div class="modal fade" id="optimizeAggressiveModal" tabindex="-1" aria-labelledby="optimizeAggressiveModalLabel" aria-hidden="true">
1408
+ <div class="modal-dialog modal-dialog-centered">
1409
+ <div class="modal-content" style="background:var(--ng-bg-elevated);border:1px solid var(--ng-status-warning-bg);">
1410
+ <div class="modal-header" style="border-bottom:1px solid var(--ng-border-subtle);">
1411
+ <h5 class="modal-title" id="optimizeAggressiveModalLabel" style="color:var(--ng-status-warning);">
1412
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>Aggressive Compression Warning
1413
+ </h5>
1414
+ </div>
1415
+ <div class="modal-body" style="color:var(--ng-text-secondary);">
1416
+ <p><strong style="color:var(--ng-text-primary);">Aggressive mode may reduce output fidelity.</strong></p>
1417
+ <ul>
1418
+ <li>Do NOT use for: code generation, legal text, exact-output tasks, math.</li>
1419
+ <li>Safe for: summarization, brainstorming, open-ended chat.</li>
1420
+ </ul>
1421
+ <p class="mb-0">Switch to Aggressive mode anyway?</p>
1422
+ </div>
1423
+ <div class="modal-footer" style="border-top:1px solid var(--ng-border-subtle);">
1424
+ <button type="button" class="btn btn-outline-secondary" id="optimize-aggressive-cancel">Keep Safe Mode</button>
1425
+ <button type="button" class="btn btn-warning" id="optimize-aggressive-confirm">I Understand — Use Aggressive</button>
1426
+ </div>
1427
+ </div>
1428
+ </div>
1429
+ </div>
1332
1430
 
1333
1431
  <footer style="text-align:center; padding:24px; border-top:1px solid var(--bs-border-color); font-size:0.8125rem;">
1334
1432
  <div style="display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:8px;">