diff --git a/sdks/code-interpreter/python/src/code_interpreter/sync/adapters/code_adapter.py b/sdks/code-interpreter/python/src/code_interpreter/sync/adapters/code_adapter.py index 33096ec64..e55086258 100644 --- a/sdks/code-interpreter/python/src/code_interpreter/sync/adapters/code_adapter.py +++ b/sdks/code-interpreter/python/src/code_interpreter/sync/adapters/code_adapter.py @@ -81,7 +81,9 @@ class CodesAdapterSync(CodesSync): RUN_CODE_PATH = "/code" CREATE_CONTEXT_PATH = "/code/context" - def __init__(self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync) -> None: + def __init__( + self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync + ) -> None: """ Initialize the code service adapter (sync). @@ -96,7 +98,11 @@ def __init__(self, execd_endpoint: SandboxEndpoint, connection_config: Connectio base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}" timeout_seconds = self.connection_config.request_timeout.total_seconds() timeout = httpx.Timeout(timeout_seconds) - headers = {"User-Agent": self.connection_config.user_agent, **self.connection_config.headers} + headers = { + "User-Agent": self.connection_config.user_agent, + **self.connection_config.headers, + **(self.execd_endpoint.headers or {}), + } self._client = Client(base_url=base_url, timeout=timeout) self._httpx_client = httpx.Client( @@ -107,16 +113,24 @@ def __init__(self, execd_endpoint: SandboxEndpoint, connection_config: Connectio ) self._client.set_httpx_client(self._httpx_client) - sse_headers = {**headers, "Accept": "text/event-stream", "Cache-Control": "no-cache"} + sse_headers = { + **headers, + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + } self._sse_client = httpx.Client( headers=sse_headers, - timeout=httpx.Timeout(connect=timeout_seconds, read=None, write=timeout_seconds, pool=None), + timeout=httpx.Timeout( + connect=timeout_seconds, read=None, write=timeout_seconds, pool=None + ), transport=self.connection_config.transport, ) def _get_execd_url(self, path: str) -> str: """Build URL for execd endpoint.""" - return f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}" + return ( + f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}" + ) def create_context(self, language: str) -> CodeContextSync: """ @@ -243,7 +257,11 @@ def run( raise InvalidArgumentException("Code cannot be empty") try: - if context is not None and language is not None and context.language != language: + if ( + context is not None + and language is not None + and context.language != language + ): raise InvalidArgumentException( f"language '{language}' must match context.language '{context.language}'" ) @@ -251,7 +269,9 @@ def run( if context is None: # Default context: language default context (server-side behavior). # When context.id is omitted, execd will create/reuse a default session per language. - context = CodeContextSync(language=language or SupportedLanguageSync.PYTHON) + context = CodeContextSync( + language=language or SupportedLanguageSync.PYTHON + ) api_request = { "code": code, "context": { @@ -305,7 +325,9 @@ def interrupt(self, execution_id: str) -> None: try: from opensandbox.api.execd.api.code_interpreting import interrupt_code - response_obj = interrupt_code.sync_detailed(client=self._client, id=execution_id) + response_obj = interrupt_code.sync_detailed( + client=self._client, id=execution_id + ) handle_api_error(response_obj, "Interrupt code execution") except Exception as e: logger.error("Failed to interrupt code execution", exc_info=e) diff --git a/sdks/code-interpreter/python/tests/test_code_service_adapter_sync_headers.py b/sdks/code-interpreter/python/tests/test_code_service_adapter_sync_headers.py new file mode 100644 index 000000000..8ac7dcb6f --- /dev/null +++ b/sdks/code-interpreter/python/tests/test_code_service_adapter_sync_headers.py @@ -0,0 +1,47 @@ +# +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from opensandbox.config.connection_sync import ConnectionConfigSync +from opensandbox.models.sandboxes import SandboxEndpoint + +from code_interpreter.sync.adapters.code_adapter import CodesAdapterSync + + +def test_sync_adapter_merges_endpoint_headers_into_both_clients() -> None: + cfg = ConnectionConfigSync(protocol="http", headers={"X-Base": "base"}) + endpoint = SandboxEndpoint( + endpoint="localhost:44772", + headers={"X-Endpoint": "endpoint"}, + ) + + adapter = CodesAdapterSync(endpoint, cfg) + + assert adapter._httpx_client.headers["X-Base"] == "base" + assert adapter._httpx_client.headers["X-Endpoint"] == "endpoint" + assert adapter._sse_client.headers["X-Base"] == "base" + assert adapter._sse_client.headers["X-Endpoint"] == "endpoint" + + +def test_sync_adapter_endpoint_headers_override_connection_headers() -> None: + cfg = ConnectionConfigSync(protocol="http", headers={"X-Shared": "base"}) + endpoint = SandboxEndpoint( + endpoint="localhost:44772", + headers={"X-Shared": "endpoint"}, + ) + + adapter = CodesAdapterSync(endpoint, cfg) + + assert adapter._httpx_client.headers["X-Shared"] == "endpoint" + assert adapter._sse_client.headers["X-Shared"] == "endpoint"