diff --git a/tests/run_agent/test_create_openai_client_proxy_env.py b/tests/run_agent/test_create_openai_client_proxy_env.py index 9ef8e3dcd..9bd4ab929 100644 --- a/tests/run_agent/test_create_openai_client_proxy_env.py +++ b/tests/run_agent/test_create_openai_client_proxy_env.py @@ -22,7 +22,7 @@ from unittest.mock import patch import httpx -from run_agent import AIAgent, _get_proxy_from_env +from run_agent import AIAgent, _get_proxy_from_env, _get_proxy_for_base_url def _make_agent(): @@ -143,3 +143,78 @@ def test_create_openai_client_no_proxy_when_env_unset(mock_openai, monkeypatch): "pools were %r" % (pool_types,) ) http_client.close() + + +def test_get_proxy_for_base_url_returns_none_when_host_bypassed(monkeypatch): + """NO_PROXY must suppress the proxy for matching base_urls. + + Regression for #14966: users running a local inference endpoint + (Ollama, LM Studio, llama.cpp) with a global HTTPS_PROXY would see + the keepalive client route loopback traffic through the proxy, which + typically answers 502 for local hosts. NO_PROXY should opt those + hosts out via stdlib ``urllib.request.proxy_bypass_environment``. + """ + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy", + "NO_PROXY", "no_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:7897") + monkeypatch.setenv("NO_PROXY", "localhost,127.0.0.1,192.168.0.0/16") + + # Local endpoint — must bypass the proxy. + assert _get_proxy_for_base_url("http://127.0.0.1:11434/v1") is None + assert _get_proxy_for_base_url("http://localhost:1234/v1") is None + + # Non-local endpoint — proxy still applies. + assert _get_proxy_for_base_url("https://api.openai.com/v1") == "http://127.0.0.1:7897" + + +def test_get_proxy_for_base_url_returns_proxy_when_no_proxy_unset(monkeypatch): + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy", + "NO_PROXY", "no_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("HTTPS_PROXY", "http://corp:8080") + assert _get_proxy_for_base_url("http://127.0.0.1:11434/v1") == "http://corp:8080" + + +def test_get_proxy_for_base_url_returns_none_when_proxy_unset(monkeypatch): + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy", + "NO_PROXY", "no_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("NO_PROXY", "localhost,127.0.0.1") + assert _get_proxy_for_base_url("http://127.0.0.1:11434/v1") is None + assert _get_proxy_for_base_url("https://api.openai.com/v1") is None + + +@patch("run_agent.OpenAI") +def test_create_openai_client_bypasses_proxy_for_no_proxy_host(mock_openai, monkeypatch): + """E2E: with HTTPS_PROXY + NO_PROXY=localhost, a local base_url gets a + keepalive client with NO HTTPProxy mount.""" + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy", + "NO_PROXY", "no_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:7897") + monkeypatch.setenv("NO_PROXY", "localhost,127.0.0.1") + + agent = _make_agent() + kwargs = { + "api_key": "***", + "base_url": "http://127.0.0.1:11434/v1", + } + agent._create_openai_client(kwargs, reason="test", shared=False) + + forwarded = mock_openai.call_args.kwargs + http_client = _extract_http_client(forwarded) + assert isinstance(http_client, httpx.Client) + pool_types = [ + type(mount._pool).__name__ + for mount in http_client._mounts.values() + if mount is not None and hasattr(mount, "_pool") + ] + assert "HTTPProxy" not in pool_types, ( + "NO_PROXY host must not route through HTTPProxy; pools were %r" % (pool_types,) + ) + http_client.close()