From fb073ad29199c32dfadbd422b5a8ed0f2cfd7bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zi=C3=B3=C5=82kowski?= Date: Tue, 4 Aug 2026 16:04:20 +0100 Subject: [PATCH] feat: forward engine routing overrides via ClientOptions.environment (ENG-2612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional `environment` mapping to ClientOptions and send it verbatim as the `environment` field of the session request when set. This lets a caller pin a session to a specific engine pod / devspace / preview (e.g. {"podName": ..., "engineVersion": ...}) — the same routing the Lab page and the e2e-tests browser adapter already use by injecting an `environment` object into the session-token body. Needed by anam-org/e2e-tests release qualification: its python_sdk adapter sets ANAM_POD_NAME/ANAM_ENGINE_VERSION to target the freshly deployed release pod and passes them via ClientOptions(environment=...), which the SDK previously had no way to forward. Omitted entirely for production. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anam/_api.py | 4 +++ src/anam/types.py | 6 ++++ tests/test_client.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/src/anam/_api.py b/src/anam/_api.py index f28c769..df9afc3 100644 --- a/src/anam/_api.py +++ b/src/anam/_api.py @@ -69,6 +69,10 @@ async def start_session( "sessionOptions": session_options.to_dict(), "clientMetadata": CLIENT_METADATA, } + # Engine routing overrides (e.g. pin to a specific pod / devspace / + # preview). Only sent when set; omitted entirely for production. + if self._options.environment: + body["environment"] = self._options.environment logger.debug("Starting session at %s (direct API-key auth)", url) diff --git a/src/anam/types.py b/src/anam/types.py index ff37909..eab42c2 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -261,12 +261,18 @@ class ClientOptions: ice_servers: Custom ICE servers for WebRTC (optional). client_label: Custom label for session tracking (optional). Defaults to 'python-sdk' if not specified. + environment: Engine routing overrides for non-production targets + (optional), e.g. ``{"podName": ..., "engineVersion": ...}`` to pin + the session to a specific engine pod / devspace / preview. Sent + verbatim as the ``environment`` field of the session request; the + backend ignores it for standard production routing. """ api_base_url: str = "https://api.anam.ai" api_version: str = "v1" ice_servers: list[dict[str, Any]] | None = None client_label: str | None = None + environment: dict[str, str] | None = None @dataclass diff --git a/tests/test_client.py b/tests/test_client.py index 3933e70..313b39d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -72,6 +72,7 @@ def test_init_with_options(self) -> None: """Test initialization with ClientOptions.""" options = ClientOptions( api_base_url="https://custom.api.com", + environment={"podName": "anam-engine-abc", "engineVersion": "v5.13.2"}, ) client = AnamClient( api_key="test-key", @@ -79,6 +80,76 @@ def test_init_with_options(self) -> None: options=options, ) assert client._options.api_base_url == "https://custom.api.com" + assert client._options.environment == { + "podName": "anam-engine-abc", + "engineVersion": "v5.13.2", + } + + def test_environment_defaults_to_none(self) -> None: + assert ClientOptions().environment is None + + +class TestCoreApiClientSessionBody: + """The session request body carries engine routing overrides.""" + + @staticmethod + def _fake_session(captured: dict): + response_payload = { + "sessionId": "sess-1", + "engineHost": "engine.example", + "engineProtocol": "https", + "signallingEndpoint": "wss://engine.example/ws", + } + + class _Resp: + status = 201 + + async def json(self) -> dict: + return response_payload + + async def __aenter__(self) -> "_Resp": + return self + + async def __aexit__(self, *_: object) -> bool: + return False + + class _Session: + async def __aenter__(self) -> "_Session": + return self + + async def __aexit__(self, *_: object) -> bool: + return False + + def post(self, url: str, headers=None, json=None): # noqa: A002 + captured["body"] = json + return _Resp() + + return _Session + + @pytest.mark.asyncio + async def test_environment_is_sent_when_set(self, monkeypatch: pytest.MonkeyPatch) -> None: + from anam._api import CoreApiClient + + captured: dict[str, Any] = {} + monkeypatch.setattr("aiohttp.ClientSession", self._fake_session(captured)) + + overrides = {"podName": "anam-engine-abc", "engineVersion": "v5.13.2"} + client = CoreApiClient("key", ClientOptions(environment=overrides)) + await client.start_session(PersonaConfig(persona_id="p"), SessionOptions()) + + assert captured["body"]["environment"] == overrides + + @pytest.mark.asyncio + async def test_environment_is_omitted_when_unset(self, monkeypatch: pytest.MonkeyPatch) -> None: + from anam._api import CoreApiClient + + captured: dict[str, Any] = {} + monkeypatch.setattr("aiohttp.ClientSession", self._fake_session(captured)) + + client = CoreApiClient("key", ClientOptions()) + await client.start_session(PersonaConfig(persona_id="p"), SessionOptions()) + + assert "environment" not in captured["body"] class TestAnamClientEvents: