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.
- package/ATTRIBUTION.md +24 -0
- package/CHANGELOG.md +40 -0
- package/README.md +142 -35
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/cache_cmd.py +198 -0
- package/src/superlocalmemory/cli/commands.py +100 -2
- package/src/superlocalmemory/cli/compress_cmd.py +179 -0
- package/src/superlocalmemory/cli/help_cmd.py +197 -0
- package/src/superlocalmemory/cli/main.py +122 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
- package/src/superlocalmemory/cli/optimize_constants.py +31 -0
- package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
- package/src/superlocalmemory/core/config.py +5 -0
- package/src/superlocalmemory/core/engine.py +23 -0
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
- package/src/superlocalmemory/infra/process_reaper.py +12 -1
- package/src/superlocalmemory/llm/backbone.py +10 -4
- package/src/superlocalmemory/mcp/server.py +34 -0
- package/src/superlocalmemory/mcp/tools_v3.py +6 -2
- package/src/superlocalmemory/optimize/NOTICE +11 -0
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
- package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
- package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
- package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
- package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
- package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
- package/src/superlocalmemory/optimize/cache/exact.py +85 -0
- package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
- package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
- package/src/superlocalmemory/optimize/cache/manager.py +452 -0
- package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
- package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
- package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
- package/src/superlocalmemory/optimize/compress/align.py +153 -0
- package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
- package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
- package/src/superlocalmemory/optimize/compress/router.py +548 -0
- package/src/superlocalmemory/optimize/config/__init__.py +35 -0
- package/src/superlocalmemory/optimize/config/defaults.py +48 -0
- package/src/superlocalmemory/optimize/config/schema.py +255 -0
- package/src/superlocalmemory/optimize/config/store.py +209 -0
- package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
- package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
- package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
- package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
- package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
- package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
- package/src/superlocalmemory/optimize/proxy/server.py +151 -0
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +1016 -0
- package/src/superlocalmemory/optimize/storage/schema.py +184 -0
- package/src/superlocalmemory/server/routes/optimize.py +166 -0
- package/src/superlocalmemory/server/routes/v3_api.py +63 -1
- package/src/superlocalmemory/server/unified_daemon.py +105 -0
- package/src/superlocalmemory/ui/index.html +98 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
- package/src/superlocalmemory/ui/js/optimize.js +173 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Agent redirect registry — DATA ONLY, no logic.
|
|
2
|
+
|
|
3
|
+
PORT: 8765 (INTERFACE-CONTRACT §0). There is NO port 52415 anywhere.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
AGENT_REGISTRY: dict[str, dict[str, Any]] = {
|
|
11
|
+
"claude": {
|
|
12
|
+
"binary": "claude",
|
|
13
|
+
"mechanism": "env",
|
|
14
|
+
"env_vars": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8765"},
|
|
15
|
+
"settings_path": None,
|
|
16
|
+
"protocol": "anthropic",
|
|
17
|
+
"print_only": False,
|
|
18
|
+
"help_text": (
|
|
19
|
+
"Launches `claude` with ANTHROPIC_BASE_URL pointing at the SLM proxy.\n"
|
|
20
|
+
"All Claude Code calls are intercepted: cache checked, response stored."
|
|
21
|
+
),
|
|
22
|
+
},
|
|
23
|
+
"claude-settings": {
|
|
24
|
+
"binary": None,
|
|
25
|
+
"mechanism": "settings-file",
|
|
26
|
+
"settings_path": "~/.claude/settings.json",
|
|
27
|
+
"env_vars": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8765"},
|
|
28
|
+
"protocol": "anthropic",
|
|
29
|
+
"print_only": False,
|
|
30
|
+
"help_text": (
|
|
31
|
+
"Writes ANTHROPIC_BASE_URL into ~/.claude/settings.json env block."
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
"antigravity": {
|
|
35
|
+
"binary": "agy",
|
|
36
|
+
"mechanism": "print-only",
|
|
37
|
+
"protocol": "anthropic",
|
|
38
|
+
"print_only": True,
|
|
39
|
+
"help_text": (
|
|
40
|
+
"Antigravity (agy) redirect must be verified at runtime.\n"
|
|
41
|
+
"Run: agy --help | grep -i base\n"
|
|
42
|
+
"Then export the appropriate env var manually."
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
"cline": {
|
|
46
|
+
"binary": None,
|
|
47
|
+
"mechanism": "config-file",
|
|
48
|
+
"config_path": "{vscode_user_dir}/settings.json",
|
|
49
|
+
"config_key": "cline.openAiApiBase",
|
|
50
|
+
"config_value": "http://127.0.0.1:8765/v1",
|
|
51
|
+
"protocol": "openai",
|
|
52
|
+
"print_only": False,
|
|
53
|
+
"help_text": (
|
|
54
|
+
"Edits VS Code user settings to set cline.openAiApiBase."
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
"opencode": {
|
|
58
|
+
"binary": "opencode",
|
|
59
|
+
"mechanism": "print-only",
|
|
60
|
+
"protocol": "openai",
|
|
61
|
+
"print_only": True,
|
|
62
|
+
"help_text": (
|
|
63
|
+
"OpenCode redirect must be verified at runtime.\n"
|
|
64
|
+
"Run: opencode --print-config"
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
"cursor": {
|
|
68
|
+
"binary": "cursor",
|
|
69
|
+
"mechanism": "print-only",
|
|
70
|
+
"protocol": "openai",
|
|
71
|
+
"print_only": True,
|
|
72
|
+
"help_text": (
|
|
73
|
+
"Cursor settings cannot be automated via env vars.\n"
|
|
74
|
+
"Manual: Settings → Models → Override OpenAI Base URL → http://127.0.0.1:8765/v1"
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
"aider": {
|
|
78
|
+
"binary": "aider",
|
|
79
|
+
"mechanism": "env",
|
|
80
|
+
"env_vars": {
|
|
81
|
+
"OPENAI_API_BASE": "http://127.0.0.1:8765/v1",
|
|
82
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8765",
|
|
83
|
+
},
|
|
84
|
+
"protocol": "both",
|
|
85
|
+
"print_only": False,
|
|
86
|
+
"help_text": (
|
|
87
|
+
"Launches `aider` with both OpenAI and Anthropic base URLs set."
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
"codex": {
|
|
91
|
+
"binary": "codex",
|
|
92
|
+
"mechanism": "env",
|
|
93
|
+
"env_vars": {"OPENAI_BASE_URL": "http://127.0.0.1:8765/v1"},
|
|
94
|
+
"protocol": "openai",
|
|
95
|
+
"print_only": False,
|
|
96
|
+
"help_text": (
|
|
97
|
+
"Launches `codex` with OPENAI_BASE_URL pointing at the SLM proxy."
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
"copilot": {
|
|
101
|
+
"binary": "copilot",
|
|
102
|
+
"mechanism": "print-only",
|
|
103
|
+
"protocol": "anthropic",
|
|
104
|
+
"print_only": True,
|
|
105
|
+
"help_text": (
|
|
106
|
+
"Copilot redirect must be verified at runtime.\n"
|
|
107
|
+
"Run: gh copilot --help | grep -i provider"
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
"generic": {
|
|
111
|
+
"binary": None,
|
|
112
|
+
"mechanism": "print-only",
|
|
113
|
+
"protocol": "openai",
|
|
114
|
+
"print_only": True,
|
|
115
|
+
"help_text": (
|
|
116
|
+
"Generic OpenAI-compatible client:\n"
|
|
117
|
+
" export OPENAI_BASE_URL=http://127.0.0.1:8765/v1"
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""SLM Anthropic SDK adapter.
|
|
2
|
+
|
|
3
|
+
Adapted from OmniCache (MIT). See ATTRIBUTION.md.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from superlocalmemory.optimize.cache.manager import CacheManager
|
|
14
|
+
from superlocalmemory.optimize.config.schema import OptimizeConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _detect_async_anthropic(client: Any) -> bool:
|
|
20
|
+
try:
|
|
21
|
+
from anthropic import AsyncAnthropic
|
|
22
|
+
return isinstance(client, AsyncAnthropic)
|
|
23
|
+
except ImportError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SLMAnthropicAdapter:
|
|
28
|
+
"""Wraps anthropic.Anthropic (or AsyncAnthropic) messages with SLM cache."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
client: Any,
|
|
33
|
+
cache_manager: "CacheManager",
|
|
34
|
+
config: "OptimizeConfig",
|
|
35
|
+
tenant_id: str = "default",
|
|
36
|
+
) -> None:
|
|
37
|
+
self._original = client
|
|
38
|
+
self._cache_manager = cache_manager
|
|
39
|
+
self._cache_view = cache_manager.for_tenant(tenant_id)
|
|
40
|
+
self._config = config
|
|
41
|
+
self._tenant_id = tenant_id
|
|
42
|
+
self._is_async: bool = _detect_async_anthropic(client)
|
|
43
|
+
self.messages = _MessagesProxy(self)
|
|
44
|
+
|
|
45
|
+
def _build_cache_key(self, kwargs: dict) -> str:
|
|
46
|
+
model = kwargs.get("model", "unknown")
|
|
47
|
+
messages = kwargs.get("messages", [])
|
|
48
|
+
system = kwargs.get("system", "")
|
|
49
|
+
exclude = {"model", "messages", "system", "stream", "metadata"}
|
|
50
|
+
params = {k: v for k, v in kwargs.items() if k not in exclude}
|
|
51
|
+
return self._cache_manager.build_key(
|
|
52
|
+
{"model": model, "messages": messages, "system": system, "params": params},
|
|
53
|
+
self._tenant_id,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _should_cache(self, kwargs: dict) -> bool:
|
|
57
|
+
if kwargs.get("stream", False):
|
|
58
|
+
return False
|
|
59
|
+
if kwargs.get("tools"):
|
|
60
|
+
return False
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def _messages_create_sync(self, **kwargs: Any) -> Any:
|
|
64
|
+
if not self._config.cache_enabled or not self._should_cache(kwargs):
|
|
65
|
+
return self._original.messages.create(**kwargs)
|
|
66
|
+
key = self._build_cache_key(kwargs)
|
|
67
|
+
if key is None:
|
|
68
|
+
return self._original.messages.create(**kwargs)
|
|
69
|
+
hit = self._cache_view.get(key)
|
|
70
|
+
if hit is not None:
|
|
71
|
+
try:
|
|
72
|
+
import json as _json
|
|
73
|
+
return _json.loads(hit.decode("utf-8"))
|
|
74
|
+
except Exception:
|
|
75
|
+
return self._original.messages.create(**kwargs)
|
|
76
|
+
response = self._original.messages.create(**kwargs)
|
|
77
|
+
try:
|
|
78
|
+
import json as _json
|
|
79
|
+
encoded = _json.dumps(response, default=str).encode("utf-8")
|
|
80
|
+
self._cache_view.set(key, encoded)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
logger.warning("SLMAnthropicAdapter: failed to cache response: %s", exc)
|
|
83
|
+
return response
|
|
84
|
+
|
|
85
|
+
async def _messages_create_async(self, **kwargs: Any) -> Any:
|
|
86
|
+
if not self._config.cache_enabled or not self._should_cache(kwargs):
|
|
87
|
+
return await self._original.messages.create(**kwargs)
|
|
88
|
+
key = self._build_cache_key(kwargs)
|
|
89
|
+
if key is None:
|
|
90
|
+
return await self._original.messages.create(**kwargs)
|
|
91
|
+
hit = self._cache_view.get(key)
|
|
92
|
+
if hit is not None:
|
|
93
|
+
try:
|
|
94
|
+
import json as _json
|
|
95
|
+
return _json.loads(hit.decode("utf-8"))
|
|
96
|
+
except Exception:
|
|
97
|
+
return await self._original.messages.create(**kwargs)
|
|
98
|
+
response = await self._original.messages.create(**kwargs)
|
|
99
|
+
try:
|
|
100
|
+
import json as _json
|
|
101
|
+
encoded = _json.dumps(response, default=str).encode("utf-8")
|
|
102
|
+
self._cache_view.set(key, encoded)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.warning("SLMAnthropicAdapter: failed to cache response: %s", exc)
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _MessagesProxy:
|
|
109
|
+
def __init__(self, adapter: SLMAnthropicAdapter) -> None:
|
|
110
|
+
self._adapter = adapter
|
|
111
|
+
|
|
112
|
+
def create(self, **kwargs: Any) -> Any:
|
|
113
|
+
if self._adapter._is_async:
|
|
114
|
+
return self._adapter._messages_create_async(**kwargs)
|
|
115
|
+
return self._adapter._messages_create_sync(**kwargs)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""SLM OpenAI SDK adapter.
|
|
2
|
+
|
|
3
|
+
Adapted from OmniCache (MIT). See ATTRIBUTION.md.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from superlocalmemory.optimize.cache.manager import CacheManager
|
|
14
|
+
from superlocalmemory.optimize.config.schema import OptimizeConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _detect_async_openai(client: Any) -> bool:
|
|
20
|
+
try:
|
|
21
|
+
from openai import AsyncOpenAI
|
|
22
|
+
return isinstance(client, AsyncOpenAI)
|
|
23
|
+
except ImportError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SLMOpenAIAdapter:
|
|
28
|
+
"""Wraps openai.OpenAI (or AsyncOpenAI) with SLM cache hooks."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
client: Any,
|
|
33
|
+
cache_manager: "CacheManager",
|
|
34
|
+
config: "OptimizeConfig",
|
|
35
|
+
tenant_id: str = "default",
|
|
36
|
+
) -> None:
|
|
37
|
+
self._original = client
|
|
38
|
+
self._cache_manager = cache_manager
|
|
39
|
+
self._cache_view = cache_manager.for_tenant(tenant_id)
|
|
40
|
+
self._config = config
|
|
41
|
+
self._tenant_id = tenant_id
|
|
42
|
+
self._is_async: bool = _detect_async_openai(client)
|
|
43
|
+
self.chat = _ChatCompletionsProxy(self)
|
|
44
|
+
|
|
45
|
+
def _build_cache_key(self, kwargs: dict) -> str:
|
|
46
|
+
model = kwargs.get("model", "unknown")
|
|
47
|
+
messages = kwargs.get("messages", [])
|
|
48
|
+
exclude = {"model", "messages", "stream", "user", "request_id"}
|
|
49
|
+
params = {k: v for k, v in kwargs.items() if k not in exclude}
|
|
50
|
+
canonical = json.dumps(
|
|
51
|
+
{"messages": messages, "params": params},
|
|
52
|
+
sort_keys=True, default=str,
|
|
53
|
+
)
|
|
54
|
+
return self._cache_manager.build_key(
|
|
55
|
+
{"model": model, "messages": messages, "params": params},
|
|
56
|
+
self._tenant_id,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _should_cache(self, kwargs: dict) -> bool:
|
|
60
|
+
if kwargs.get("stream", False):
|
|
61
|
+
return False
|
|
62
|
+
if kwargs.get("tools") or kwargs.get("functions"):
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
def _chat_create_sync(self, **kwargs: Any) -> Any:
|
|
67
|
+
if not self._config.cache_enabled or not self._should_cache(kwargs):
|
|
68
|
+
return self._original.chat.completions.create(**kwargs)
|
|
69
|
+
key = self._build_cache_key(kwargs)
|
|
70
|
+
if key is None:
|
|
71
|
+
return self._original.chat.completions.create(**kwargs)
|
|
72
|
+
hit = self._cache_view.get(key)
|
|
73
|
+
if hit is not None:
|
|
74
|
+
try:
|
|
75
|
+
import json as _json
|
|
76
|
+
return _json.loads(hit.decode("utf-8"))
|
|
77
|
+
except Exception:
|
|
78
|
+
return self._original.chat.completions.create(**kwargs)
|
|
79
|
+
response = self._original.chat.completions.create(**kwargs)
|
|
80
|
+
try:
|
|
81
|
+
import json as _json
|
|
82
|
+
encoded = _json.dumps(response, default=str).encode("utf-8")
|
|
83
|
+
self._cache_view.set(key, encoded)
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
logger.warning("SLMOpenAIAdapter: failed to cache response: %s", exc)
|
|
86
|
+
return response
|
|
87
|
+
|
|
88
|
+
async def _chat_create_async(self, **kwargs: Any) -> Any:
|
|
89
|
+
if not self._config.cache_enabled or not self._should_cache(kwargs):
|
|
90
|
+
return await self._original.chat.completions.create(**kwargs)
|
|
91
|
+
key = self._build_cache_key(kwargs)
|
|
92
|
+
if key is None:
|
|
93
|
+
return await self._original.chat.completions.create(**kwargs)
|
|
94
|
+
hit = self._cache_view.get(key)
|
|
95
|
+
if hit is not None:
|
|
96
|
+
try:
|
|
97
|
+
import json as _json
|
|
98
|
+
return _json.loads(hit.decode("utf-8"))
|
|
99
|
+
except Exception:
|
|
100
|
+
return await self._original.chat.completions.create(**kwargs)
|
|
101
|
+
response = await self._original.chat.completions.create(**kwargs)
|
|
102
|
+
try:
|
|
103
|
+
import json as _json
|
|
104
|
+
encoded = _json.dumps(response, default=str).encode("utf-8")
|
|
105
|
+
self._cache_view.set(key, encoded)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.warning("SLMOpenAIAdapter: failed to cache response: %s", exc)
|
|
108
|
+
return response
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _ChatCompletionsProxy:
|
|
112
|
+
"""Mimics openai.resources.chat.Chat interface: `client.chat.completions.create`."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, adapter: SLMOpenAIAdapter) -> None:
|
|
115
|
+
self._adapter = adapter
|
|
116
|
+
self.completions = self._Completions(adapter)
|
|
117
|
+
|
|
118
|
+
class _Completions:
|
|
119
|
+
def __init__(self, adapter: SLMOpenAIAdapter) -> None:
|
|
120
|
+
self._adapter = adapter
|
|
121
|
+
|
|
122
|
+
def create(self, **kwargs: Any) -> Any:
|
|
123
|
+
if self._adapter._is_async:
|
|
124
|
+
return self._adapter._chat_create_async(**kwargs)
|
|
125
|
+
return self._adapter._chat_create_sync(**kwargs)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""slm wrap <agent> — table-driven per-agent proxy activation.
|
|
2
|
+
|
|
3
|
+
PORT: 8765 (INTERFACE-CONTRACT §0). No 52415 anywhere in this file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from superlocalmemory.optimize.adapters._agent_registry import AGENT_REGISTRY
|
|
17
|
+
from superlocalmemory.optimize.proxy.lifecycle import ensure_proxy_running, proxy_port
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_agents() -> list[str]:
|
|
21
|
+
"""Return all registered agent keys."""
|
|
22
|
+
return list(AGENT_REGISTRY.keys())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def wrap_agent(
|
|
26
|
+
agent_key: str,
|
|
27
|
+
agent_args: list[str],
|
|
28
|
+
*,
|
|
29
|
+
persistent: bool = False,
|
|
30
|
+
dry_run: bool = False,
|
|
31
|
+
) -> int:
|
|
32
|
+
"""Configure proxy redirection for <agent_key> and optionally launch it.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
0 on success, 1 on error, agent's exit code when subprocess launched.
|
|
36
|
+
"""
|
|
37
|
+
if agent_key not in AGENT_REGISTRY:
|
|
38
|
+
known = ", ".join(AGENT_REGISTRY.keys())
|
|
39
|
+
print(
|
|
40
|
+
f"[slm wrap] Unknown agent '{agent_key}'. Known: {known}",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
if not ensure_proxy_running():
|
|
46
|
+
print(
|
|
47
|
+
f"[slm wrap] proxy is not enabled in optimize.json — set "
|
|
48
|
+
f"`proxy_enabled: true` (port 8765) and re-run, or run "
|
|
49
|
+
f"`slm optimize on` first.",
|
|
50
|
+
file=sys.stderr,
|
|
51
|
+
)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
port = proxy_port()
|
|
55
|
+
spec = AGENT_REGISTRY[agent_key]
|
|
56
|
+
mechanism = spec.get("mechanism", "print-only")
|
|
57
|
+
|
|
58
|
+
if mechanism == "print-only":
|
|
59
|
+
print(f"[slm wrap] {agent_key}: manual instructions")
|
|
60
|
+
print(spec.get("help_text", ""))
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
if mechanism == "settings-file":
|
|
64
|
+
# Persist env var to a settings file
|
|
65
|
+
settings_path_str = spec.get("settings_path", "")
|
|
66
|
+
env_vars = spec.get("env_vars", {})
|
|
67
|
+
if not env_vars:
|
|
68
|
+
print(f"[slm wrap] {agent_key}: no env vars to set", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
expanded = str(Path(settings_path_str).expanduser())
|
|
71
|
+
path = Path(expanded)
|
|
72
|
+
if dry_run:
|
|
73
|
+
print(f"[slm wrap] would write {path} with env={env_vars}")
|
|
74
|
+
return 0
|
|
75
|
+
existing: dict = {}
|
|
76
|
+
if path.exists():
|
|
77
|
+
try:
|
|
78
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
except (json.JSONDecodeError, OSError, ValueError):
|
|
80
|
+
existing = {}
|
|
81
|
+
existing.setdefault("env", {})
|
|
82
|
+
for k, v in env_vars.items():
|
|
83
|
+
existing["env"][k] = v
|
|
84
|
+
try:
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
_atomic_write_text(path, json.dumps(existing, indent=2))
|
|
87
|
+
except (OSError, ValueError) as exc:
|
|
88
|
+
print(f"[slm wrap] could not write {path}: {exc}", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
print(f"[slm wrap] wrote {path}")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
if mechanism == "config-file":
|
|
94
|
+
# VS Code settings.json edit. config_path may be:
|
|
95
|
+
# - absolute path → use directly
|
|
96
|
+
# - template with {vscode_user_dir} → expand via _vscode_user_dir()
|
|
97
|
+
# - relative path → resolve against current working directory
|
|
98
|
+
config_value = spec.get("config_value", "")
|
|
99
|
+
config_key = spec.get("config_key", "")
|
|
100
|
+
config_path_str = spec.get("config_path", "")
|
|
101
|
+
if not config_path_str:
|
|
102
|
+
print("[slm wrap] no config_path specified", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
if "{vscode_user_dir}" in config_path_str:
|
|
105
|
+
vscode_dir = _vscode_user_dir()
|
|
106
|
+
if vscode_dir is None:
|
|
107
|
+
print("[slm wrap] VS Code user dir not found", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
path = Path(config_path_str.replace("{vscode_user_dir}", str(vscode_dir)))
|
|
110
|
+
else:
|
|
111
|
+
path = Path(config_path_str).expanduser()
|
|
112
|
+
if dry_run:
|
|
113
|
+
print(f"[slm wrap] would write {path} key={config_key} value={config_value}")
|
|
114
|
+
return 0
|
|
115
|
+
existing = {}
|
|
116
|
+
if path.exists():
|
|
117
|
+
try:
|
|
118
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
119
|
+
except (json.JSONDecodeError, OSError):
|
|
120
|
+
existing = {}
|
|
121
|
+
existing[config_key] = config_value
|
|
122
|
+
try:
|
|
123
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
_atomic_write_text(path, json.dumps(existing, indent=2))
|
|
125
|
+
except OSError as exc:
|
|
126
|
+
print(f"[slm wrap] could not write {path}: {exc}", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
print(f"[slm wrap] wrote {path}")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
if mechanism == "env":
|
|
132
|
+
# Launch the binary with env vars injected
|
|
133
|
+
binary = spec.get("binary")
|
|
134
|
+
env_vars = spec.get("env_vars", {})
|
|
135
|
+
if not binary:
|
|
136
|
+
print(f"[slm wrap] {agent_key}: no binary specified", file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
if shutil.which(binary) is None:
|
|
139
|
+
print(
|
|
140
|
+
f"[slm wrap] binary '{binary}' not found in PATH. "
|
|
141
|
+
f"Install {agent_key} or set PATH.",
|
|
142
|
+
file=sys.stderr,
|
|
143
|
+
)
|
|
144
|
+
return 1
|
|
145
|
+
full_env = os.environ.copy()
|
|
146
|
+
for k, v in env_vars.items():
|
|
147
|
+
full_env[k] = v.replace("{port}", str(port))
|
|
148
|
+
if dry_run:
|
|
149
|
+
print(f"[slm wrap] would exec: {binary} {' '.join(agent_args)}")
|
|
150
|
+
print(f"[slm wrap] env: {env_vars}")
|
|
151
|
+
return 0
|
|
152
|
+
try:
|
|
153
|
+
return subprocess.call([binary, *agent_args], env=full_env)
|
|
154
|
+
except FileNotFoundError as exc:
|
|
155
|
+
print(f"[slm wrap] could not launch {binary}: {exc}", file=sys.stderr)
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
print(f"[slm wrap] {agent_key}: unknown mechanism {mechanism!r}", file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _atomic_write_text(path: Path, content: str) -> None:
|
|
163
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
164
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
165
|
+
f.write(content)
|
|
166
|
+
f.flush()
|
|
167
|
+
os.fsync(f.fileno())
|
|
168
|
+
os.replace(tmp, path)
|
|
169
|
+
try:
|
|
170
|
+
os.chmod(path, 0o600)
|
|
171
|
+
except OSError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _vscode_user_dir() -> Path | None:
|
|
176
|
+
"""Return the VS Code user settings directory for the current OS."""
|
|
177
|
+
if sys.platform == "darwin":
|
|
178
|
+
p = Path.home() / "Library" / "Application Support" / "Code" / "User"
|
|
179
|
+
return p if p.exists() or p.parent.exists() else None
|
|
180
|
+
if sys.platform.startswith("win"):
|
|
181
|
+
appdata = os.environ.get("APPDATA")
|
|
182
|
+
if appdata:
|
|
183
|
+
p = Path(appdata) / "Code" / "User"
|
|
184
|
+
return p
|
|
185
|
+
return None
|
|
186
|
+
# Linux
|
|
187
|
+
p = Path.home() / ".config" / "Code" / "User"
|
|
188
|
+
return p
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""optimize/cache — exact-response cache (Phase 1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from superlocalmemory.optimize.cache.exact import ExactCache
|
|
6
|
+
from superlocalmemory.optimize.cache.invalidation import InvalidationEngine
|
|
7
|
+
from superlocalmemory.optimize.cache.key_builder import CacheConfig, KeyBuilder
|
|
8
|
+
from superlocalmemory.optimize.cache.manager import (
|
|
9
|
+
CacheManager,
|
|
10
|
+
NoOpSemantic,
|
|
11
|
+
SemanticTier,
|
|
12
|
+
_TenantScopedManager,
|
|
13
|
+
)
|
|
14
|
+
from superlocalmemory.optimize.cache.stampede import StampedeShield
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CacheManager",
|
|
18
|
+
"CacheView",
|
|
19
|
+
"ExactCache",
|
|
20
|
+
"InvalidationEngine",
|
|
21
|
+
"KeyBuilder",
|
|
22
|
+
"CacheConfig",
|
|
23
|
+
"StampedeShield",
|
|
24
|
+
"SemanticTier",
|
|
25
|
+
"NoOpSemantic",
|
|
26
|
+
"_TenantScopedManager",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Lazy alias so import order doesn't matter
|
|
31
|
+
CacheView = _TenantScopedManager
|