Proxy handling bugs and fixes across HTTP clients
NousResearch/hermes-agent · cluster 5454 · agent
Data as of
5 open items. Fix merged in #12657. Check it covers your case.
The short answer
- Known?
- Yes. 33 related items grouped here, 11 issues already closed.
- Fix event
- A fix was merged to main in PR #12657. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #53974, #115803. That is separate from the fix event. - Still open
- 2 issues and 3 PRs.
Next step: read #12657, then check whether your report is one of the 2 still open.
Read #12657Pick up a Lab taskThese issues and pull requests concern proxy configuration behavior—including honoring HTTP_PROXY/HTTPS_PROXY, bypassing proxies for localhost and custom endpoints, and supporting CIDR notation in no_proxy—across various components such as agents, image generation, memory, and auxiliary clients.
Open issues2
Open PRs3
Fixes on record
-
#12657 fix(agent): respect HTTP_PROXY/HTTPS_PROXY when using custom httpx transport
files:
run_agent.py,scripts/release.py,tests/run_agent/test_create_openai_client_proxy_env.pyShow the diff
-
run_agent.py@@ -160,6 +160,20 @@ def __getattr__(self, name): return getattr(self._inner, name) +def _get_proxy_from_env() -> Optional[str]: + """Read proxy URL from environment variables. + + Checks HTTPS_PROXY, HTTP_PROXY, ALL_PROXY (and lowercase variants) in order. + Returns the first valid proxy URL found, or None if no proxy is configured. + """ + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + value = os.environ.get(key, "").strip() + if value: + return value + return None + + def _install_safe_stdio() -> None: """Wrap stdout/stderr so best-effort console output cannot crash the agent.""" for stream_name in ("stdout", "stderr"): @@ -4758,8 +4772,13 @@ def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: boo elif hasattr(_socket, "TCP_KEEPALIVE"): # macOS (uses TCP_KEEPALIVE instead of TCP_KEEPIDLE) _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPALIVE, 30)) + # When a custom transport is provided, httpx won't auto-read proxy + # from env vars (allow_env_proxies = trust_env and transport is None). + # Explicitly read proxy settings to ensure HTTP_PROXY/HTTPS_PROXY work. + _proxy = _get_proxy_from_env() client_kwargs["http_client"] = _httpx.Client( transport=_httpx.HTTPTransport(socket_options=_sock_opts), + proxy=_proxy, ) except Exception: pass # Fall through to default transport if socket opts fail -
scripts/release.py@@ -274,6 +274,7 @@ "[redacted:email]": "JimLiu", "[redacted:email]": "maxchernin", "[redacted:email]": "LVT382009", + "[redacted:email]": "heykb", } -
tests/run_agent/test_create_openai_client_proxy_env.py@@ -0,0 +1,137 @@ +"""Regression guard: _create_openai_client must honor HTTP(S)_PROXY env vars. + +When #11277 re-landed TCP keepalives, ``_create_openai_client`` began passing +a custom ``transport=httpx.HTTPTransport(...)`` to ``httpx.Client``. httpx only +auto-reads ``HTTP_PROXY`` / ``HTTPS_PROXY`` / ``ALL_PROXY`` when +``transport is None`` (see ``Client.__init__``: +``allow_env_proxies = trust_env and transport is None``). As a result, proxy +env vars were silently ignored for the primary chat client, causing requests +to bypass local proxies (Clash, corporate egress, etc.) and hit upstream +directly from the raw interface. + +For users on WSL2 + Clash TUN this surfaced as Cloudflare ``cf-mitigated: +challenge`` 403s against ``chatgpt.com/backend-api/codex`` once they upgraded +past #11277. The fix forwards the proxy URL explicitly to ``httpx.Client`` +while keeping the keepalive-enabled transport in place. + +This test pins that the constructed ``httpx.Client`` mounts an ``HTTPProxy`` +pool when a proxy env var is set, AND that the socket-level keepalive +transport is still installed on the no-proxy default path. +""" +from unittest.mock import patch + +import httpx + +from run_agent import AIAgent, _get_proxy_from_env + + +def _make_agent(): + return AIAgent( + api_key="[redacted:secret-assignment]", + base_url="https://chatgpt.com/backend-api/codex", + provider="openai-codex", + model="gpt-5.4", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + +def _extract_http_client(client_kwargs: dict): + """_create_openai_client calls ``OpenAI(**client_kwargs)``; grab the injected client.""" + return client_kwargs.get("http_client") + + +def test_get_proxy_from_env_prefers_https_then_http_then_all(monkeypatch): + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + monkeypatch.delenv(key, raising=False) + assert _get_proxy_from_env() is None + + monkeypatch.setenv("ALL_PROXY", "http://all:1") + assert _get_proxy_from_env() == "http://all:1" + + monkeypatch.setenv("HTTP_PROXY", "http://http:2") + assert _get_proxy_from_env() == "http://http:2" + + monkeypatch.setenv("HTTPS_PROXY", "http://https:3") + assert _get_proxy_from_env() == "http://https:3" + + +def test_get_proxy_from_env_ignores_blank_values(monkeypatch): + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("HTTPS_PROXY", " ") + monkeypatch.setenv("HTTP_PROXY", "http://real-proxy:8080") + assert _get_proxy_from_env() == "http://real-proxy:8080" + + +@patch("run_agent.OpenAI") +def test_create_openai_client_routes_via_proxy_when_env_set(mock_openai, monkeypatch): + """With HTTPS_PROXY set, the custom httpx.Client must mount an HTTPProxy pool. + + This is the WSL2 + Clash / corporate-egress case. Before the fix, the custom + transport suppressed httpx's env-proxy auto-detection, so requests bypassed + the proxy entirely. + """ + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:7897") + + agent = _make_agent() + kwargs = { + "api_key": "test-key", + "base_url": "https://chatgpt.com/backend-api/codex", + } + 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), ( + "Expected _create_openai_client to inject a keepalive-enabled " + "httpx.Client; got %r" % (http_client,) + ) + # Verify a proxy mount exists. httpx Client(proxy=...) rewrites _mounts so + # the proxied pool (HTTPProxy) sits alongside the base transport. + proxied_pools = [ + type(mount._pool).__name__ + for mount in http_client._mounts.values() + if mount is not None and hasattr(mount, "_pool") + ] + assert "HTTPProxy" in proxied_pools, ( + "Expected httpx.Client to route through HTTPProxy when HTTPS_PROXY is " + "set; found pools: %r" % (proxied_pools,) + ) + http_client.close() + + +@patch("run_agent.OpenAI") +def test_create_openai_client_no_proxy_when_env_unset(mock_openai, monkeypatch): + """Without proxy env vars, the keepalive transport must still be installed + and no HTTPProxy mount should exist.""" + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + monkeypatch.delenv(key, raising=False) + + agent = _make_agent() + kwargs = { + "api_key": "test-key", + "base_url": "https://chatgpt.com/backend-api/codex", + } + 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 env set but httpx.Client still mounted HTTPProxy; " + "pools were %r" % (pool_types,) + ) + http_client.close()
-
-
#53974 fix(auxiliary): env-only proxy policy for OpenAI SDK clients on macOS (#53702)
files:
agent/auxiliary_client.py,agent/process_bootstrap.py,scripts/release.py,tests/agent/test_auxiliary_client_proxy_env.pyShow the diff
-
agent/auxiliary_client.py@@ -102,6 +102,7 @@ def __repr__(self): from agent.credential_pool import load_pool from agent.model_metadata import MINIMUM_CONTEXT_LENGTH, get_model_context_length +from agent.process_bootstrap import build_keepalive_http_client from hermes_cli.config import get_hermes_home from hermes_constants import OPENROUTER_BASE_URL from utils import base_url_host_matches, base_url_hostname, env_float, model_forces_max_completion_tokens, normalize_proxy_env_vars @@ -109,6 +110,23 @@ def __repr__(self): logger = logging.getLogger(__name__) +def _openai_http_client_kwargs( + base_url: Optional[str], + *, + async_mode: bool = False, +) -> Dict[str, Any]: + """Inject keepalive httpx client with env-only proxy (not macOS system proxy).""" + client = build_keepalive_http_client(str(base_url or ""), async_mode=async_mode) + if client is None: + return {} + return {"http_client": client} + + +def _create_openai_client(*, api_key: str, base_url: str, **kwargs: Any) -> Any: + kwargs = {**_openai_http_client_kwargs(base_url), **kwargs} + return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **kwargs) + + # ── Interrupt protection for atomic auxiliary tasks ────────────────────── # Some auxiliary tasks must NOT be aborted mid-flight by a gateway interrupt # (e.g. an incoming user message while the agent is busy). Context @@ -1614,7 +1632,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: _merged_aux = _apply_user_default_headers(extra.get("default_headers")) if _merged_aux: extra["default_headers"] = _merged_aux - _client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra) + _client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, **extra) _client = _maybe_wrap_anthropic(_client, model, api_key, raw_base_url) return _client, model @@ -1654,7 +1672,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: _merged_aux2 = _apply_user_default_headers(extra.get("default_headers")) if _merged_aux2: extra["default_headers"] = _merged_aux2 - _client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra) + _client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, **extra) _client = _maybe_wrap_anthropic(_client, model, api_key, raw_base_url) return _client, model @@ -1672,7 +1690,7 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op if or_key: base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL logger.debug("Auxiliary client: OpenRouter via pool") - return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, + return _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, default_headers=build_or_headers()), model or _OPENROUTER_MODEL # Pool exists but is exhausted (no usable runtime key) — fall through to # the OPENROUTER_API_KEY env-var path rather than failing outright. @@ -1683,7 +1701,7 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op _mark_provider_unhealthy("openrouter", ttl=60) return None, None logger.debug("Auxiliary client: OpenRouter") - return OpenAI(api_key=[redacted:secret-assignment] base_url=OPENROUTER_BASE_URL, + return _create_openai_client(api_key=[redacted:secret-assignment] base_url=OPENROUTER_BASE_URL, default_headers=build_or_headers()), model or _OPENROUTER_MODEL @@ -1776,7 +1794,7 @@ def _try_nous(vision: bool = False) -> Tuple[Optional[OpenAI], Optional[str]]: return None, None base_url = str((nous or {}).get("inference_base_url") or _nous_base_url()).rstrip("/") return ( - OpenAI( + _create_openai_client( api_key=[redacted:secret-assignment] base_url=base_url, ), @@ -2053,7 +2071,7 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]: if _custom_headers: _extra["default_headers"] = _custom_headers if custom_mode == "codex_responses": - real_client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra) + real_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra) return CodexAuxiliaryClient(real_client, model), model if custom_mode == "anthropic_messages": # Third-party Anthropic-compatible gateway (MiniMax, Zhipu GLM, @@ -2067,14 +2085,14 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]: "Custom endpoint declares api_mode=anthropic_messages but the " "anthropic SDK is not installed — falling back to OpenAI-wire." ) - return OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra), model + return _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra), model return ( AnthropicAuxiliaryClient(real_client, model, custom_key, custom_base, is_oauth=False), model, ) # URL-based anthropic detection for custom endpoints that didn't set # api_mode explicitly (e.g. kimi.com/coding reached via custom config). - _fallback_client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra) + _fallback_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra) _fallback_client = _maybe_wrap_anthropic( _fallback_client, model, custom_key, custom_base, custom_mode, ) @@ -2103,7 +2121,7 @@ def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str return None, None api_key, base_url = resolved logger.debug("Auxiliary client: xAI OAuth (%s via Responses API)", model) - real_client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url) + real_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url) return CodexAuxiliaryClient(real_client, model), model @@ -2140,7 +2158,7 @@ def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]: return None, None base_url = _CODEX_AUX_BASE_URL logger.debug("Auxiliary client: Codex OAuth (%s via Responses API)", model) - real_client = OpenAI( + real_client = _create_openai_client( api_key=[redacted:secret-assignment] base_url=base_url, default_headers=_codex_cloudflare_headers(codex_token), @@ -2240,7 +2258,7 @@ def _try_azure_foundry( if _dq: extra["default_query"] = _dq - client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra) + client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra) if runtime_api_mode == "codex_responses": # GPT-5.x / o-series / codex models on Azure Foundry are @@ -3802,6 +3820,10 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False): _merged_async = _apply_user_default_headers(async_kwargs.get("default_headers")) if _merged_async: async_kwargs["default_headers"] = _merged_async + async_kwargs = { + **_openai_http_client_kwargs(sync_base_url, async_mode=True), + **async_kwargs, + } return AsyncOpenAI(**async_kwargs), model @@ -4012,7 +4034,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", "but no Codex OAuth token found (run: hermes model)") return None, None final_model = _normalize_resolved_model(model, provider) - raw_client = OpenAI( + raw_client = _create_openai_client( api_key=[redacted:secret-assignment] base_url=_CODEX_AUX_BASE_URL, default_headers=_codex_cloudflare_headers(codex_token), @@ -4093,7 +4115,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", _merged_custom = _apply_user_default_headers(extra.get("default_headers")) if _merged_custom: extra["default_headers"] = _merged_custom - client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra) + client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra) client = _wrap_if_needed(client, final_model, custom_base, custom_key) return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode else (client, final_model)) @@ -4197,7 +4219,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", _fb_headers = _apply_user_default_headers(_fb_extra.get("default_headers")) if _fb_headers: _fb_extra["default_headers"] = _fb_headers - client = OpenAI(api_key=[redacted:secret-assignment] base_url=_fb_clean, **_fb_extra) + client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_fb_clean, **_fb_extra) return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode else (client, final_model)) sync_anthropic = AnthropicAuxiliaryClient( @@ -4206,7 +4228,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", if async_mode: return AsyncAnthropicAuxiliaryClient(sync_anthropic), final_model return sync_anthropic, final_model - client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base2, **_extra2) + client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base2, **_extra2) # codex_responses or inherited auto-detect (via _wrap_if_needed). # _wrap_if_needed reads the closed-over `api_mode` (the task-level # override). Named-provider entry api_mode=codex_responses also @@ -4348,7 +4370,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", _merged_main = _apply_user_default_headers(headers) if _merged_main: headers = _merged_main - client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, + client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, **({"default_headers": headers} if headers else {})) # Copilot GPT-5+ models (except gpt-5-mini) require the Responses @@ -4884,7 +4906,7 @@ def _refresh_nous_auxiliary_client( return None, model fresh_key, fresh_base_url = runtime - sync_client = OpenAI(api_key=[redacted:secret-assignment] base_url=fresh_base_url) + sync_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=fresh_base_url) final_model = model current_loop = None -
agent/process_bootstrap.py@@ -26,7 +26,7 @@ import os import sys import urllib.request -from typing import Optional +from typing import Any, Optional from utils import base_url_hostname, normalize_proxy_url @@ -142,6 +142,46 @@ def _get_proxy_for_base_url(base_url: Optional[str]) -> Optional[str]: return proxy +def build_keepalive_http_client( + base_url: str = "", + *, + async_mode: bool = False, +) -> Optional[Any]: + """Build an httpx client for OpenAI SDK calls with env-only proxy policy. + + Uses explicit ``HTTPS_PROXY`` / ``NO_PROXY`` env vars via + ``_get_proxy_for_base_url``. A custom transport disables httpx's default + ``trust_env`` path, so macOS system proxy settings from + ``urllib.request.getproxies()`` (which omit the ExceptionsList) are not + applied. Mirrors ``AIAgent._build_keepalive_http_client``. + """ + try: + import httpx + import socket + + if "api.githubcopilot.com" in str(base_url or "").lower(): + client_cls = httpx.AsyncClient if async_mode else httpx.Client + return client_cls() + + sock_opts = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + if hasattr(socket, "TCP_KEEPIDLE"): + sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30)) + sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)) + sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)) + elif hasattr(socket, "TCP_KEEPALIVE"): + sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 30)) + + proxy = _get_proxy_for_base_url(base_url) + transport_cls = httpx.AsyncHTTPTransport if async_mode else httpx.HTTPTransport + client_cls = httpx.AsyncClient if async_mode else httpx.Client + return client_cls( + transport=transport_cls(socket_options=sock_opts), + proxy=proxy, + ) + except Exception: + return None + + def _install_safe_stdio() -> None: """Wrap stdout/stderr so best-effort console output cannot crash the agent.""" for stream_name in ("stdout", "stderr"): @@ -164,4 +204,5 @@ def _install_safe_stdio() -> None: "_install_safe_stdio", "_get_proxy_from_env", "_get_proxy_for_base_url", + "build_keepalive_http_client", ] -
scripts/release.py@@ -50,6 +50,7 @@ "[redacted:email]": "djstunami", # PR #5316 salvage / co-author (suppress transient check_fn flakes so subagents keep file/terminal tools; #21658 / #5304) "[redacted:email]": "jmmaloney4", # PR #25206 salvage (re-select credential pool on primary runtime restore; #25205) "[redacted:email]": "dalenguyen", # PR #53678 salvage (strip VIRTUAL_ENV/CONDA_PREFIX from terminal subprocess env; #23473) + "[redacted:email]": "HexLab98", # PR #53863 salvage (env-only proxy policy for auxiliary OpenAI clients on macOS; #53702) "[redacted:email]": "Blaryxoff", # PR #32602 salvage (deep-merge PUT /api/config to preserve unrelated sections; #13396) "[redacted:email]": "DiamondEyesFox", # PR #53351 salvage (rebaseline in-place compression flushes to prevent duplicate compacted rows; #9096) "[redacted:email]": "rlaope", # PR #35075 salvage (align cron invisible-unicode set with install-time scanner; #35075) -
tests/agent/test_auxiliary_client_proxy_env.py@@ -0,0 +1,98 @@ +"""Regression guard: auxiliary OpenAI clients must use env-only proxy policy. + +On macOS, httpx with default ``trust_env=True`` reads system proxy settings +via ``urllib.request.getproxies()`` but not the macOS proxy exception list. +Auxiliary clients (vision, title generation, etc.) must mirror the main +agent: explicit ``HTTPS_PROXY`` / ``NO_PROXY`` env vars only, via a custom +keepalive transport that suppresses automatic system-proxy detection. +""" +from unittest.mock import patch + +import httpx + +from agent.auxiliary_client import _create_openai_client, _openai_http_client_kwargs +from agent.process_bootstrap import _get_proxy_for_base_url + + +def _pool_types(http_client) -> list: + return [ + type(mount._pool).__name__ + for mount in http_client._mounts.values() + if mount is not None and hasattr(mount, "_pool") + ] + + +@patch("agent.auxiliary_client.OpenAI") +def test_create_openai_client_routes_via_env_proxy(mock_openai, 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://127.0.0.1:7897") + + _create_openai_client( + api_key="[redacted:secret-assignment]", + base_url="https://litellm.internal.example.com/v1", + ) + + http_client = mock_openai.call_args.kwargs.get("http_client") + assert isinstance(http_client, httpx.Client) + assert "HTTPProxy" in _pool_types(http_client) + http_client.close() + + +@patch("agent.auxiliary_client.OpenAI") +def test_create_openai_client_no_proxy_when_env_unset(mock_openai, 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) + + _create_openai_client( + api_key="[redacted:secret-assignment]", + base_url="https://litellm.internal.example.com/v1", + ) + + http_client = mock_openai.call_args.kwargs.get("http_client") + assert isinstance(http_client, httpx.Client) + assert "HTTPProxy" not in _pool_types(http_client) + http_client.close() + + +@patch("agent.auxiliary_client.OpenAI") +def test_create_openai_client_ignores_macos_system_proxy(mock_openai, monkeypatch): + """System proxy from getproxies() must not apply when env vars are unset.""" + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy", "NO_PROXY", "no_proxy"): + monkeypatch.delenv(key, raising=False) + + with patch( + "urllib.request.getproxies", + return_value={"http": "http://127.0.0.1:7897", "https": "http://127.0.0.1:7897"}, + ): + _create_openai_client( + api_key="[redacted:secret-assignment]", + base_url="https://litellm.internal.example.com/v1", + ) + + http_client = mock_openai.call_args.kwargs.get("http_client") + assert isinstance(http_client, httpx.Client) + assert "HTTPProxy" not in _pool_types(http_client) + http_client.close() + + +def test_get_proxy_for_base_url_respects_no_proxy(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://127.0.0.1:7897") + monkeypatch.setenv("NO_PROXY", "internal.example.com") + + assert _get_proxy_for_base_url("https://litellm.internal.example.com/v1") is None + assert _get_proxy_for_base_url("https://api.openai.com/v1") == "http://127.0.0.1:7897" + + +def test_openai_http_client_kwargs_async_mode(): + kwargs = _openai_http_client_kwargs( + "https://litellm.internal.example.com/v1", + async_mode=True, + ) + assert isinstance(kwargs["http_client"], httpx.AsyncClient)
-
-
#115803 feat(image_gen): OpenAI image provider honours image_gen.openai.base_url/key_env, no project-header 403, no macOS system-proxy detour (#65309, #97928, #13798, #60748, #64888, salvage #65312 #64893 #60749)
files:
agent/video_gen_provider.py,plugins/image_gen/_common.py,plugins/image_gen/openai/__init__.py,tests/plugins/image_gen/test_openai_provider.py,tests/plugins/video_gen/test_deepinfra_provider.py,website/docs/user-guide/features/image-generation.md
14 PRs closed without merging
From the close comments: done elsewhere 4 · duplicate 4 · superseded by another PR 2 · merged via another change 1 · stale or abandoned 1 · other 2
-
#11664 fix(agent): respect proxy env in general client path · done elsewhere →
023208b,cbc39a8,#11609 -
#11733 fix(agent): honor configured proxy in OpenAI client path (#11609) · superseded by another PR →
8dbed63,#12657,#11609 -
#12010 fix(agent): honor proxy-aware OpenAI client routing (#5454) · superseded by another PR →
8324dd1 -
#12110 fix(client): inject explicit httpx client for custom endpoints to prevent stale connection errors · done elsewhere →
8c47898,#10324,#11277,#11969 -
#14790 fix(agent): respect no_proxy in _build_keepalive_http_client · done elsewhere →
cbc39a8,166b960,#13606
Show 9 more
-
#25324 fix(auxiliary): bypass proxies for local OpenAI endpoints · done elsewhere →
073847c,#25319 -
#25326 [Bug]: Avoid system proxy on localhost auxiliary OpenAI clients · other →
#25323,#25324 -
#25334 fix(agent): bypass system proxy for localhost auxiliary clients · other →
#25324 -
#25770 fix(auxiliary_client): bypass system proxy for localhost requests · duplicate →
#25324,#25319 -
#40021 fix(memory): bypass proxies for local Hindsight · stale or abandoned →
9d799e0,4cbf862,dc75038,#119888 -
#59505 fix(proxy): honor CIDR notation in no_proxy for custom providers · duplicate →
#59474,#59473,#59465 -
#59558 fix(agent): support CIDR notation in NO_PROXY for custom provider proxy bypass (#59465) · duplicate →
#59474,#59473,#59465 -
#59934 fix(agent): honor NO_PROXY CIDR ranges · duplicate →
#59474,#59473,#59465 -
#64893 fix(image-gen): bypass macOS system proxy for custom endpoints · merged via another change →
e113c1b,#115803,#65309,#97928,#13798,#60748,#64888,#65312,#64893,#60749
Closed issues · 11
- Closed#5454Proxy support for LLM API calls
- Closed#10980[Setup]: Copilot // API call failed (attempt 1/3): APIConnectionError
- Closed#11609[Bug] All API calls timeout on WSL with HTTP proxy — custom httpx transport bypasses proxy env vars in general agent path
- Closed#11969[Bug]: APIConnectionError with custom endpoint ~80% failure rate while direct Python SDK works 100%
- Closed#14451Bug: _build_keepalive_http_client ignores no_proxy — localhost requests go through proxy
- Closed#14966_build_keepalive_http_client() ignores NO_PROXY, causing 502 on local endpoints
- Closed#25319Auxiliary client returns 502 when Windows system proxy is configured (httpx trust_env picks up proxy for localhost)
- Closed#39524[Bug]: Transient APIConnectionError on custom — rebuilt client, waiting 6s before one last primary attempt.
- Closed#40006[Bug]: Hindsight plugin doesn't bypass SOCK5 proxy even if the config.json directs to localhost/127.0.0.1
- Closed#53702Bug: Auxiliary vision fails with Connection error on macOS when system proxy (Clash/V2Ray) is configured
- Closed#64888[Bug]: OpenAI image generation ignores macOS proxy exceptions for local/custom base URLs
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.05: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab