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

2open issues
3open PRs
33items grouped
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.

These 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

  • Issue#31421[Bug] urllib / internal HTTP to localhost fails (503) when HTTP_PROXY env points to local proxy
  • Issue#59465no_proxy with CIDR notation (e.g. 10.0.0.0/24) silently ignored -- custom/local providers get routed through unrelated HTTP proxy and fail with empty 503

Open PRs3

  • PR#59473fix: honor no_proxy CIDR for provider clients
  • PR#59474fix(proxy): support CIDR notation in no_proxy for custom provider routing
  • PR#59667fix(network): support CIDR notation in no_proxy (#59465)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #12657 fix(agent): respect HTTP_PROXY/HTTPS_PROXY when using custom httpx transport

    merged to main

    files: run_agent.py, scripts/release.py, tests/run_agent/test_create_openai_client_proxy_env.py

    Show 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)

    merged to main

    files: agent/auxiliary_client.py, agent/process_bootstrap.py, scripts/release.py, tests/agent/test_auxiliary_client_proxy_env.py

    Show 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)

    merged to main

    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.00 means 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

At a glance

Subsystem
agent
Merged PRs
#12657, #53974, #115803
Closed issues
11
Attempts
14 PRs closed without merging
Fix link checked
19 Sep 2026

For your agent

The same facts, typed.