superlocalmemory 3.6.13 → 3.6.15
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/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +28 -0
- package/README.md +189 -740
- package/package.json +12 -5
- package/plugin/.claude-plugin/plugin.json +20 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +44 -0
- package/plugin/_GENERATED.md +6 -0
- package/plugin/agents/slm-memory-advisor.md +44 -0
- package/plugin/agents/slm-optimize-advisor.md +38 -0
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/requirements.txt +1 -0
- package/plugin/scripts/ensure-venv.bat +122 -0
- package/plugin/scripts/ensure-venv.sh +105 -0
- package/plugin/scripts/slm-launch +15 -0
- package/plugin/scripts/slm-launch.bat +17 -0
- package/plugin/settings.json +16 -0
- package/plugin/skills/slm-cache/SKILL.md +140 -0
- package/plugin/skills/slm-compress/SKILL.md +143 -0
- package/plugin/skills/slm-graph/SKILL.md +300 -0
- package/plugin/skills/slm-recall/SKILL.md +204 -0
- package/plugin/skills/slm-remember/SKILL.md +194 -0
- package/plugin/skills/slm-session/SKILL.md +207 -0
- package/plugin/skills/slm-status/SKILL.md +149 -0
- package/plugin-src/.mcp.json +12 -0
- package/plugin-src/agents/slm-memory-advisor.md +44 -0
- package/plugin-src/agents/slm-optimize-advisor.md +38 -0
- package/plugin-src/commands/slm-optimize.md +22 -0
- package/plugin-src/commands/slm-recall.md +16 -0
- package/plugin-src/commands/slm-remember.md +16 -0
- package/plugin-src/commands/slm-status.md +15 -0
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +14 -0
- package/plugin-src/manifest.json +25 -0
- package/plugin-src/requirements.txt +1 -0
- package/plugin-src/rules/AGENTS.md +91 -0
- package/plugin-src/rules/CLAUDE.md.fragment +44 -0
- package/plugin-src/scripts/ensure-venv.bat +122 -0
- package/plugin-src/scripts/ensure-venv.sh +105 -0
- package/plugin-src/scripts/slm-launch +15 -0
- package/plugin-src/scripts/slm-launch.bat +17 -0
- package/plugin-src/settings.json +16 -0
- package/plugin-src/skills/slm-cache/SKILL.md +140 -0
- package/plugin-src/skills/slm-compress/SKILL.md +143 -0
- package/plugin-src/skills/slm-graph/SKILL.md +300 -0
- package/plugin-src/skills/slm-recall/SKILL.md +204 -0
- package/plugin-src/skills/slm-remember/SKILL.md +194 -0
- package/plugin-src/skills/slm-session/SKILL.md +207 -0
- package/plugin-src/skills/slm-status/SKILL.md +149 -0
- package/pyproject.toml +6 -2
- package/scripts/__tests__/build-plugin.test.mjs +613 -0
- package/scripts/_savings_math.py +270 -0
- package/scripts/build-plugin.js +742 -0
- package/scripts/dogfood_savings.py +490 -0
- package/scripts/install-skills.ps1 +4 -334
- package/scripts/install-skills.sh +4 -435
- package/scripts/postinstall-interactive.js +0 -27
- package/scripts/postinstall.js +21 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +115 -0
- package/src/superlocalmemory/cli/commands.py +439 -41
- package/src/superlocalmemory/cli/main.py +92 -4
- package/src/superlocalmemory/cli/setup_wizard.py +47 -6
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +194 -9
- package/src/superlocalmemory/core/embeddings.py +10 -5
- package/src/superlocalmemory/core/engine.py +76 -5
- package/src/superlocalmemory/core/fact_consolidator.py +20 -3
- package/src/superlocalmemory/core/platform_utils.py +8 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/recall_worker.py +7 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/core/worker_pool.py +14 -2
- package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
- package/src/superlocalmemory/hooks/portable_kit.py +506 -0
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/infra/cloud_backup.py +99 -23
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
- package/src/superlocalmemory/mcp/server.py +75 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
- package/src/superlocalmemory/mcp/tools_core.py +37 -4
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
- package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
- package/src/superlocalmemory/optimize/cache/manager.py +92 -6
- package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
- package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
- package/src/superlocalmemory/optimize/compress/router.py +46 -13
- package/src/superlocalmemory/optimize/config/schema.py +6 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
- package/src/superlocalmemory/optimize/proxy/server.py +11 -0
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
- package/src/superlocalmemory/optimize/storage/db.py +30 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
- package/src/superlocalmemory/retrieval/engine.py +36 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
- package/src/superlocalmemory/server/recall_serializer.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +156 -16
- package/src/superlocalmemory/storage/database.py +215 -43
- package/src/superlocalmemory/storage/migration_runner.py +17 -1
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
- package/src/superlocalmemory/storage/models.py +10 -0
- package/src/superlocalmemory/storage/schema.py +15 -10
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
- package/src/superlocalmemory/ui/index.html +2 -2
- package/src/superlocalmemory/ui/js/core.js +98 -0
- package/src/superlocalmemory/ui/js/dashboard.js +8 -1
- package/src/superlocalmemory/ui/js/ide-status.js +16 -3
- package/src/superlocalmemory/ui/js/math-health.js +15 -3
- package/src/superlocalmemory/ui/js/optimize.js +18 -2
- package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
- package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/ide/skills/slm-build-graph/SKILL.md +0 -423
- package/ide/skills/slm-list-recent/SKILL.md +0 -348
- package/ide/skills/slm-recall/SKILL.md +0 -326
- package/ide/skills/slm-remember/SKILL.md +0 -194
- package/ide/skills/slm-show-patterns/SKILL.md +0 -224
- package/ide/skills/slm-status/SKILL.md +0 -363
- package/ide/skills/slm-switch-profile/SKILL.md +0 -442
- package/skills/slm-build-graph/SKILL.md +0 -423
- package/skills/slm-list-recent/SKILL.md +0 -348
- package/skills/slm-optimize/README.md +0 -55
- package/skills/slm-optimize/SKILL.md +0 -139
- package/skills/slm-recall/SKILL.md +0 -343
- package/skills/slm-remember/SKILL.md +0 -194
- package/skills/slm-show-patterns/SKILL.md +0 -224
- package/skills/slm-status/SKILL.md +0 -363
- package/skills/slm-switch-profile/SKILL.md +0 -442
- package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
- package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
|
@@ -87,6 +87,8 @@ class OptimizeConfig:
|
|
|
87
87
|
semantic_verifier_model: str = ""
|
|
88
88
|
semantic_pad_latency_ms: float = 0.0
|
|
89
89
|
semantic_centroid_min_similarity: float = 0.85
|
|
90
|
+
semantic_max_index_entries: int = 10000
|
|
91
|
+
semantic_max_tenants: int = 10000
|
|
90
92
|
|
|
91
93
|
# Compress
|
|
92
94
|
compress_enabled: bool = False
|
|
@@ -177,6 +179,8 @@ class OptimizeConfig:
|
|
|
177
179
|
semantic_centroid_min_similarity=float(
|
|
178
180
|
d.get("semantic_centroid_min_similarity", 0.85)
|
|
179
181
|
),
|
|
182
|
+
semantic_max_index_entries=int(d.get("semantic_max_index_entries", 10000)),
|
|
183
|
+
semantic_max_tenants=int(d.get("semantic_max_tenants", 10000)),
|
|
180
184
|
compress_enabled=bool(d.get("compress_enabled", False)),
|
|
181
185
|
compress_mode=str(d.get("compress_mode", "safe")),
|
|
182
186
|
compress_prose=bool(d.get("compress_prose", False)),
|
|
@@ -216,6 +220,8 @@ class OptimizeConfig:
|
|
|
216
220
|
"semantic_verifier_model": self.semantic_verifier_model,
|
|
217
221
|
"semantic_pad_latency_ms": self.semantic_pad_latency_ms,
|
|
218
222
|
"semantic_centroid_min_similarity": self.semantic_centroid_min_similarity,
|
|
223
|
+
"semantic_max_index_entries": self.semantic_max_index_entries,
|
|
224
|
+
"semantic_max_tenants": self.semantic_max_tenants,
|
|
219
225
|
"compress_enabled": self.compress_enabled,
|
|
220
226
|
"compress_mode": self.compress_mode,
|
|
221
227
|
"compress_prose": self.compress_prose,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import inspect
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
from typing import Any, AsyncIterator, Callable
|
|
@@ -22,6 +23,39 @@ _get_running_loop = asyncio.get_running_loop
|
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger("slm.optimize.proxy.helpers")
|
|
24
25
|
|
|
26
|
+
# Per-callable cache of "does this hook method accept a tenant_id kwarg?".
|
|
27
|
+
# Keyed by id() of the bound method's __func__ so it is stable per hook class.
|
|
28
|
+
_HOOK_TENANT_SUPPORT: dict[int, bool] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _accepts_tenant_id(fn: Callable) -> bool:
|
|
32
|
+
"""Return True if a hook method accepts a ``tenant_id`` keyword argument.
|
|
33
|
+
|
|
34
|
+
SECURITY (Stage-9 R1): the original WP-D shim used ``except TypeError`` to
|
|
35
|
+
detect legacy hooks, but that clause is structurally unable to tell a
|
|
36
|
+
signature mismatch from a TypeError raised *inside* a tenant-aware hook —
|
|
37
|
+
so an internal bug silently downgraded an authenticated request onto the
|
|
38
|
+
shared (tenant-less) path, re-opening cross-tenant disclosure. We instead
|
|
39
|
+
probe the signature ONCE (cached): a hook is treated as tenant-aware if it
|
|
40
|
+
has an explicit ``tenant_id`` parameter or accepts ``**kwargs``. A
|
|
41
|
+
tenant-aware hook is NEVER retried without the tenant_id; any error it
|
|
42
|
+
raises fails open to a cache MISS, never the shared namespace.
|
|
43
|
+
"""
|
|
44
|
+
target = getattr(fn, "__func__", fn)
|
|
45
|
+
key = id(target)
|
|
46
|
+
cached = _HOOK_TENANT_SUPPORT.get(key)
|
|
47
|
+
if cached is None:
|
|
48
|
+
try:
|
|
49
|
+
params = inspect.signature(fn).parameters
|
|
50
|
+
cached = "tenant_id" in params or any(
|
|
51
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
52
|
+
)
|
|
53
|
+
except (ValueError, TypeError):
|
|
54
|
+
# Builtins / C callables without a signature — assume legacy.
|
|
55
|
+
cached = False
|
|
56
|
+
_HOOK_TENANT_SUPPORT[key] = cached
|
|
57
|
+
return cached
|
|
58
|
+
|
|
25
59
|
# SEC-M-02 (CWE-400): reject oversized bodies to prevent compression-bomb DoS.
|
|
26
60
|
_MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
27
61
|
|
|
@@ -76,6 +110,16 @@ _OPENAI_FORWARD_HEADERS = frozenset([
|
|
|
76
110
|
_GEMINI_NATIVE_FORWARD_HEADERS = frozenset([
|
|
77
111
|
"x-goog-api-key",
|
|
78
112
|
"content-type",
|
|
113
|
+
"authorization", # WP-11a: Antigravity ADC/OAuth bearer was dropped; add it back
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
# WP-11: Vertex AI forward headers — Authorization passed untouched (AC-2).
|
|
117
|
+
# x-goog-user-project required for quota attribution on Vertex calls.
|
|
118
|
+
_VERTEX_FORWARD_HEADERS = frozenset([
|
|
119
|
+
"authorization",
|
|
120
|
+
"content-type",
|
|
121
|
+
"x-goog-api-key",
|
|
122
|
+
"x-goog-user-project",
|
|
79
123
|
])
|
|
80
124
|
|
|
81
125
|
_GEMINI_OPENAI_COMPAT_FORWARD_HEADERS = frozenset([
|
|
@@ -98,6 +142,28 @@ def _redact_headers(headers: dict) -> dict:
|
|
|
98
142
|
}
|
|
99
143
|
|
|
100
144
|
|
|
145
|
+
def _derive_tenant_id(provider: str, raw_credential: "str | None") -> "str | None":
|
|
146
|
+
"""Derive a per-tenant isolation key from the raw (un-redacted) credential.
|
|
147
|
+
|
|
148
|
+
SECURITY (WP-D): Called at the surface-handler layer BEFORE _redact_headers
|
|
149
|
+
strips the credential from ProxyRequest.headers. The derived id is threaded
|
|
150
|
+
into CacheManager.check() / .store() so that two users sharing the same
|
|
151
|
+
prompt but using different API keys receive independent cache namespaces.
|
|
152
|
+
|
|
153
|
+
Returns None when no credential is present — callers must SKIP caching
|
|
154
|
+
(never collapse to the default tenant) to prevent cross-tenant disclosure.
|
|
155
|
+
|
|
156
|
+
Output: 64-char lowercase hex SHA-256 of ``f"{provider}:{raw_credential}"``.
|
|
157
|
+
Provider is folded in so anthropic:K and openai:K are distinct tenants even
|
|
158
|
+
if the literal key string coincidentally matches.
|
|
159
|
+
"""
|
|
160
|
+
import hashlib as _hashlib
|
|
161
|
+
|
|
162
|
+
if not raw_credential:
|
|
163
|
+
return None
|
|
164
|
+
return _hashlib.sha256(f"{provider}:{raw_credential}".encode()).hexdigest()
|
|
165
|
+
|
|
166
|
+
|
|
101
167
|
def _body_has_tools(body: dict) -> bool:
|
|
102
168
|
tools = body.get("tools")
|
|
103
169
|
return isinstance(tools, list) and len(tools) > 0
|
|
@@ -545,25 +611,62 @@ async def capture_passthrough_forward(
|
|
|
545
611
|
)
|
|
546
612
|
|
|
547
613
|
|
|
548
|
-
async def _safe_cache_check(
|
|
614
|
+
async def _safe_cache_check(
|
|
615
|
+
hooks: HookChain,
|
|
616
|
+
ctx: ProxyRequest,
|
|
617
|
+
tenant_id: "str | None" = None,
|
|
618
|
+
) -> CachedResponse:
|
|
619
|
+
"""Invoke cache.check(); fail-open on error.
|
|
620
|
+
|
|
621
|
+
SECURITY (WP-D): tenant_id is forwarded to the CacheHook so that the
|
|
622
|
+
credential-derived namespace is used. None signals "skip caching" — the
|
|
623
|
+
cache hook must not collapse to a default tenant for unauthenticated
|
|
624
|
+
requests. When tenant_id is None, return an empty miss immediately.
|
|
625
|
+
|
|
626
|
+
Backward compat: a genuinely legacy hook (no tenant_id parameter and no
|
|
627
|
+
**kwargs) is called without the kwarg. A tenant-aware hook is NEVER
|
|
628
|
+
downgraded to the tenant-less path — see _accepts_tenant_id (Stage-9 R1).
|
|
629
|
+
"""
|
|
630
|
+
miss = CachedResponse(hit=False, data=None, cache_key="", ttl_seconds=0)
|
|
631
|
+
if tenant_id is None:
|
|
632
|
+
return miss
|
|
549
633
|
try:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
634
|
+
if _accepts_tenant_id(hooks.cache.check):
|
|
635
|
+
result = hooks.cache.check(ctx, tenant_id=tenant_id)
|
|
636
|
+
else:
|
|
637
|
+
# Genuine legacy hook — no tenant_id support at all.
|
|
638
|
+
result = hooks.cache.check(ctx)
|
|
639
|
+
return result if result is not None else miss
|
|
554
640
|
except Exception as exc:
|
|
555
|
-
|
|
556
|
-
|
|
641
|
+
# SECURITY: a tenant-aware hook that errors must fail-open to a MISS,
|
|
642
|
+
# never be retried on the shared (tenant-less) namespace.
|
|
643
|
+
logger.warning("cache.check failed (fail-open miss): %s", exc)
|
|
644
|
+
return miss
|
|
557
645
|
|
|
558
646
|
|
|
559
647
|
async def _safe_cache_store(
|
|
560
648
|
hooks: HookChain,
|
|
561
649
|
ctx: ProxyRequest,
|
|
562
650
|
resp: ProviderResponse,
|
|
651
|
+
tenant_id: "str | None" = None,
|
|
563
652
|
) -> None:
|
|
653
|
+
"""Invoke cache.store(); fail-open on error.
|
|
654
|
+
|
|
655
|
+
SECURITY (WP-D): tenant_id is forwarded. None means skip (unauthenticated).
|
|
656
|
+
|
|
657
|
+
Backward compat: a genuinely legacy hook (no tenant_id parameter and no
|
|
658
|
+
**kwargs) is called without the kwarg. A tenant-aware hook is NEVER
|
|
659
|
+
downgraded to the tenant-less path — see _accepts_tenant_id (Stage-9 R1).
|
|
660
|
+
"""
|
|
661
|
+
if tenant_id is None:
|
|
662
|
+
return
|
|
564
663
|
try:
|
|
565
|
-
hooks.cache.store
|
|
664
|
+
if _accepts_tenant_id(hooks.cache.store):
|
|
665
|
+
hooks.cache.store(ctx, resp, tenant_id=tenant_id)
|
|
666
|
+
else:
|
|
667
|
+
hooks.cache.store(ctx, resp)
|
|
566
668
|
except Exception as exc:
|
|
669
|
+
# SECURITY: never retry a tenant-aware hook without tenant_id.
|
|
567
670
|
logger.warning("cache.store failed (fail-open): %s", exc)
|
|
568
671
|
|
|
569
672
|
|
|
@@ -15,6 +15,7 @@ from superlocalmemory.optimize.proxy._helpers import (
|
|
|
15
15
|
_MAX_REQUEST_BODY_BYTES,
|
|
16
16
|
_body_has_tools,
|
|
17
17
|
_build_forward_headers,
|
|
18
|
+
_derive_tenant_id,
|
|
18
19
|
_fail_open_forward,
|
|
19
20
|
_filter_response_headers,
|
|
20
21
|
_parse_sse_to_json,
|
|
@@ -204,6 +205,14 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
204
205
|
sse_parser=_parse_sse_to_json, is_stream=stream,
|
|
205
206
|
)
|
|
206
207
|
|
|
208
|
+
# SECURITY (WP-D): derive tenant BEFORE _redact_headers strips the key.
|
|
209
|
+
# x-api-key is the primary Anthropic credential; fall back to authorization.
|
|
210
|
+
_raw_key = (
|
|
211
|
+
request.headers.get("x-api-key")
|
|
212
|
+
or request.headers.get("authorization")
|
|
213
|
+
)
|
|
214
|
+
_tenant_id = _derive_tenant_id("anthropic", _raw_key)
|
|
215
|
+
|
|
207
216
|
ctx = ProxyRequest(
|
|
208
217
|
provider="anthropic",
|
|
209
218
|
method="POST",
|
|
@@ -225,7 +234,7 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
225
234
|
# accumulated and stored, return it as a proper SSE stream rather
|
|
226
235
|
# than forwarding to Anthropic at all.
|
|
227
236
|
if proxy.hooks.cache:
|
|
228
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
237
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
229
238
|
if cache_result and cache_result.hit and cache_result.data:
|
|
230
239
|
logger.debug(
|
|
231
240
|
"[%s] streaming cache HIT key=%s",
|
|
@@ -261,6 +270,7 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
261
270
|
if proxy.hooks.cache:
|
|
262
271
|
_hooks = proxy.hooks
|
|
263
272
|
_ctx = ctx
|
|
273
|
+
_tid = _tenant_id
|
|
264
274
|
async def _store_from_sse(sse_bytes: bytes) -> None:
|
|
265
275
|
parsed = _parse_sse_to_json(sse_bytes)
|
|
266
276
|
if parsed is None:
|
|
@@ -269,7 +279,7 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
269
279
|
modified=False, body={}, body_bytes=parsed,
|
|
270
280
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
271
281
|
)
|
|
272
|
-
await _safe_cache_store(_hooks, _ctx, prov)
|
|
282
|
+
await _safe_cache_store(_hooks, _ctx, prov, tenant_id=_tid)
|
|
273
283
|
store_callback = _store_from_sse
|
|
274
284
|
|
|
275
285
|
return await _stream_and_cache_forward(
|
|
@@ -279,7 +289,7 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
279
289
|
|
|
280
290
|
cache_result = None
|
|
281
291
|
if proxy.hooks.cache:
|
|
282
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
292
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
283
293
|
if cache_result.hit and cache_result.data:
|
|
284
294
|
logger.debug("[%s] cache HIT key=%s", request_id, cache_result.cache_key)
|
|
285
295
|
await _safe_cache_hit_callbacks(
|
|
@@ -315,7 +325,7 @@ async def handle_messages(proxy: object, request: Request) -> Response:
|
|
|
315
325
|
modified=False, body={}, body_bytes=resp_bytes,
|
|
316
326
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
317
327
|
)
|
|
318
|
-
await _safe_cache_store(proxy.hooks, ctx, _prov_resp)
|
|
328
|
+
await _safe_cache_store(proxy.hooks, ctx, _prov_resp, tenant_id=_tenant_id)
|
|
319
329
|
|
|
320
330
|
return Response(
|
|
321
331
|
content=resp_bytes,
|
|
@@ -38,6 +38,7 @@ from superlocalmemory.optimize.proxy._helpers import (
|
|
|
38
38
|
_GEMINI_NATIVE_FORWARD_HEADERS,
|
|
39
39
|
_GEMINI_OPENAI_COMPAT_FORWARD_HEADERS,
|
|
40
40
|
_body_has_tools,
|
|
41
|
+
_derive_tenant_id,
|
|
41
42
|
_fail_open_forward,
|
|
42
43
|
_filter_response_headers,
|
|
43
44
|
_redact_headers,
|
|
@@ -259,6 +260,14 @@ async def handle_gemini_native(
|
|
|
259
260
|
sse_parser=_parse_gemini_sse_to_json, is_stream=stream,
|
|
260
261
|
)
|
|
261
262
|
|
|
263
|
+
# SECURITY (WP-D): derive tenant BEFORE _redact_headers strips the key.
|
|
264
|
+
# Gemini uses x-goog-api-key; OAuth uses authorization bearer.
|
|
265
|
+
_raw_key = (
|
|
266
|
+
request.headers.get("x-goog-api-key")
|
|
267
|
+
or request.headers.get("authorization")
|
|
268
|
+
)
|
|
269
|
+
_tenant_id = _derive_tenant_id("gemini", _raw_key)
|
|
270
|
+
|
|
262
271
|
ctx = ProxyRequest(
|
|
263
272
|
provider="gemini",
|
|
264
273
|
method="POST",
|
|
@@ -279,7 +288,7 @@ async def handle_gemini_native(
|
|
|
279
288
|
if stream:
|
|
280
289
|
# ── 1. Cache check ──────────────────────────────────────────────
|
|
281
290
|
if proxy.hooks.cache:
|
|
282
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
291
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
283
292
|
if cache_result and cache_result.hit and cache_result.data:
|
|
284
293
|
logger.debug(
|
|
285
294
|
"[%s] Gemini native streaming cache HIT key=%s",
|
|
@@ -314,6 +323,7 @@ async def handle_gemini_native(
|
|
|
314
323
|
if proxy.hooks.cache:
|
|
315
324
|
_hooks = proxy.hooks
|
|
316
325
|
_ctx = ctx
|
|
326
|
+
_tid = _tenant_id
|
|
317
327
|
|
|
318
328
|
async def _store_from_gemini_sse(sse_bytes: bytes) -> None:
|
|
319
329
|
parsed = _parse_gemini_sse_to_json(sse_bytes)
|
|
@@ -323,7 +333,7 @@ async def handle_gemini_native(
|
|
|
323
333
|
modified=False, body={}, body_bytes=parsed,
|
|
324
334
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
325
335
|
)
|
|
326
|
-
await _safe_cache_store(_hooks, _ctx, prov)
|
|
336
|
+
await _safe_cache_store(_hooks, _ctx, prov, tenant_id=_tid)
|
|
327
337
|
|
|
328
338
|
store_callback = _store_from_gemini_sse
|
|
329
339
|
|
|
@@ -335,7 +345,7 @@ async def handle_gemini_native(
|
|
|
335
345
|
# ── Non-streaming path ──────────────────────────────────────────────
|
|
336
346
|
cache_result = None
|
|
337
347
|
if proxy.hooks.cache:
|
|
338
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
348
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
339
349
|
if cache_result.hit and cache_result.data:
|
|
340
350
|
await _safe_cache_hit_callbacks(
|
|
341
351
|
proxy.hooks, ctx, cache_result.data, tokens_saved=0
|
|
@@ -369,7 +379,7 @@ async def handle_gemini_native(
|
|
|
369
379
|
modified=False, body={}, body_bytes=resp_bytes,
|
|
370
380
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
371
381
|
)
|
|
372
|
-
await _safe_cache_store(proxy.hooks, ctx, prov_resp)
|
|
382
|
+
await _safe_cache_store(proxy.hooks, ctx, prov_resp, tenant_id=_tenant_id)
|
|
373
383
|
|
|
374
384
|
return Response(
|
|
375
385
|
content=resp_bytes,
|
|
@@ -438,6 +448,13 @@ async def handle_gemini_openai_compat(proxy: object, request: Request) -> Respon
|
|
|
438
448
|
sse_parser=_parse_openai_sse_to_json, is_stream=stream,
|
|
439
449
|
)
|
|
440
450
|
|
|
451
|
+
# SECURITY (WP-D): derive tenant BEFORE _redact_headers strips the key.
|
|
452
|
+
_raw_key_compat = (
|
|
453
|
+
request.headers.get("x-goog-api-key")
|
|
454
|
+
or request.headers.get("authorization")
|
|
455
|
+
)
|
|
456
|
+
_tenant_id_compat = _derive_tenant_id("gemini-openai-compat", _raw_key_compat)
|
|
457
|
+
|
|
441
458
|
ctx = ProxyRequest(
|
|
442
459
|
provider="gemini-openai-compat",
|
|
443
460
|
method="POST",
|
|
@@ -453,7 +470,7 @@ async def handle_gemini_openai_compat(proxy: object, request: Request) -> Respon
|
|
|
453
470
|
# Cache check
|
|
454
471
|
cache_result = None
|
|
455
472
|
if proxy.hooks.cache:
|
|
456
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
473
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id_compat)
|
|
457
474
|
if cache_result.hit and cache_result.data:
|
|
458
475
|
await _safe_cache_hit_callbacks(
|
|
459
476
|
proxy.hooks, ctx, cache_result.data, tokens_saved=0
|
|
@@ -495,7 +512,7 @@ async def handle_gemini_openai_compat(proxy: object, request: Request) -> Respon
|
|
|
495
512
|
modified=False, body={}, body_bytes=resp_bytes,
|
|
496
513
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
497
514
|
)
|
|
498
|
-
await _safe_cache_store(proxy.hooks, ctx, prov_resp)
|
|
515
|
+
await _safe_cache_store(proxy.hooks, ctx, prov_resp, tenant_id=_tenant_id_compat)
|
|
499
516
|
|
|
500
517
|
return Response(
|
|
501
518
|
content=resp_bytes,
|
|
@@ -24,6 +24,7 @@ from superlocalmemory.optimize.proxy._helpers import (
|
|
|
24
24
|
_OPENAI_FORWARD_HEADERS,
|
|
25
25
|
_body_has_tools,
|
|
26
26
|
_build_forward_headers,
|
|
27
|
+
_derive_tenant_id,
|
|
27
28
|
_fail_open_forward,
|
|
28
29
|
_filter_response_headers,
|
|
29
30
|
_redact_headers,
|
|
@@ -328,6 +329,10 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
328
329
|
sse_parser=_parse_openai_sse_to_json, is_stream=stream,
|
|
329
330
|
)
|
|
330
331
|
|
|
332
|
+
# SECURITY (WP-D): derive tenant BEFORE _redact_headers strips the key.
|
|
333
|
+
_raw_key = request.headers.get("authorization")
|
|
334
|
+
_tenant_id = _derive_tenant_id("openai", _raw_key)
|
|
335
|
+
|
|
331
336
|
ctx = ProxyRequest(
|
|
332
337
|
provider="openai", method="POST", path="/v1/chat/completions",
|
|
333
338
|
headers=_redact_headers(dict(request.headers)),
|
|
@@ -343,7 +348,7 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
343
348
|
|
|
344
349
|
# 1. Cache check
|
|
345
350
|
if proxy.hooks.cache:
|
|
346
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
351
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
347
352
|
if cache_result and cache_result.hit and cache_result.data:
|
|
348
353
|
logger.debug(
|
|
349
354
|
"[%s] OpenAI streaming cache HIT key=%s",
|
|
@@ -371,6 +376,7 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
371
376
|
if proxy.hooks.cache:
|
|
372
377
|
_hooks = proxy.hooks
|
|
373
378
|
_ctx = ctx
|
|
379
|
+
_tid = _tenant_id
|
|
374
380
|
async def _store_from_openai_sse(sse_bytes: bytes) -> None:
|
|
375
381
|
parsed = _parse_openai_sse_to_json(sse_bytes)
|
|
376
382
|
if parsed is None:
|
|
@@ -379,7 +385,7 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
379
385
|
modified=False, body={}, body_bytes=parsed,
|
|
380
386
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
381
387
|
)
|
|
382
|
-
await _safe_cache_store(_hooks, _ctx, prov)
|
|
388
|
+
await _safe_cache_store(_hooks, _ctx, prov, tenant_id=_tid)
|
|
383
389
|
store_callback = _store_from_openai_sse
|
|
384
390
|
|
|
385
391
|
return await _stream_and_cache_forward(
|
|
@@ -390,7 +396,7 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
390
396
|
# --- Non-streaming path ---
|
|
391
397
|
cache_result = None
|
|
392
398
|
if proxy.hooks.cache:
|
|
393
|
-
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
399
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx, tenant_id=_tenant_id)
|
|
394
400
|
if cache_result.hit and cache_result.data:
|
|
395
401
|
await _safe_cache_hit_callbacks(
|
|
396
402
|
proxy.hooks, ctx, cache_result.data, 0
|
|
@@ -425,7 +431,7 @@ async def handle_chat_completions(proxy: object, request: Request) -> Response:
|
|
|
425
431
|
modified=False, body={}, body_bytes=resp_bytes,
|
|
426
432
|
tokens_before=0, tokens_after=0, strategy="none",
|
|
427
433
|
)
|
|
428
|
-
await _safe_cache_store(proxy.hooks, ctx, _prov_resp)
|
|
434
|
+
await _safe_cache_store(proxy.hooks, ctx, _prov_resp, tenant_id=_tenant_id)
|
|
429
435
|
|
|
430
436
|
return Response(
|
|
431
437
|
content=resp_bytes,
|
|
@@ -109,6 +109,9 @@ def build_proxy_router(proxy: ProxyApp) -> APIRouter:
|
|
|
109
109
|
handle_chat_completions,
|
|
110
110
|
handle_embeddings,
|
|
111
111
|
)
|
|
112
|
+
from superlocalmemory.optimize.proxy.vertex_surface import (
|
|
113
|
+
handle_vertex_generative,
|
|
114
|
+
)
|
|
112
115
|
|
|
113
116
|
router = APIRouter(tags=["slm-optimize-proxy"])
|
|
114
117
|
|
|
@@ -146,6 +149,14 @@ def build_proxy_router(proxy: ProxyApp) -> APIRouter:
|
|
|
146
149
|
async def gemini_openai_models_route(request: Request) -> Response:
|
|
147
150
|
return await handle_gemini_openai_compat(proxy, request)
|
|
148
151
|
|
|
152
|
+
# WP-11: Vertex AI passthrough — must be registered AFTER exact /v1/* routes
|
|
153
|
+
# to avoid shadowing /v1/messages, /v1/chat/completions, /v1/embeddings, etc.
|
|
154
|
+
# FastAPI resolves routes in registration order; the exact routes above are
|
|
155
|
+
# declared before this catch-path, so there is no shadowing.
|
|
156
|
+
@router.post("/v1/projects/{vertex_path:path}")
|
|
157
|
+
async def vertex_route(request: Request, vertex_path: str) -> Response:
|
|
158
|
+
return await handle_vertex_generative(proxy, request, vertex_path)
|
|
159
|
+
|
|
149
160
|
return router
|
|
150
161
|
|
|
151
162
|
|