From 72b8fc806fabeacf320ab09201ace2ca57768b8f Mon Sep 17 00:00:00 2001 From: michaelxer Date: Tue, 16 Jun 2026 19:51:08 +0700 Subject: [PATCH] fix(providers): disable proxy for local endpoints, respect env proxy for cloud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the host has HTTP_PROXY / HTTPS_PROXY / ALL_PROXY set, httpx routes all traffic through the proxy — including requests to localhost or LAN addresses that the proxy typically cannot reach. This breaks local model servers (Ollama, llama.cpp, vLLM) silently. - Local endpoints: pass transport=httpx.AsyncHTTPTransport(proxy=None) so proxy env vars are ignored for local traffic. - Cloud endpoints: pass trust_env=True so corporate/VPN proxies work without explicit configuration. Fixes #4366 --- nanobot/providers/openai_compat_provider.py | 17 +++++- tests/providers/test_openai_compat_timeout.py | 5 +- tests/providers/test_proxy_env.py | 56 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/providers/test_proxy_env.py diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index ed054a37..a3d3772c 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -406,9 +406,24 @@ class OpenAICompatProvider(LLMProvider): # opening a fresh connection for each request, which is cheap on a # LAN. Cloud providers benefit from keepalive, so we leave the # default pool settings for them. + # + # Also disable proxy for local endpoints: when the host has + # HTTP_PROXY / HTTPS_PROXY / ALL_PROXY set, httpx would try to + # route local traffic through the proxy, which typically cannot + # reach localhost or LAN addresses. + _local_limits = httpx.Limits(keepalive_expiry=0) http_client = httpx.AsyncClient( - limits=httpx.Limits(keepalive_expiry=0), + limits=_local_limits, timeout=timeout_s, + transport=httpx.AsyncHTTPTransport(proxy=None, limits=_local_limits), + ) + else: + # Cloud endpoints: respect proxy environment variables + # (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY) so corporate + # or VPN proxies work without explicit configuration. + http_client = httpx.AsyncClient( + timeout=timeout_s, + trust_env=True, ) self._client = AsyncOpenAI( api_key=self._api_key_for_client, diff --git a/tests/providers/test_openai_compat_timeout.py b/tests/providers/test_openai_compat_timeout.py index 98241fcd..bf685158 100644 --- a/tests/providers/test_openai_compat_timeout.py +++ b/tests/providers/test_openai_compat_timeout.py @@ -16,7 +16,10 @@ async def test_openai_compat_provider_defers_sdk_client_until_first_use() -> Non kwargs = mock_async_openai.call_args.kwargs _assert_openai_compat_timeout(kwargs["timeout"]) - assert kwargs["http_client"] is None + # Cloud endpoints get an httpx client with trust_env=True to respect + # proxy environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY). + assert kwargs["http_client"] is not None + assert kwargs["http_client"]._trust_env is True async def test_openai_compat_provider_sets_timeout_on_local_http_client() -> None: diff --git a/tests/providers/test_proxy_env.py b/tests/providers/test_proxy_env.py new file mode 100644 index 00000000..640c9dfb --- /dev/null +++ b/tests/providers/test_proxy_env.py @@ -0,0 +1,56 @@ +"""Tests for proxy environment variable handling in OpenAICompatProvider.""" + +from unittest.mock import MagicMock + +import httpx + +from nanobot.providers.openai_compat_provider import OpenAICompatProvider + + +def _make_spec(is_local: bool = False) -> MagicMock: + spec = MagicMock() + spec.is_local = is_local + return spec + + +class TestLocalEndpointProxyDisabled: + """Local endpoints must bypass proxy to avoid routing LAN traffic through it.""" + + async def test_local_disables_proxy(self): + spec = _make_spec(is_local=True) + spec.env_key = "" + spec.default_api_base = "http://localhost:11434/v1" + provider = OpenAICompatProvider( + api_key="test", api_base="http://localhost:11434/v1", spec=spec, + ) + await provider._ensure_client() + transport = provider._client._client._transport + # The transport should be an AsyncHTTPTransport with proxy=None + assert isinstance(transport, httpx.AsyncHTTPTransport) + + async def test_lan_ip_disables_proxy(self): + spec = _make_spec(is_local=False) + spec.env_key = "" + spec.default_api_base = None + provider = OpenAICompatProvider( + api_key="test", api_base="http://192.168.8.188:1234/v1", spec=spec, + ) + await provider._ensure_client() + transport = provider._client._client._transport + assert isinstance(transport, httpx.AsyncHTTPTransport) + + +class TestCloudEndpointProxyEnabled: + """Cloud endpoints must respect proxy env vars for corporate/VPN proxies.""" + + async def test_cloud_respects_trust_env(self): + spec = _make_spec(is_local=False) + spec.env_key = "" + spec.default_api_base = "https://api.openai.com/v1" + provider = OpenAICompatProvider( + api_key="test", api_base=None, spec=spec, + ) + await provider._ensure_client() + client = provider._client._client + # trust_env should be True so httpx reads HTTP_PROXY etc. + assert client._trust_env is True