From d371c1d931fae7b5bfe05e4759daf8fe5c6c6e0e Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Thu, 13 Aug 2026 17:31:23 +0530 Subject: [PATCH] fix(client): honor MEM0_HOST and MEM0_API_URL environment variables MemoryClient and AsyncMemoryClient hardcoded the default host, so a self-hosted or proxied deployment had to pass host= at every construction site even with MEM0_HOST exported. The API key already reads from the environment; the host now does too. Resolution order: explicit host argument, MEM0_HOST, MEM0_API_URL, then https://api.mem0.ai. Existing callers are unaffected. Closes #6622 --- mem0/client/main.py | 21 +++++++++---- tests/test_client.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/mem0/client/main.py b/mem0/client/main.py index a1b68d73b..2b27f725f 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -39,6 +39,13 @@ setup_config() # Entity parameters that must be passed via filters, not top-level ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"}) +DEFAULT_HOST = "https://api.mem0.ai" + + +def _resolve_host(host: Optional[str]) -> str: + """Resolve the API host from the explicit argument, then MEM0_HOST, then MEM0_API_URL.""" + return host or os.getenv("MEM0_HOST") or os.getenv("MEM0_API_URL") or DEFAULT_HOST + def _validate_and_trim_search_query(query: str) -> str: if not isinstance(query, str): @@ -104,8 +111,9 @@ class MemoryClient: api_key: The API key for authenticating with the Mem0 API. If not provided, it will attempt to use the MEM0_API_KEY environment variable. - host: The base URL for the Mem0 API. Defaults to - "https://api.mem0.ai". + host: The base URL for the Mem0 API. If not provided, it will + attempt to use the MEM0_HOST or MEM0_API_URL environment + variables, then default to "https://api.mem0.ai". client: A custom httpx.Client instance. If provided, it will be used instead of creating a new one. Note that base_url and headers will be set/overridden as needed. @@ -114,7 +122,7 @@ class MemoryClient: ValueError: If no API key is provided or found in the environment. """ self.api_key = api_key or os.getenv("MEM0_API_KEY") - self.host = host or "https://api.mem0.ai" + self.host = _resolve_host(host) self.org_id = None self.project_id = None self.user_id = get_user_id() @@ -996,8 +1004,9 @@ class AsyncMemoryClient: api_key: The API key for authenticating with the Mem0 API. If not provided, it will attempt to use the MEM0_API_KEY environment variable. - host: The base URL for the Mem0 API. Defaults to - "https://api.mem0.ai". + host: The base URL for the Mem0 API. If not provided, it will + attempt to use the MEM0_HOST or MEM0_API_URL environment + variables, then default to "https://api.mem0.ai". client: A custom httpx.AsyncClient instance. If provided, it will be used instead of creating a new one. Note that base_url and headers will be set/overridden as needed. @@ -1006,7 +1015,7 @@ class AsyncMemoryClient: ValueError: If no API key is provided or found in the environment. """ self.api_key = api_key or os.getenv("MEM0_API_KEY") - self.host = host or "https://api.mem0.ai" + self.host = _resolve_host(host) self.org_id = None self.project_id = None self.user_id = get_user_id() diff --git a/tests/test_client.py b/tests/test_client.py index d7d11ca86..16455f38b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,6 +8,7 @@ import httpx import pytest import requests +from mem0.client.main import DEFAULT_HOST, _resolve_host from mem0.client.types import GetAllMemoryOptions, SearchMemoryOptions @@ -488,3 +489,76 @@ class TestAddAgentCustomInstructions: _, kwargs = mock_memory_client.client.post.call_args assert "agent_custom_instructions" not in kwargs["json"] + + +class TestHostResolution: + """host comes from the explicit argument, then MEM0_HOST, then MEM0_API_URL, then the default.""" + + @pytest.fixture(autouse=True) + def _clear_host_env(self, monkeypatch): + monkeypatch.delenv("MEM0_HOST", raising=False) + monkeypatch.delenv("MEM0_API_URL", raising=False) + + def test_explicit_argument_wins_over_env(self, monkeypatch): + monkeypatch.setenv("MEM0_HOST", "https://host-env.example.com") + monkeypatch.setenv("MEM0_API_URL", "https://url-env.example.com") + + assert _resolve_host("https://explicit.example.com") == "https://explicit.example.com" + + def test_mem0_host_wins_over_mem0_api_url(self, monkeypatch): + monkeypatch.setenv("MEM0_HOST", "https://host-env.example.com") + monkeypatch.setenv("MEM0_API_URL", "https://url-env.example.com") + + assert _resolve_host(None) == "https://host-env.example.com" + + def test_mem0_api_url_is_used_when_mem0_host_is_unset(self, monkeypatch): + monkeypatch.setenv("MEM0_API_URL", "https://url-env.example.com") + + assert _resolve_host(None) == "https://url-env.example.com" + + def test_empty_env_value_falls_through(self, monkeypatch): + monkeypatch.setenv("MEM0_HOST", "") + monkeypatch.setenv("MEM0_API_URL", "https://url-env.example.com") + + assert _resolve_host(None) == "https://url-env.example.com" + + def test_defaults_when_nothing_is_set(self): + assert _resolve_host(None) == DEFAULT_HOST == "https://api.mem0.ai" + + def test_sync_client_uses_mem0_host(self, monkeypatch): + monkeypatch.setenv("MEM0_HOST", "https://host-env.example.com") + + with patch("mem0.client.main.httpx.Client") as mock_httpx, patch("mem0.client.main.capture_client_event"): + mock_httpx.return_value = MagicMock( + get=MagicMock( + return_value=MagicMock( + json=lambda: {"org_id": "org1", "project_id": "proj1", "user_email": "test@test.com"}, + raise_for_status=lambda: None, + ) + ) + ) + from mem0.client.main import MemoryClient + + client = MemoryClient(api_key="test-api-key") + + assert client.host == "https://host-env.example.com" + assert mock_httpx.call_args.kwargs["base_url"] == "https://host-env.example.com" + + def test_async_client_uses_mem0_api_url(self, monkeypatch): + monkeypatch.setenv("MEM0_API_URL", "https://url-env.example.com") + + with ( + patch("mem0.client.main.httpx.AsyncClient"), + patch("mem0.client.main.capture_client_event"), + patch("mem0.client.main.requests.get") as mock_get, + ): + mock_get.return_value = MagicMock( + json=lambda: {"org_id": "org1", "project_id": "proj1", "user_email": "test@test.com"}, + raise_for_status=lambda: None, + ) + from mem0.client.main import AsyncMemoryClient + + client = AsyncMemoryClient(api_key="test-api-key") + + assert client.host == "https://url-env.example.com" + assert mock_get.call_args.args[0] == "https://url-env.example.com/v1/ping/"