superlocalmemory 3.5.7 → 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 (73) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +40 -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 +100 -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/infra/process_reaper.py +12 -1
  19. package/src/superlocalmemory/llm/backbone.py +10 -4
  20. package/src/superlocalmemory/mcp/server.py +34 -0
  21. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  22. package/src/superlocalmemory/optimize/NOTICE +11 -0
  23. package/src/superlocalmemory/optimize/__init__.py +0 -0
  24. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  25. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  26. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  27. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  28. package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
  29. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  30. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  31. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  32. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  33. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  34. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  35. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  36. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  37. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  38. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  39. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  40. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  41. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  43. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  44. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  45. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  46. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  47. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  48. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  49. package/src/superlocalmemory/optimize/config/store.py +209 -0
  50. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  51. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  52. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  53. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  54. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  55. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  56. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  57. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  58. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  59. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  60. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  61. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  62. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  63. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  64. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  65. package/src/superlocalmemory/server/routes/optimize.py +166 -0
  66. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  67. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  68. package/src/superlocalmemory/ui/index.html +98 -0
  69. package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
  70. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  71. package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
  72. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  73. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,90 @@
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
+ # ATTRIBUTION: PricingRegistry + stale-detection pattern adapted from:
6
+ # headroom/pricing/registry.py (Apache-2.0)
7
+
8
+ """Savings estimator — converts MetricsSnapshot to dollar/rupee savings."""
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from datetime import date, timedelta
14
+ from typing import Any
15
+
16
+ from superlocalmemory.optimize.storage.db import MetricsSnapshot
17
+
18
+ logger = logging.getLogger("superlocalmemory.optimize.metrics.estimator")
19
+
20
+ # DATED: 2026-06-07 — prices verified from official pages on this date.
21
+ BUILTIN_PRICING_TABLE: dict[str, dict[str, float]] = {
22
+ "anthropic": {"input_per_1m_usd": 3.00, "output_per_1m_usd": 15.00},
23
+ "openai": {"input_per_1m_usd": 2.50, "output_per_1m_usd": 10.00},
24
+ "gemini": {"input_per_1m_usd": 1.25, "output_per_1m_usd": 10.00},
25
+ }
26
+
27
+ _PRICING_STALE_DAYS: int = 90
28
+
29
+
30
+ class SavingsEstimator:
31
+ """Converts MetricsSnapshot to dollar/rupee savings. Daemon-scoped singleton.
32
+
33
+ Pricing (June 2026 — dated defaults, user-configurable):
34
+ anthropic: $3.00/M input tokens
35
+ openai: $2.50/M input tokens
36
+ gemini: $1.25/M input tokens
37
+
38
+ CRITICAL pricing rule (INTERFACE-CONTRACT §7):
39
+ Cache SKIP saves BOTH input+output tokens (whole call avoided).
40
+ Compression saves INPUT tokens ONLY (output tokens not compressed).
41
+ NEVER apply output-token pricing to compression savings.
42
+
43
+ INR conversion: 83.5 (hardcoded, configurable).
44
+ Stale detection: if pricing data is > 90 days old → include is_stale=True in output.
45
+ """
46
+
47
+ INR_RATE: float = 83.5
48
+ _PRICING_DATE: str = "2026-06-07"
49
+
50
+ def estimate(self, snap: MetricsSnapshot, provider: str = "anthropic") -> dict[str, Any]:
51
+ """Compute savings from a MetricsSnapshot.
52
+
53
+ Returns:
54
+ dict with keys: usd, inr, tokens_saved_total, cache_tokens,
55
+ compress_tokens, is_stale, pricing_date
56
+ """
57
+ provider_key = provider if provider in BUILTIN_PRICING_TABLE else "anthropic"
58
+ rates = BUILTIN_PRICING_TABLE[provider_key]
59
+ input_rate = rates["input_per_1m_usd"]
60
+ output_rate = rates["output_per_1m_usd"]
61
+
62
+ cache_tokens = snap.tokens_saved_input + snap.tokens_saved_output
63
+ compress_tokens = snap.tokens_saved_compress
64
+ tokens_saved_total = cache_tokens + compress_tokens
65
+
66
+ # Cache skip: input + output tokens saved (whole call avoided)
67
+ savings_cache_usd = (snap.tokens_saved_input / 1_000_000) * input_rate + (snap.tokens_saved_output / 1_000_000) * output_rate
68
+ # Compression: input tokens only
69
+ savings_compress_usd = (compress_tokens / 1_000_000) * input_rate
70
+ total_usd = savings_cache_usd + savings_compress_usd
71
+
72
+ is_stale = self._is_stale()
73
+
74
+ return {
75
+ "usd": round(total_usd, 6),
76
+ "inr": round(total_usd * self.INR_RATE, 4),
77
+ "tokens_saved_total": tokens_saved_total,
78
+ "cache_tokens": cache_tokens,
79
+ "compress_tokens": compress_tokens,
80
+ "is_stale": is_stale,
81
+ "pricing_date": self._PRICING_DATE,
82
+ }
83
+
84
+ def _is_stale(self) -> bool:
85
+ """Return True if pricing data is older than _PRICING_STALE_DAYS."""
86
+ try:
87
+ table_date = date.fromisoformat(self._PRICING_DATE)
88
+ return (date.today() - table_date) > timedelta(days=_PRICING_STALE_DAYS)
89
+ except ValueError:
90
+ return True
@@ -0,0 +1,77 @@
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
+ """Optional Prometheus/OpenTelemetry export. Guarded by try/except — no hard dependency."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger("superlocalmemory.optimize.metrics.exporters")
13
+
14
+
15
+ def get_prometheus_exporter() -> Any | None:
16
+ """Return a Prometheus exporter if prometheus_client is installed, else None.
17
+
18
+ Prometheus port: 9091 (NOT 8765, NOT 8766).
19
+ """
20
+ try:
21
+ from prometheus_client import Counter, Gauge, start_http_server # noqa: F401
22
+ return _PrometheusExporter()
23
+ except ImportError:
24
+ return None
25
+
26
+
27
+ class _PrometheusExporter:
28
+ """Thin wrapper around prometheus_client for SLM Optimize metrics."""
29
+
30
+ def __init__(self) -> None:
31
+ from prometheus_client import Counter, Gauge, start_http_server
32
+
33
+ self._cache_hits = Counter(
34
+ "slm_cache_hits_total", "Total cache hits"
35
+ )
36
+ self._cache_misses = Counter(
37
+ "slm_cache_misses_total", "Total cache misses"
38
+ )
39
+ self._tokens_saved = Counter(
40
+ "slm_tokens_saved_total", "Total tokens saved (cache + compress)"
41
+ )
42
+ self._compress_runs = Counter(
43
+ "slm_compress_runs_total", "Total compression runs"
44
+ )
45
+ self._cache_size = Gauge(
46
+ "slm_cache_size_bytes", "Cache size in bytes"
47
+ )
48
+ self._started = False
49
+
50
+ def start(self, port: int = 9091) -> None:
51
+ """Start Prometheus HTTP server on the given port."""
52
+ if self._started:
53
+ return
54
+ try:
55
+ from prometheus_client import start_http_server
56
+ start_http_server(port)
57
+ self._started = True
58
+ logger.info("Prometheus exporter started on port %d", port)
59
+ except Exception as exc:
60
+ logger.warning("Failed to start Prometheus exporter: %s", exc)
61
+
62
+ def update(self, snapshot: dict[str, Any]) -> None:
63
+ """Update Prometheus gauges/counters from a metrics snapshot dict."""
64
+ if not self._started:
65
+ return
66
+ try:
67
+ self._cache_hits._value.set(snapshot.get("hits", 0))
68
+ self._cache_misses._value.set(snapshot.get("misses", 0))
69
+ self._tokens_saved._value.set(
70
+ snapshot.get("tokens_saved_input", 0)
71
+ + snapshot.get("tokens_saved_output", 0)
72
+ + snapshot.get("tokens_saved_compress", 0)
73
+ )
74
+ self._compress_runs._value.set(snapshot.get("compress_runs", 0))
75
+ self._cache_size.set(snapshot.get("cache_size_bytes", 0))
76
+ except Exception as exc:
77
+ logger.debug("Prometheus update failed: %s", exc)
@@ -0,0 +1,115 @@
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
+ """Flush in-memory counters to llmcache.db. Load on startup to restore counters."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import threading
11
+ import time
12
+ from typing import TYPE_CHECKING
13
+
14
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector
15
+ from superlocalmemory.optimize.storage.db import CacheDB, MetricsSnapshot
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+ logger = logging.getLogger("superlocalmemory.optimize.metrics.persistence")
21
+
22
+ _FLUSH_INTERVAL_SECONDS: float = 60.0
23
+
24
+
25
+ class MetricsPersistence:
26
+ """Flush in-memory counters -> llmcache.db. Load on startup -> restore counters.
27
+
28
+ Table: llmcache_metrics (16 columns — INTERFACE-CONTRACT §6).
29
+ Single aggregate row (id=1). INSERT OR REPLACE pattern.
30
+ NEVER reads or writes memory.db.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self._thread: threading.Thread | None = None
35
+ self._stop_event = threading.Event()
36
+
37
+ def flush(self, collector: MetricsCollector, db: CacheDB) -> None:
38
+ """Flush current collector state to llmcache_metrics via CacheDB."""
39
+ try:
40
+ try:
41
+ cache_size_bytes = db.db_size_bytes()
42
+ except Exception:
43
+ cache_size_bytes = 0
44
+ try:
45
+ cache_entry_count = db.entry_count()
46
+ except Exception:
47
+ cache_entry_count = 0
48
+ snap = collector.snapshot(
49
+ cache_size_bytes=cache_size_bytes,
50
+ cache_entry_count=cache_entry_count,
51
+ )
52
+ db.metrics_flush(snap)
53
+ except Exception as exc:
54
+ logger.warning("MetricsPersistence.flush failed: %s", exc)
55
+
56
+ def load(self, collector: MetricsCollector, db: CacheDB) -> None:
57
+ """Load persisted counters from llmcache_metrics into the live collector.
58
+
59
+ Runs at daemon startup to restore counters after restart.
60
+ """
61
+ try:
62
+ snap = db.metrics_load()
63
+ if snap.hits > 0 or snap.misses > 0:
64
+ # Restore counters from persisted snapshot
65
+ with collector._data_lock:
66
+ collector._hits = snap.hits
67
+ collector._misses = snap.misses
68
+ collector._calls_skipped = snap.calls_skipped
69
+ collector._tokens_saved_input = snap.tokens_saved_input
70
+ collector._tokens_saved_output = snap.tokens_saved_output
71
+ collector._tokens_saved_compress = snap.tokens_saved_compress
72
+ collector._evictions = snap.evictions
73
+ collector._latency_overhead_ms_sum = snap.latency_overhead_ms_sum
74
+ collector._latency_samples = snap.latency_samples
75
+ collector._compress_runs = snap.compress_runs
76
+ collector._compress_bytes_original = snap.compress_bytes_original
77
+ collector._compress_bytes_after = snap.compress_bytes_after
78
+ collector._cache_size_bytes = snap.cache_size_bytes
79
+ collector._cache_entry_count = snap.cache_entry_count
80
+ logger.info(
81
+ "MetricsPersistence: restored %d hits, %d misses from llmcache.db",
82
+ snap.hits, snap.misses,
83
+ )
84
+ except Exception as exc:
85
+ logger.warning("MetricsPersistence.load failed: %s", exc)
86
+
87
+ def start_background_flush(
88
+ self, collector: MetricsCollector, db: CacheDB
89
+ ) -> None:
90
+ """Start a background thread that flushes every 60s."""
91
+ if self._thread is not None and self._thread.is_alive():
92
+ return
93
+ self._stop_event.clear()
94
+
95
+ def _loop() -> None:
96
+ while not self._stop_event.is_set():
97
+ if self._stop_event.wait(timeout=_FLUSH_INTERVAL_SECONDS):
98
+ return
99
+ try:
100
+ self.flush(collector, db)
101
+ except Exception as exc:
102
+ logger.warning("MetricsPersistence background flush error: %s", exc)
103
+
104
+ self._thread = threading.Thread(
105
+ target=_loop, name="slm-metrics-flush", daemon=True,
106
+ )
107
+ self._thread.start()
108
+
109
+ def stop_background_flush(self) -> None:
110
+ """Signal the background flush thread to stop."""
111
+ self._stop_event.set()
112
+ t = self._thread
113
+ if t is not None:
114
+ t.join(timeout=5.0)
115
+ self._thread = None
@@ -0,0 +1,28 @@
1
+ """Public exports for the Optimize proxy package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from superlocalmemory.optimize.proxy.lifecycle import (
6
+ CachedResponse,
7
+ CompressHook,
8
+ CacheHook,
9
+ HookChain,
10
+ ProviderResponse,
11
+ ProxyRequest,
12
+ ensure_proxy_running,
13
+ proxy_port,
14
+ )
15
+ from superlocalmemory.optimize.proxy.server import ProxyApp, build_proxy_router
16
+
17
+ __all__ = [
18
+ "ProxyApp",
19
+ "build_proxy_router",
20
+ "ProxyRequest",
21
+ "CachedResponse",
22
+ "ProviderResponse",
23
+ "CacheHook",
24
+ "CompressHook",
25
+ "HookChain",
26
+ "ensure_proxy_running",
27
+ "proxy_port",
28
+ ]
@@ -0,0 +1,257 @@
1
+ """_helpers.py — Shared HTTP utilities used by all surface modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any, AsyncIterator
8
+
9
+ import httpx
10
+ from fastapi.requests import Request
11
+ from fastapi.responses import Response, StreamingResponse
12
+
13
+ from superlocalmemory.optimize.proxy.lifecycle import (
14
+ CachedResponse,
15
+ HookChain,
16
+ ProviderResponse,
17
+ ProxyRequest,
18
+ )
19
+
20
+ _get_running_loop = asyncio.get_running_loop
21
+
22
+ logger = logging.getLogger("slm.optimize.proxy.helpers")
23
+
24
+ # SEC-M-02 (CWE-400): reject oversized bodies to prevent compression-bomb DoS.
25
+ _MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024 # 10 MB
26
+
27
+
28
+ # Test helper: in-test httpx mock transport. NOT used in production code.
29
+ from typing import Callable as _Callable
30
+ import httpx as _httpx
31
+
32
+
33
+ class _MockTransport(_httpx.AsyncBaseTransport):
34
+ """Minimal in-test mock for httpx that records requests and returns canned responses.
35
+
36
+ Handler may be sync (returns Response) or async (returns coroutine).
37
+ """
38
+
39
+ def __init__(self, handler: _Callable) -> None:
40
+ self._handler = handler
41
+ self.requests: list = []
42
+
43
+ async def handle_async_request(self, request: _httpx.Request) -> _httpx.Response:
44
+ self.requests.append(request)
45
+ result = self._handler(request)
46
+ if hasattr(result, "__await__"):
47
+ return await result
48
+ return result
49
+
50
+ _HOP_BY_HOP = frozenset([
51
+ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
52
+ "te", "trailer", "transfer-encoding", "upgrade", "host",
53
+ "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto",
54
+ "x-real-ip", "x-original-forwarded-for",
55
+ ])
56
+
57
+ _ANTHROPIC_FORWARD_HEADERS = frozenset([
58
+ "x-api-key",
59
+ "anthropic-version",
60
+ "anthropic-beta",
61
+ "x-claude-code-session-id",
62
+ "x-claude-code-agent-id",
63
+ "x-claude-code-parent-agent-id",
64
+ "authorization",
65
+ "content-type",
66
+ ])
67
+
68
+ _OPENAI_FORWARD_HEADERS = frozenset([
69
+ "authorization",
70
+ "content-type",
71
+ "openai-beta",
72
+ "openai-organization",
73
+ ])
74
+
75
+ _GEMINI_NATIVE_FORWARD_HEADERS = frozenset([
76
+ "x-goog-api-key",
77
+ "content-type",
78
+ ])
79
+
80
+ _GEMINI_OPENAI_COMPAT_FORWARD_HEADERS = frozenset([
81
+ "authorization",
82
+ "content-type",
83
+ ])
84
+
85
+ _SENSITIVE_HEADER_KEYS = frozenset([
86
+ "authorization",
87
+ "x-api-key",
88
+ "x-goog-api-key",
89
+ ])
90
+ _REDACTED = "[REDACTED]"
91
+
92
+
93
+ def _redact_headers(headers: dict) -> dict:
94
+ return {
95
+ k: (_REDACTED if k.lower() in _SENSITIVE_HEADER_KEYS else v)
96
+ for k, v in headers.items()
97
+ }
98
+
99
+
100
+ def _body_has_tools(body: dict) -> bool:
101
+ tools = body.get("tools")
102
+ return isinstance(tools, list) and len(tools) > 0
103
+
104
+
105
+ def _build_forward_headers(request: Request, allowed: frozenset) -> dict:
106
+ result: dict = {}
107
+ for k, v in request.headers.items():
108
+ kl = k.lower()
109
+ if kl in _HOP_BY_HOP:
110
+ continue
111
+ if kl in allowed:
112
+ result[kl] = v
113
+ return result
114
+
115
+
116
+ def _filter_response_headers(headers) -> dict:
117
+ if hasattr(headers, "items"):
118
+ items = headers.items()
119
+ else:
120
+ items = headers
121
+ return {k: v for k, v in items if k.lower() not in _HOP_BY_HOP}
122
+
123
+
124
+ async def _fail_open_forward(proxy: Any, request: Request, upstream_url: str) -> Response:
125
+ if proxy.http_client is None:
126
+ logger.error(
127
+ "fail_open_forward: http_client is None — startup() was not called. "
128
+ "Check daemon lifespan wiring."
129
+ )
130
+ return Response(
131
+ content=b'{"type":"error","error":{"type":"api_error",'
132
+ b'"message":"SLM proxy not started - lifespan wiring error"}}',
133
+ status_code=502,
134
+ media_type="application/json",
135
+ )
136
+ try:
137
+ body_bytes = await request.body()
138
+ fwd_headers = {
139
+ k: v for k, v in request.headers.items()
140
+ if k.lower() not in _HOP_BY_HOP
141
+ }
142
+ upstream_resp = await proxy.http_client.request(
143
+ method=request.method,
144
+ url=upstream_url,
145
+ headers=fwd_headers,
146
+ content=body_bytes,
147
+ )
148
+ return Response(
149
+ content=upstream_resp.content,
150
+ status_code=upstream_resp.status_code,
151
+ headers=_filter_response_headers(dict(upstream_resp.headers)),
152
+ )
153
+ except Exception as exc:
154
+ logger.error("fail_open_forward failed upstream=%s exc=%r", upstream_url, exc)
155
+ return Response(
156
+ content=b'{"type":"error","error":{"type":"api_error",'
157
+ b'"message":"SLM proxy unreachable - check upstream"}}',
158
+ status_code=502,
159
+ media_type="application/json",
160
+ )
161
+
162
+
163
+ async def _stream_forward(
164
+ proxy: Any,
165
+ request_id: str,
166
+ fwd_headers: dict,
167
+ body_bytes: bytes,
168
+ upstream_url: str,
169
+ ) -> Response | StreamingResponse:
170
+ if proxy.http_client is None:
171
+ logger.error(
172
+ "[%s] _stream_forward: http_client is None - startup() was not called. "
173
+ "Check daemon lifespan wiring.",
174
+ request_id,
175
+ )
176
+ return Response(
177
+ content=b'{"type":"error","error":{"type":"api_error",'
178
+ b'"message":"SLM proxy not started - lifespan wiring error"}}',
179
+ status_code=502,
180
+ media_type="application/json",
181
+ )
182
+
183
+ async def _generate() -> AsyncIterator[bytes]:
184
+ try:
185
+ async with proxy.http_client.stream(
186
+ "POST", upstream_url, content=body_bytes, headers=fwd_headers,
187
+ ) as upstream_resp:
188
+ async for chunk in upstream_resp.aiter_bytes():
189
+ if chunk:
190
+ yield chunk
191
+ except httpx.RemoteProtocolError as exc:
192
+ logger.warning("[%s] upstream stream closed early: %r", request_id, exc)
193
+ yield (
194
+ b'event: error\ndata: {"type":"error","error":{'
195
+ b'"type":"api_error","message":"upstream stream closed"}}\n\n'
196
+ )
197
+ except Exception as exc:
198
+ logger.error("[%s] stream forward error: %r", request_id, exc)
199
+ yield (
200
+ b'event: error\ndata: {"type":"error","error":{'
201
+ b'"type":"api_error","message":"SLM proxy stream error"}}\n\n'
202
+ )
203
+
204
+ return StreamingResponse(
205
+ _generate(),
206
+ media_type="text/event-stream",
207
+ headers={
208
+ "Cache-Control": "no-cache",
209
+ "Connection": "keep-alive",
210
+ "X-Accel-Buffering": "no",
211
+ },
212
+ )
213
+
214
+
215
+ async def _safe_cache_check(hooks: HookChain, ctx: ProxyRequest) -> CachedResponse:
216
+ try:
217
+ result = hooks.cache.check(ctx)
218
+ return result if result is not None else CachedResponse(
219
+ hit=False, data=None, cache_key="", ttl_seconds=0
220
+ )
221
+ except Exception as exc:
222
+ logger.warning("cache.check failed (fail-open): %s", exc)
223
+ return CachedResponse(hit=False, data=None, cache_key="", ttl_seconds=0)
224
+
225
+
226
+ async def _safe_cache_store(
227
+ hooks: HookChain,
228
+ ctx: ProxyRequest,
229
+ resp: ProviderResponse,
230
+ ) -> None:
231
+ try:
232
+ hooks.cache.store(ctx, resp)
233
+ except Exception as exc:
234
+ logger.warning("cache.store failed (fail-open): %s", exc)
235
+
236
+
237
+ async def _safe_cache_hit_callbacks(
238
+ hooks: HookChain,
239
+ ctx: ProxyRequest,
240
+ response_bytes: bytes,
241
+ tokens_saved: int,
242
+ ) -> None:
243
+ try:
244
+ hooks.cache.on_hit(ctx, response_bytes, tokens_saved)
245
+ except Exception as exc:
246
+ logger.warning("cache.on_hit failed (fail-open): %s", exc)
247
+
248
+
249
+ async def _safe_compress(hooks: HookChain, ctx: ProxyRequest) -> ProxyRequest:
250
+ try:
251
+ result: ProxyRequest = hooks.compress.compress(ctx)
252
+ except Exception as exc:
253
+ logger.warning("compress.compress failed (fail-open): %s", exc)
254
+ return ctx
255
+ # on_compress is fired internally by CompressRouter.compress() with
256
+ # actual token counts — no duplicate fire here.
257
+ return result