{"repo":"NousResearch/hermes-agent","public_id":"5454","display_label":"Proxy handling bugs and fixes across HTTP clients","summary":"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.","subsystem":"agent","counts":{"members":33,"open_issues":2,"open_issues_distinct":2,"open_prs":3,"closed_issues":11,"merged_prs":3,"attempt_prs":17},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":12657},"resolution":{"state":"partially_resolved","by":"pr-53974,pr-115803"},"freshness":{"state":"verified","last_verified_at":"2026-09-19T16:51:13+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.052878787878787865},"fix_edges":[{"issue_number":11609,"pr_number":12657,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":13798,"pr_number":115803,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":18793,"pr_number":115803,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":53702,"pr_number":53974,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":60748,"pr_number":115803,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":64888,"pr_number":115803,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":65309,"pr_number":115803,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":97928,"pr_number":115803,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":5454,"title":"Proxy support for LLM API calls","url":null,"state":"closed"},{"kind":"issue","number":10980,"title":"[Setup]: Copilot // API call failed (attempt 1/3): APIConnectionError","url":null,"state":"closed"},{"kind":"issue","number":11609,"title":"[Bug] All API calls timeout on WSL with HTTP proxy — custom httpx transport bypasses proxy env vars in general agent path","url":null,"state":"closed"},{"kind":"issue","number":11969,"title":"[Bug]: APIConnectionError with custom endpoint ~80% failure rate while direct Python SDK works 100%","url":null,"state":"closed"},{"kind":"issue","number":14451,"title":"Bug: _build_keepalive_http_client ignores no_proxy — localhost requests go through proxy","url":null,"state":"closed"},{"kind":"issue","number":14966,"title":"_build_keepalive_http_client() ignores NO_PROXY, causing 502 on local endpoints","url":null,"state":"closed"},{"kind":"issue","number":25319,"title":"Auxiliary client returns 502 when Windows system proxy is configured (httpx trust_env picks up proxy for localhost)","url":null,"state":"closed"},{"kind":"issue","number":31421,"title":"[Bug] urllib / internal HTTP to localhost fails (503) when HTTP_PROXY env points to local proxy","url":null,"state":"open"},{"kind":"issue","number":39524,"title":"[Bug]: Transient APIConnectionError on custom — rebuilt client, waiting 6s before one last primary attempt.","url":null,"state":"closed"},{"kind":"issue","number":40006,"title":"[Bug]: Hindsight plugin doesn't bypass SOCK5 proxy even if the config.json directs to localhost/127.0.0.1","url":null,"state":"closed"},{"kind":"issue","number":53702,"title":"Bug: Auxiliary vision fails with Connection error on macOS when system proxy (Clash/V2Ray) is configured","url":null,"state":"closed"},{"kind":"issue","number":59465,"title":"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","url":null,"state":"open"},{"kind":"issue","number":64888,"title":"[Bug]: OpenAI image generation ignores macOS proxy exceptions for local/custom base URLs","url":null,"state":"closed"},{"kind":"pr","number":11664,"title":"fix(agent): respect proxy env in general client path","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"023208b17","number":0},{"kind":"commit","value":"cbc39a867","number":0},{"kind":"pr","value":"#11609","number":11609}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":11733,"title":"fix(agent): honor configured proxy in OpenAI client path (#11609)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8dbed632eeb8959650a8b264e690704b6e3b9b5c","number":0},{"kind":"pr","value":"#12657","number":12657},{"kind":"pr","value":"#11609","number":11609}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":12010,"title":"fix(agent): honor proxy-aware OpenAI client routing (#5454)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8324dd19c","number":0}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":12110,"title":"fix(client): inject explicit httpx client for custom endpoints to prevent stale connection errors","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8c478983e","number":0},{"kind":"commit","value":"8c478983ed0ec5609212950d5044398dd4d27a5a","number":0},{"kind":"pr","value":"#10324","number":10324},{"kind":"pr","value":"#11277","number":11277},{"kind":"pr","value":"#11969","number":11969}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":12657,"title":"fix(agent): respect HTTP_PROXY/HTTPS_PROXY when using custom httpx transport","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","scripts/release.py","tests/run_agent/test_create_openai_client_proxy_env.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -160,6 +160,20 @@ def __getattr__(self, name):\n         return getattr(self._inner, name)\n \n \n+def _get_proxy_from_env() -> Optional[str]:\n+    \"\"\"Read proxy URL from environment variables.\n+\n+    Checks HTTPS_PROXY, HTTP_PROXY, ALL_PROXY (and lowercase variants) in order.\n+    Returns the first valid proxy URL found, or None if no proxy is configured.\n+    \"\"\"\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\"):\n+        value = os.environ.get(key, \"\").strip()\n+        if value:\n+            return value\n+    return None\n+\n+\n def _install_safe_stdio() -> None:\n     \"\"\"Wrap stdout/stderr so best-effort console output cannot crash the agent.\"\"\"\n     for stream_name in (\"stdout\", \"stderr\"):","@@ -4758,8 +4772,13 @@ def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: boo\n                 elif hasattr(_socket, \"TCP_KEEPALIVE\"):\n                     # macOS (uses TCP_KEEPALIVE instead of TCP_KEEPIDLE)\n                     _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPALIVE, 30))\n+                # When a custom transport is provided, httpx won't auto-read proxy\n+                # from env vars (allow_env_proxies = trust_env and transport is None).\n+                # Explicitly read proxy settings to ensure HTTP_PROXY/HTTPS_PROXY work.\n+                _proxy = _get_proxy_from_env()\n                 client_kwargs[\"http_client\"] = _httpx.Client(\n                     transport=_httpx.HTTPTransport(socket_options=_sock_opts),\n+                    proxy=_proxy,\n                 )\n             except Exception:\n                 pass  # Fall through to default transport if socket opts fail"]},{"file":"scripts/release.py","hunks":["@@ -274,6 +274,7 @@\n     \"[redacted:email]\": \"JimLiu\",\n     \"[redacted:email]\": \"maxchernin\",\n     \"[redacted:email]\": \"LVT382009\",\n+    \"[redacted:email]\": \"heykb\",\n }\n \n "]},{"file":"tests/run_agent/test_create_openai_client_proxy_env.py","hunks":["@@ -0,0 +1,137 @@\n+\"\"\"Regression guard: _create_openai_client must honor HTTP(S)_PROXY env vars.\n+\n+When #11277 re-landed TCP keepalives, ``_create_openai_client`` began passing\n+a custom ``transport=httpx.HTTPTransport(...)`` to ``httpx.Client``. httpx only\n+auto-reads ``HTTP_PROXY`` / ``HTTPS_PROXY`` / ``ALL_PROXY`` when\n+``transport is None`` (see ``Client.__init__``:\n+``allow_env_proxies = trust_env and transport is None``). As a result, proxy\n+env vars were silently ignored for the primary chat client, causing requests\n+to bypass local proxies (Clash, corporate egress, etc.) and hit upstream\n+directly from the raw interface.\n+\n+For users on WSL2 + Clash TUN this surfaced as Cloudflare ``cf-mitigated:\n+challenge`` 403s against ``chatgpt.com/backend-api/codex`` once they upgraded\n+past #11277. The fix forwards the proxy URL explicitly to ``httpx.Client``\n+while keeping the keepalive-enabled transport in place.\n+\n+This test pins that the constructed ``httpx.Client`` mounts an ``HTTPProxy``\n+pool when a proxy env var is set, AND that the socket-level keepalive\n+transport is still installed on the no-proxy default path.\n+\"\"\"\n+from unittest.mock import patch\n+\n+import httpx\n+\n+from run_agent import AIAgent, _get_proxy_from_env\n+\n+\n+def _make_agent():\n+    return AIAgent(\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://chatgpt.com/backend-api/codex\",\n+        provider=\"openai-codex\",\n+        model=\"gpt-5.4\",\n+        quiet_mode=True,\n+        skip_context_files=True,\n+        skip_memory=True,\n+    )\n+\n+\n+def _extract_http_client(client_kwargs: dict):\n+    \"\"\"_create_openai_client calls ``OpenAI(**client_kwargs)``; grab the injected client.\"\"\"\n+    return client_kwargs.get(\"http_client\")\n+\n+\n+def test_get_proxy_from_env_prefers_https_then_http_then_all(monkeypatch):\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+    assert _get_proxy_from_env() is None\n+\n+    monkeypatch.setenv(\"ALL_PROXY\", \"http://all:1\")\n+    assert _get_proxy_from_env() == \"http://all:1\"\n+\n+    monkeypatch.setenv(\"HTTP_PROXY\", \"http://http:2\")\n+    assert _get_proxy_from_env() == \"http://http:2\"\n+\n+    monkeypatch.setenv(\"HTTPS_PROXY\", \"http://https:3\")\n+    assert _get_proxy_from_env() == \"http://https:3\"\n+\n+\n+def test_get_proxy_from_env_ignores_blank_values(monkeypatch):\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+    monkeypatch.setenv(\"HTTPS_PROXY\", \"   \")\n+    monkeypatch.setenv(\"HTTP_PROXY\", \"http://real-proxy:8080\")\n+    assert _get_proxy_from_env() == \"http://real-proxy:8080\"\n+\n+\n+@patch(\"run_agent.OpenAI\")\n+def test_create_openai_client_routes_via_proxy_when_env_set(mock_openai, monkeypatch):\n+    \"\"\"With HTTPS_PROXY set, the custom httpx.Client must mount an HTTPProxy pool.\n+\n+    This is the WSL2 + Clash / corporate-egress case. Before the fix, the custom\n+    transport suppressed httpx's env-proxy auto-detection, so requests bypassed\n+    the proxy entirely.\n+    \"\"\"\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+    monkeypatch.setenv(\"HTTPS_PROXY\", \"http://127.0.0.1:7897\")\n+\n+    agent = _make_agent()\n+    kwargs = {\n+        \"api_key\": \"test-key\",\n+        \"base_url\": \"https://chatgpt.com/backend-api/codex\",\n+    }\n+    agent._create_openai_client(kwargs, reason=\"test\", shared=False)\n+\n+    forwarded = mock_openai.call_args.kwargs\n+    http_client = _extract_http_client(forwarded)\n+    assert isinstance(http_client, httpx.Client), (\n+        \"Expected _create_openai_client to inject a keepalive-enabled \"\n+        \"httpx.Client; got %r\" % (http_client,)\n+    )\n+    # Verify a proxy mount exists. httpx Client(proxy=...) rewrites _mounts so\n+    # the proxied pool (HTTPProxy) sits alongside the base transport.\n+    proxied_pools = [\n+        type(mount._pool).__name__\n+        for mount in http_client._mounts.values()\n+        if mount is not None and hasattr(mount, \"_pool\")\n+    ]\n+    assert \"HTTPProxy\" in proxied_pools, (\n+        \"Expected httpx.Client to route through HTTPProxy when HTTPS_PROXY is \"\n+        \"set; found pools: %r\" % (proxied_pools,)\n+    )\n+    http_client.close()\n+\n+\n+@patch(\"run_agent.OpenAI\")\n+def test_create_openai_client_no_proxy_when_env_unset(mock_openai, monkeypatch):\n+    \"\"\"Without proxy env vars, the keepalive transport must still be installed\n+    and no HTTPProxy mount should exist.\"\"\"\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+\n+    agent = _make_agent()\n+    kwargs = {\n+        \"api_key\": \"test-key\",\n+        \"base_url\": \"https://chatgpt.com/backend-api/codex\",\n+    }\n+    agent._create_openai_client(kwargs, reason=\"test\", shared=False)\n+\n+    forwarded = mock_openai.call_args.kwargs\n+    http_client = _extract_http_client(forwarded)\n+    assert isinstance(http_client, httpx.Client)\n+    pool_types = [\n+        type(mount._pool).__name__\n+        for mount in http_client._mounts.values()\n+        if mount is not None and hasattr(mount, \"_pool\")\n+    ]\n+    assert \"HTTPProxy\" not in pool_types, (\n+        \"No proxy env set but httpx.Client still mounted HTTPProxy; \"\n+        \"pools were %r\" % (pool_types,)\n+    )\n+    http_client.close()\n"]}]},{"kind":"pr","number":14790,"title":"fix(agent): respect no_proxy in _build_keepalive_http_client","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"cbc39a867","number":0},{"kind":"commit","value":"166b960fe","number":0},{"kind":"pr","value":"#13606","number":13606}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":25324,"title":"fix(auxiliary): bypass proxies for local OpenAI endpoints","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"073847c0f206c93ee4471ce0a6fe01e12d14c7bd","number":0},{"kind":"pr","value":"#25319","number":25319}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":25326,"title":"[Bug]: Avoid system proxy on localhost auxiliary OpenAI clients","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25323","number":25323},{"kind":"pr","value":"#25324","number":25324}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":25334,"title":"fix(agent): bypass system proxy for localhost auxiliary clients","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25324","number":25324}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":25770,"title":"fix(auxiliary_client): bypass system proxy for localhost requests","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25324","number":25324},{"kind":"pr","value":"#25324","number":25324},{"kind":"pr","value":"#25319","number":25319}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":40021,"title":"fix(memory): bypass proxies for local Hindsight","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9d799e0531c","number":0},{"kind":"commit","value":"4cbf862abe4","number":0},{"kind":"commit","value":"dc75038866a3966b5bf5c82ff57c7a758bb4070a","number":0},{"kind":"commit","value":"4cbf862abe4","number":0},{"kind":"pr","value":"#119888","number":119888},{"kind":"pr","value":"#119888","number":119888}],"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":53974,"title":"fix(auxiliary): env-only proxy policy for OpenAI SDK clients on macOS (#53702)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/process_bootstrap.py","scripts/release.py","tests/agent/test_auxiliary_client_proxy_env.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -102,6 +102,7 @@ def __repr__(self):\n \n from agent.credential_pool import load_pool\n from agent.model_metadata import MINIMUM_CONTEXT_LENGTH, get_model_context_length\n+from agent.process_bootstrap import build_keepalive_http_client\n from hermes_cli.config import get_hermes_home\n from hermes_constants import OPENROUTER_BASE_URL\n 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):\n logger = logging.getLogger(__name__)\n \n \n+def _openai_http_client_kwargs(\n+    base_url: Optional[str],\n+    *,\n+    async_mode: bool = False,\n+) -> Dict[str, Any]:\n+    \"\"\"Inject keepalive httpx client with env-only proxy (not macOS system proxy).\"\"\"\n+    client = build_keepalive_http_client(str(base_url or \"\"), async_mode=async_mode)\n+    if client is None:\n+        return {}\n+    return {\"http_client\": client}\n+\n+\n+def _create_openai_client(*, api_key: str, base_url: str, **kwargs: Any) -> Any:\n+    kwargs = {**_openai_http_client_kwargs(base_url), **kwargs}\n+    return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **kwargs)\n+\n+\n # ── Interrupt protection for atomic auxiliary tasks ──────────────────────\n # Some auxiliary tasks must NOT be aborted mid-flight by a gateway interrupt\n # (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]]:\n             _merged_aux = _apply_user_default_headers(extra.get(\"default_headers\"))\n             if _merged_aux:\n                 extra[\"default_headers\"] = _merged_aux\n-            _client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra)\n+            _client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, **extra)\n             _client = _maybe_wrap_anthropic(_client, model, api_key, raw_base_url)\n             return _client, model\n ","@@ -1654,7 +1672,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n         _merged_aux2 = _apply_user_default_headers(extra.get(\"default_headers\"))\n         if _merged_aux2:\n             extra[\"default_headers\"] = _merged_aux2\n-        _client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra)\n+        _client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url, **extra)\n         _client = _maybe_wrap_anthropic(_client, model, api_key, raw_base_url)\n         return _client, model\n ","@@ -1672,7 +1690,7 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op\n         if or_key:\n             base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL\n             logger.debug(\"Auxiliary client: OpenRouter via pool\")\n-            return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,\n+            return _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url,\n                            default_headers=build_or_headers()), model or _OPENROUTER_MODEL\n         # Pool exists but is exhausted (no usable runtime key) — fall through to\n         # 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\n         _mark_provider_unhealthy(\"openrouter\", ttl=60)\n         return None, None\n     logger.debug(\"Auxiliary client: OpenRouter\")\n-    return OpenAI(api_key=[redacted:secret-assignment] base_url=OPENROUTER_BASE_URL,\n+    return _create_openai_client(api_key=[redacted:secret-assignment] base_url=OPENROUTER_BASE_URL,\n                    default_headers=build_or_headers()), model or _OPENROUTER_MODEL\n \n ","@@ -1776,7 +1794,7 @@ def _try_nous(vision: bool = False) -> Tuple[Optional[OpenAI], Optional[str]]:\n             return None, None\n         base_url = str((nous or {}).get(\"inference_base_url\") or _nous_base_url()).rstrip(\"/\")\n     return (\n-        OpenAI(\n+        _create_openai_client(\n             api_key=[redacted:secret-assignment]\n             base_url=base_url,\n         ),","@@ -2053,7 +2071,7 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]:\n     if _custom_headers:\n         _extra[\"default_headers\"] = _custom_headers\n     if custom_mode == \"codex_responses\":\n-        real_client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra)\n+        real_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra)\n         return CodexAuxiliaryClient(real_client, model), model\n     if custom_mode == \"anthropic_messages\":\n         # Third-party Anthropic-compatible gateway (MiniMax, Zhipu GLM,","@@ -2067,14 +2085,14 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]:\n                 \"Custom endpoint declares api_mode=anthropic_messages but the \"\n                 \"anthropic SDK is not installed — falling back to OpenAI-wire.\"\n             )\n-            return OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra), model\n+            return _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra), model\n         return (\n             AnthropicAuxiliaryClient(real_client, model, custom_key, custom_base, is_oauth=False),\n             model,\n         )\n     # URL-based anthropic detection for custom endpoints that didn't set\n     # api_mode explicitly (e.g. kimi.com/coding reached via custom config).\n-    _fallback_client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra)\n+    _fallback_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **_extra)\n     _fallback_client = _maybe_wrap_anthropic(\n         _fallback_client, model, custom_key, custom_base, custom_mode,\n     )","@@ -2103,7 +2121,7 @@ def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str\n         return None, None\n     api_key, base_url = resolved\n     logger.debug(\"Auxiliary client: xAI OAuth (%s via Responses API)\", model)\n-    real_client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url)\n+    real_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url)\n     return CodexAuxiliaryClient(real_client, model), model\n \n ","@@ -2140,7 +2158,7 @@ def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]:\n             return None, None\n         base_url = _CODEX_AUX_BASE_URL\n     logger.debug(\"Auxiliary client: Codex OAuth (%s via Responses API)\", model)\n-    real_client = OpenAI(\n+    real_client = _create_openai_client(\n         api_key=[redacted:secret-assignment]\n         base_url=base_url,\n         default_headers=_codex_cloudflare_headers(codex_token),","@@ -2240,7 +2258,7 @@ def _try_azure_foundry(\n     if _dq:\n         extra[\"default_query\"] = _dq\n \n-    client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra)\n+    client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra)\n \n     if runtime_api_mode == \"codex_responses\":\n         # 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):\n     _merged_async = _apply_user_default_headers(async_kwargs.get(\"default_headers\"))\n     if _merged_async:\n         async_kwargs[\"default_headers\"] = _merged_async\n+    async_kwargs = {\n+        **_openai_http_client_kwargs(sync_base_url, async_mode=True),\n+        **async_kwargs,\n+    }\n     return AsyncOpenAI(**async_kwargs), model\n \n ","@@ -4012,7 +4034,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n                                \"but no Codex OAuth token found (run: hermes model)\")\n                 return None, None\n             final_model = _normalize_resolved_model(model, provider)\n-            raw_client = OpenAI(\n+            raw_client = _create_openai_client(\n                 api_key=[redacted:secret-assignment]\n                 base_url=_CODEX_AUX_BASE_URL,\n                 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 = \"\",\n             _merged_custom = _apply_user_default_headers(extra.get(\"default_headers\"))\n             if _merged_custom:\n                 extra[\"default_headers\"] = _merged_custom\n-            client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra)\n+            client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base, **extra)\n             client = _wrap_if_needed(client, final_model, custom_base, custom_key)\n             return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode\n                     else (client, final_model))","@@ -4197,7 +4219,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n                         _fb_headers = _apply_user_default_headers(_fb_extra.get(\"default_headers\"))\n                         if _fb_headers:\n                             _fb_extra[\"default_headers\"] = _fb_headers\n-                        client = OpenAI(api_key=[redacted:secret-assignment] base_url=_fb_clean, **_fb_extra)\n+                        client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_fb_clean, **_fb_extra)\n                         return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode\n                                 else (client, final_model))\n                     sync_anthropic = AnthropicAuxiliaryClient(","@@ -4206,7 +4228,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n                     if async_mode:\n                         return AsyncAnthropicAuxiliaryClient(sync_anthropic), final_model\n                     return sync_anthropic, final_model\n-                client = OpenAI(api_key=[redacted:secret-assignment] base_url=_clean_base2, **_extra2)\n+                client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=_clean_base2, **_extra2)\n                 # codex_responses or inherited auto-detect (via _wrap_if_needed).\n                 # _wrap_if_needed reads the closed-over `api_mode` (the task-level\n                 # 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 = \"\",\n         _merged_main = _apply_user_default_headers(headers)\n         if _merged_main:\n             headers = _merged_main\n-        client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,\n+        client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=base_url,\n                         **({\"default_headers\": headers} if headers else {}))\n \n         # Copilot GPT-5+ models (except gpt-5-mini) require the Responses","@@ -4884,7 +4906,7 @@ def _refresh_nous_auxiliary_client(\n         return None, model\n \n     fresh_key, fresh_base_url = runtime\n-    sync_client = OpenAI(api_key=[redacted:secret-assignment] base_url=fresh_base_url)\n+    sync_client = _create_openai_client(api_key=[redacted:secret-assignment] base_url=fresh_base_url)\n     final_model = model\n \n     current_loop = None"]},{"file":"agent/process_bootstrap.py","hunks":["@@ -26,7 +26,7 @@\n import os\n import sys\n import urllib.request\n-from typing import Optional\n+from typing import Any, Optional\n \n from utils import base_url_hostname, normalize_proxy_url\n ","@@ -142,6 +142,46 @@ def _get_proxy_for_base_url(base_url: Optional[str]) -> Optional[str]:\n     return proxy\n \n \n+def build_keepalive_http_client(\n+    base_url: str = \"\",\n+    *,\n+    async_mode: bool = False,\n+) -> Optional[Any]:\n+    \"\"\"Build an httpx client for OpenAI SDK calls with env-only proxy policy.\n+\n+    Uses explicit ``HTTPS_PROXY`` / ``NO_PROXY`` env vars via\n+    ``_get_proxy_for_base_url``. A custom transport disables httpx's default\n+    ``trust_env`` path, so macOS system proxy settings from\n+    ``urllib.request.getproxies()`` (which omit the ExceptionsList) are not\n+    applied. Mirrors ``AIAgent._build_keepalive_http_client``.\n+    \"\"\"\n+    try:\n+        import httpx\n+        import socket\n+\n+        if \"api.githubcopilot.com\" in str(base_url or \"\").lower():\n+            client_cls = httpx.AsyncClient if async_mode else httpx.Client\n+            return client_cls()\n+\n+        sock_opts = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]\n+        if hasattr(socket, \"TCP_KEEPIDLE\"):\n+            sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30))\n+            sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10))\n+            sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3))\n+        elif hasattr(socket, \"TCP_KEEPALIVE\"):\n+            sock_opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 30))\n+\n+        proxy = _get_proxy_for_base_url(base_url)\n+        transport_cls = httpx.AsyncHTTPTransport if async_mode else httpx.HTTPTransport\n+        client_cls = httpx.AsyncClient if async_mode else httpx.Client\n+        return client_cls(\n+            transport=transport_cls(socket_options=sock_opts),\n+            proxy=proxy,\n+        )\n+    except Exception:\n+        return None\n+\n+\n def _install_safe_stdio() -> None:\n     \"\"\"Wrap stdout/stderr so best-effort console output cannot crash the agent.\"\"\"\n     for stream_name in (\"stdout\", \"stderr\"):","@@ -164,4 +204,5 @@ def _install_safe_stdio() -> None:\n     \"_install_safe_stdio\",\n     \"_get_proxy_from_env\",\n     \"_get_proxy_for_base_url\",\n+    \"build_keepalive_http_client\",\n ]"]},{"file":"scripts/release.py","hunks":["@@ -50,6 +50,7 @@\n     \"[redacted:email]\": \"djstunami\",  # PR #5316 salvage / co-author (suppress transient check_fn flakes so subagents keep file/terminal tools; #21658 / #5304)\n     \"[redacted:email]\": \"jmmaloney4\",  # PR #25206 salvage (re-select credential pool on primary runtime restore; #25205)\n     \"[redacted:email]\": \"dalenguyen\",  # PR #53678 salvage (strip VIRTUAL_ENV/CONDA_PREFIX from terminal subprocess env; #23473)\n+    \"[redacted:email]\": \"HexLab98\",  # PR #53863 salvage (env-only proxy policy for auxiliary OpenAI clients on macOS; #53702)\n     \"[redacted:email]\": \"Blaryxoff\",  # PR #32602 salvage (deep-merge PUT /api/config to preserve unrelated sections; #13396)\n     \"[redacted:email]\": \"DiamondEyesFox\",  # PR #53351 salvage (rebaseline in-place compression flushes to prevent duplicate compacted rows; #9096)\n     \"[redacted:email]\": \"rlaope\",  # PR #35075 salvage (align cron invisible-unicode set with install-time scanner; #35075)"]},{"file":"tests/agent/test_auxiliary_client_proxy_env.py","hunks":["@@ -0,0 +1,98 @@\n+\"\"\"Regression guard: auxiliary OpenAI clients must use env-only proxy policy.\n+\n+On macOS, httpx with default ``trust_env=True`` reads system proxy settings\n+via ``urllib.request.getproxies()`` but not the macOS proxy exception list.\n+Auxiliary clients (vision, title generation, etc.) must mirror the main\n+agent: explicit ``HTTPS_PROXY`` / ``NO_PROXY`` env vars only, via a custom\n+keepalive transport that suppresses automatic system-proxy detection.\n+\"\"\"\n+from unittest.mock import patch\n+\n+import httpx\n+\n+from agent.auxiliary_client import _create_openai_client, _openai_http_client_kwargs\n+from agent.process_bootstrap import _get_proxy_for_base_url\n+\n+\n+def _pool_types(http_client) -> list:\n+    return [\n+        type(mount._pool).__name__\n+        for mount in http_client._mounts.values()\n+        if mount is not None and hasattr(mount, \"_pool\")\n+    ]\n+\n+\n+@patch(\"agent.auxiliary_client.OpenAI\")\n+def test_create_openai_client_routes_via_env_proxy(mock_openai, monkeypatch):\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\", \"NO_PROXY\", \"no_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+    monkeypatch.setenv(\"HTTPS_PROXY\", \"http://127.0.0.1:7897\")\n+\n+    _create_openai_client(\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://litellm.internal.example.com/v1\",\n+    )\n+\n+    http_client = mock_openai.call_args.kwargs.get(\"http_client\")\n+    assert isinstance(http_client, httpx.Client)\n+    assert \"HTTPProxy\" in _pool_types(http_client)\n+    http_client.close()\n+\n+\n+@patch(\"agent.auxiliary_client.OpenAI\")\n+def test_create_openai_client_no_proxy_when_env_unset(mock_openai, monkeypatch):\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\", \"NO_PROXY\", \"no_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+\n+    _create_openai_client(\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://litellm.internal.example.com/v1\",\n+    )\n+\n+    http_client = mock_openai.call_args.kwargs.get(\"http_client\")\n+    assert isinstance(http_client, httpx.Client)\n+    assert \"HTTPProxy\" not in _pool_types(http_client)\n+    http_client.close()\n+\n+\n+@patch(\"agent.auxiliary_client.OpenAI\")\n+def test_create_openai_client_ignores_macos_system_proxy(mock_openai, monkeypatch):\n+    \"\"\"System proxy from getproxies() must not apply when env vars are unset.\"\"\"\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\", \"NO_PROXY\", \"no_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+\n+    with patch(\n+        \"urllib.request.getproxies\",\n+        return_value={\"http\": \"http://127.0.0.1:7897\", \"https\": \"http://127.0.0.1:7897\"},\n+    ):\n+        _create_openai_client(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://litellm.internal.example.com/v1\",\n+        )\n+\n+    http_client = mock_openai.call_args.kwargs.get(\"http_client\")\n+    assert isinstance(http_client, httpx.Client)\n+    assert \"HTTPProxy\" not in _pool_types(http_client)\n+    http_client.close()\n+\n+\n+def test_get_proxy_for_base_url_respects_no_proxy(monkeypatch):\n+    for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\",\n+                \"https_proxy\", \"http_proxy\", \"all_proxy\", \"NO_PROXY\", \"no_proxy\"):\n+        monkeypatch.delenv(key, raising=False)\n+    monkeypatch.setenv(\"HTTPS_PROXY\", \"http://127.0.0.1:7897\")\n+    monkeypatch.setenv(\"NO_PROXY\", \"internal.example.com\")\n+\n+    assert _get_proxy_for_base_url(\"https://litellm.internal.example.com/v1\") is None\n+    assert _get_proxy_for_base_url(\"https://api.openai.com/v1\") == \"http://127.0.0.1:7897\"\n+\n+\n+def test_openai_http_client_kwargs_async_mode():\n+    kwargs = _openai_http_client_kwargs(\n+        \"https://litellm.internal.example.com/v1\",\n+        async_mode=True,\n+    )\n+    assert isinstance(kwargs[\"http_client\"], httpx.AsyncClient)\n"]}]},{"kind":"pr","number":59473,"title":"fix: honor no_proxy CIDR for provider clients","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":59474,"title":"fix(proxy): support CIDR notation in no_proxy for custom provider routing","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":59505,"title":"fix(proxy): honor CIDR notation in no_proxy for custom providers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59473","number":59473},{"kind":"pr","value":"#59465","number":59465}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":59558,"title":"fix(agent): support CIDR notation in NO_PROXY for custom provider proxy bypass (#59465)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59473","number":59473},{"kind":"pr","value":"#59465","number":59465}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":59667,"title":"fix(network): support CIDR notation in no_proxy (#59465)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59465","number":59465},{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59505","number":59505},{"kind":"pr","value":"#59558","number":59558},{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59473","number":59473}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":59934,"title":"fix(agent): honor NO_PROXY CIDR ranges","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59474","number":59474},{"kind":"pr","value":"#59473","number":59473},{"kind":"pr","value":"#59465","number":59465}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":64893,"title":"fix(image-gen): bypass macOS system proxy for custom endpoints","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"e113c1b3aff3","number":0},{"kind":"pr","value":"#115803","number":115803},{"kind":"pr","value":"#65309","number":65309},{"kind":"pr","value":"#97928","number":97928},{"kind":"pr","value":"#13798","number":13798},{"kind":"pr","value":"#60748","number":60748},{"kind":"pr","value":"#64888","number":64888},{"kind":"pr","value":"#65312","number":65312},{"kind":"pr","value":"#64893","number":64893},{"kind":"pr","value":"#60749","number":60749}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":115803,"title":"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)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"]}],"trust":{"default_tier":"verified","includes_suggested":false}}