feat(supermemory): support custom base URL for self-hosted servers

The supermemory SDK already honors SUPERMEMORY_BASE_URL, but the raw
urllib call used for session-end conversation ingest hardcoded
https://api.supermemory.ai/v4/conversations, so ingest always hit the
cloud even when pointing at a self-hosted server (e.g.
http://localhost:6767).

Resolve the base URL as config (supermemory.json base_url) >
SUPERMEMORY_BASE_URL env var > https://api.supermemory.ai, strip any
trailing slash, and use it for both the SDK client and the
/v4/conversations ingest endpoint.
This commit is contained in:
Dhravya Shah 2026-06-10 00:01:09 -07:00 committed by Teknium
parent 244dabbd9c
commit 24ac26a3da
2 changed files with 90 additions and 5 deletions

View file

@ -31,7 +31,7 @@ _VALID_SEARCH_MODES = ("hybrid", "memories", "documents")
_DEFAULT_API_TIMEOUT = 5.0
_MIN_CAPTURE_LENGTH = 10
_MAX_ENTITY_CONTEXT_LENGTH = 1500
_CONVERSATIONS_URL = "https://api.supermemory.ai/v4/conversations"
_DEFAULT_BASE_URL = "https://api.supermemory.ai"
_API_KEY_URL = "http://app.supermemory.ai/integrations?connect=hermes"
_TRIVIAL_RE = re.compile(
r"^(ok|okay|thanks|thank you|got it|sure|yes|no|yep|nope|k|ty|thx|np)\.?$",
@ -65,6 +65,7 @@ def _default_config() -> dict:
"search_mode": _DEFAULT_SEARCH_MODE,
"entity_context": _DEFAULT_ENTITY_CONTEXT,
"api_timeout": _DEFAULT_API_TIMEOUT,
"base_url": "",
"enable_custom_container_tags": False,
"custom_containers": [],
"custom_container_instructions": "",
@ -77,6 +78,15 @@ def _sanitize_tag(raw: str) -> str:
return tag.strip("_") or _DEFAULT_CONTAINER_TAG
def _resolve_base_url(config_value: Any = "") -> str:
"""Resolve the API base URL: config > SUPERMEMORY_BASE_URL env var > default.
Supports self-hosted Supermemory servers (e.g. http://localhost:6767).
"""
raw = str(config_value or "").strip() or os.environ.get("SUPERMEMORY_BASE_URL", "").strip()
return (raw or _DEFAULT_BASE_URL).rstrip("/") or _DEFAULT_BASE_URL
def _clamp_entity_context(text: str) -> str:
if not text:
return _DEFAULT_ENTITY_CONTEXT
@ -129,6 +139,7 @@ def _load_supermemory_config(hermes_home: str) -> dict:
config["api_timeout"] = max(0.5, min(15.0, float(config.get("api_timeout", _DEFAULT_API_TIMEOUT))))
except Exception:
config["api_timeout"] = _DEFAULT_API_TIMEOUT
config["base_url"] = str(config.get("base_url", "") or "").strip()
# Multi-container support
config["enable_custom_container_tags"] = _as_bool(config.get("enable_custom_container_tags"), False)
@ -263,7 +274,8 @@ def _is_trivial_message(text: str) -> bool:
class _SupermemoryClient:
def __init__(self, api_key: str, timeout: float, container_tag: str, search_mode: str = "hybrid"):
def __init__(self, api_key: str, timeout: float, container_tag: str, search_mode: str = "hybrid",
base_url: str = ""):
# Lazy-install the supermemory SDK on demand. ensure() honors
# security.allow_lazy_installs (default true) and, on a sealed Docker
# venv, redirects the install to the durable target. On failure we
@ -276,15 +288,16 @@ class _SupermemoryClient:
pass
except Exception:
pass
from supermemory import Supermemory
self._api_key = api_key
self._container_tag = container_tag
self._search_mode = search_mode if search_mode in _VALID_SEARCH_MODES else _DEFAULT_SEARCH_MODE
self._timeout = timeout
self._base_url = (base_url or _DEFAULT_BASE_URL).rstrip("/") or _DEFAULT_BASE_URL
self._client = Supermemory(
api_key=api_key,
base_url=self._base_url,
timeout=timeout,
max_retries=0,
default_headers={"x-sm-source": "hermes"},
@ -388,7 +401,7 @@ class _SupermemoryClient:
payload["metadata"] = self._merge_metadata(metadata)
req = urllib.request.Request(
_CONVERSATIONS_URL,
f"{self._base_url}/v4/conversations",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {self._api_key}",
@ -531,6 +544,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
self._search_mode = _DEFAULT_SEARCH_MODE
self._entity_context = _DEFAULT_ENTITY_CONTEXT
self._api_timeout = _DEFAULT_API_TIMEOUT
self._base_url = _DEFAULT_BASE_URL
self._hermes_home = ""
self._write_enabled = True
self._active = False
@ -645,6 +659,9 @@ class SupermemoryMemoryProvider(MemoryProvider):
self._search_mode = self._config["search_mode"]
self._entity_context = self._config["entity_context"]
self._api_timeout = self._config["api_timeout"]
# Base URL: config > SUPERMEMORY_BASE_URL env var > api.supermemory.ai.
# Supports self-hosted Supermemory servers.
self._base_url = _resolve_base_url(self._config["base_url"])
self._enable_custom_containers = self._config["enable_custom_container_tags"]
self._custom_containers = self._config["custom_containers"]
self._custom_container_instructions = self._config["custom_container_instructions"]
@ -663,6 +680,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
timeout=self._api_timeout,
container_tag=self._container_tag,
search_mode=self._search_mode,
base_url=self._base_url,
)
except Exception:
logger.warning("Supermemory initialization failed", exc_info=True)

View file

@ -17,11 +17,13 @@ from plugins.memory.supermemory import (
class FakeClient:
def __init__(self, api_key: str, timeout: float, container_tag: str, search_mode: str = "hybrid"):
def __init__(self, api_key: str, timeout: float, container_tag: str, search_mode: str = "hybrid",
base_url: str = ""):
self.api_key = api_key
self.timeout = timeout
self.container_tag = container_tag
self.search_mode = search_mode
self.base_url = base_url
self.add_calls = []
self.search_results = []
self.profile_response = {"static": [], "dynamic": [], "search_results": []}
@ -373,6 +375,71 @@ def test_invalid_search_mode_falls_back_to_default(monkeypatch, tmp_path):
assert p._search_mode == "hybrid"
# -- Base URL tests -------------------------------------------------------------
def test_base_url_defaults_to_cloud(monkeypatch, tmp_path):
"""Without config or env override, the client targets api.supermemory.ai."""
monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key")
monkeypatch.delenv("SUPERMEMORY_BASE_URL", raising=False)
monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient)
p = SupermemoryMemoryProvider()
p.initialize("s1", hermes_home=str(tmp_path), platform="cli")
assert p._base_url == "https://api.supermemory.ai"
assert p._client.base_url == "https://api.supermemory.ai"
def test_base_url_env_var_override(monkeypatch, tmp_path):
"""SUPERMEMORY_BASE_URL points the provider at a self-hosted server (trailing slash stripped)."""
monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key")
monkeypatch.setenv("SUPERMEMORY_BASE_URL", "http://localhost:6767/")
monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient)
p = SupermemoryMemoryProvider()
p.initialize("s1", hermes_home=str(tmp_path), platform="cli")
assert p._base_url == "http://localhost:6767"
assert p._client.base_url == "http://localhost:6767"
def test_base_url_config_overrides_env(monkeypatch, tmp_path):
"""base_url in supermemory.json takes precedence over the env var."""
monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key")
monkeypatch.setenv("SUPERMEMORY_BASE_URL", "http://env-host:6767")
monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient)
_save_supermemory_config({"base_url": "http://config-host:6767/"}, str(tmp_path))
p = SupermemoryMemoryProvider()
p.initialize("s1", hermes_home=str(tmp_path), platform="cli")
assert p._base_url == "http://config-host:6767"
assert p._client.base_url == "http://config-host:6767"
def test_ingest_conversation_uses_custom_base_url(monkeypatch):
"""The raw urllib conversations ingest hits the configured base URL, not the cloud."""
from plugins.memory.supermemory import _SupermemoryClient
client = _SupermemoryClient.__new__(_SupermemoryClient)
client._api_key = "test-key"
client._container_tag = "hermes"
client._timeout = 1.0
client._base_url = "http://localhost:6767"
captured = {}
class _FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def fake_urlopen(req, timeout=None):
captured["url"] = req.full_url
return _FakeResponse()
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
client.ingest_conversation("s1", [{"role": "user", "content": "hello there"}])
assert captured["url"] == "http://localhost:6767/v4/conversations"
# -- Multi-container tests ----------------------------------------------------