# NousResearch/hermes-agent · Proxy handling bugs and fixes across HTTP clients

## Open now

- 2 issues, 3 PRs open on GitHub.
- #31421: \[Bug\] urllib / internal HTTP to localhost fails (503) when HTTP_PROXY env points to local proxy
- #59465: no_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
- PR #59473: fix: honor no_proxy CIDR for provider clients
- PR #59474: fix(proxy): support CIDR notation in no_proxy for custom provider routing
- PR #59667: fix(network): support CIDR notation in no_proxy (#59465)

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.

## TL;DR

- 33 member(s): 2 open issue(s), 11 closed issue(s), 3 open PR(s), 3 merged PR(s), 14 PR(s) closed without merging.
- A fix was merged to main in PR #12657. Verify it addresses your case.
- **Recommended:** read PR #12657 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-53974,pr-115803`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

- #31421: \[Bug\] urllib / internal HTTP to localhost fails (503) when HTTP_PROXY env points to local proxy
- #59465: no_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

## Closed / duplicate issues

- #5454: Proxy support for LLM API calls
- #10980: \[Setup\]: Copilot // API call failed (attempt 1/3): APIConnectionError
- #11609: \[Bug\] All API calls timeout on WSL with HTTP proxy — custom httpx transport bypasses proxy env vars in general agent path
- #11969: \[Bug\]: APIConnectionError with custom endpoint ~80% failure rate while direct Python SDK works 100%
- #14451: Bug: _build_keepalive_http_client ignores no_proxy — localhost requests go through proxy
- #14966: _build_keepalive_http_client() ignores NO_PROXY, causing 502 on local endpoints
- #25319: Auxiliary client returns 502 when Windows system proxy is configured (httpx trust_env picks up proxy for localhost)
- #39524: \[Bug\]: Transient APIConnectionError on custom — rebuilt client, waiting 6s before one last primary attempt.
- #40006: \[Bug\]: Hindsight plugin doesn't bypass SOCK5 proxy even if the config.json directs to localhost/127.0.0.1
- #53702: Bug: Auxiliary vision fails with Connection error on macOS when system proxy (Clash/V2Ray) is configured
- #64888: \[Bug\]: OpenAI image generation ignores macOS proxy exceptions for local/custom base URLs

## Prior & competing PRs

- **#11664: fix(agent): respect proxy env in general client path**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `023208b17`, `cbc39a867`, `#11609`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#12010: fix(agent): honor proxy-aware OpenAI client routing (#5454)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `8324dd19c`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#12657: fix(agent): respect HTTP_PROXY/HTTPS_PROXY when using custom httpx transport**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `run_agent.py`, `scripts/release.py`, `tests/run_agent/test_create_openai_client_proxy_env.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -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"):
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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()
    ```
- **#14790: fix(agent): respect no_proxy in _build_keepalive_http_client**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `cbc39a867`, `166b960fe`, `#13606`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#25324: fix(auxiliary): bypass proxies for local OpenAI endpoints**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `073847c0f206c93ee4471ce0a6fe01e12d14c7bd`, `#25319`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#25326: \[Bug\]: Avoid system proxy on localhost auxiliary OpenAI clients**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#25323`, `#25324`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#53974: fix(auxiliary): env-only proxy policy for OpenAI SDK clients on macOS (#53702)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `agent/process_bootstrap.py`, `scripts/release.py`, `tests/agent/test_auxiliary_client_proxy_env.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
     
    ```

    ```diff
    @@ -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
     
    ```

    ```diff
    @@ -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.
    ```

    ```diff
    @@ -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
     
     
    ```

    ```diff
    @@ -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,
             ),
    ```

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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,
         )
    ```

    ```diff
    @@ -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
     
     
    ```

    ```diff
    @@ -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),
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
     
     
    ```

    ```diff
    @@ -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),
    ```

    ```diff
    @@ -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))
    ```

    ```diff
    @@ -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(
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
     
    ```

    ```diff
    @@ -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"):
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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 · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`
- **#11733: fix(agent): honor configured proxy in OpenAI client path (#11609)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `8dbed632eeb8959650a8b264e690704b6e3b9b5c`, `#12657`, `#11609`
- **#12110: fix(client): inject explicit httpx client for custom endpoints to prevent stale connection errors**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8c478983e`, `8c478983ed0ec5609212950d5044398dd4d27a5a`, `#10324`, `#11277`, `#11969`
- **#25334: fix(agent): bypass system proxy for localhost auxiliary clients**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#25324`
- **#25770: fix(auxiliary_client): bypass system proxy for localhost requests**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#25324`, `#25324`, `#25319`
- **#40021: fix(memory): bypass proxies for local Hindsight**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`; refs `9d799e0531c`, `4cbf862abe4`, `dc75038866a3966b5bf5c82ff57c7a758bb4070a`, `4cbf862abe4`, `#119888`, `#119888`
- **#59473: fix: honor no_proxy CIDR for provider clients**. open
  - maintainer direction: category `silent_close`; implication `active`
  - files: `agent/process_bootstrap.py`, `gateway/platforms/base.py`, `tests/run_agent/test_create_openai_client_proxy_env.py`, `utils.py`
- **#59474: fix(proxy): support CIDR notation in no_proxy for custom provider routing**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/process_bootstrap.py`, `scripts/release.py`, `tests/agent/test_auxiliary_client_proxy_env.py`
- **#59505: fix(proxy): honor CIDR notation in no_proxy for custom providers**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59474`, `#59474`, `#59473`, `#59465`
- **#59558: fix(agent): support CIDR notation in NO_PROXY for custom provider proxy bypass (#59465)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59474`, `#59474`, `#59473`, `#59465`
- **#59667: fix(network): support CIDR notation in no_proxy (#59465)**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59474`, `#59465`, `#59474`, `#59505`, `#59558`, `#59474`, `#59473`
  - files: `agent/process_bootstrap.py`, `tests/test_no_proxy_cidr.py`
- **#59934: fix(agent): honor NO_PROXY CIDR ranges**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59474`, `#59474`, `#59473`, `#59465`
- **#64893: fix(image-gen): bypass macOS system proxy for custom endpoints**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `e113c1b3aff3`, `#115803`, `#65309`, `#97928`, `#13798`, `#60748`, `#64888`, `#65312`, `#64893`, `#60749`

## Fix status

- Merged to main in PR #12657 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-19`.
- Subsystem: `agent`
