Skip to content

fix(agent): copy client_kwargs before mutating to prevent shared httpx.Client - #11369

Closed
yeyitech wants to merge 1 commit into
NousResearch:mainfrom
yeyitech:fix/httpx-client-dict-mutation
Closed

fix(agent): copy client_kwargs before mutating to prevent shared httpx.Client#11369
yeyitech wants to merge 1 commit into
NousResearch:mainfrom
yeyitech:fix/httpx-client-dict-mutation

Conversation

@yeyitech

Copy link
Copy Markdown
Contributor

Summary

Closes #11249.

_create_openai_client() previously added the http_client key in-place into the client_kwargs dict passed by the caller. When the caller was _replace_primary_openai_client() passing self._client_kwargs directly, that mutated dict persisted across calls. Every subsequent dict(self._client_kwargs) shallow-copy (made for per-request clients) therefore shared the same httpx.Client instance that was created for the primary client.

Closing any request-scoped OpenAI client would silently close the primary's underlying transport too, causing:

RuntimeError: Cannot send a request, as the client has been closed

on all API calls after the first — reliably reproduced in long-lived gateway agents (Telegram/Discord) that handle multiple messages in the same process.

Fix

client_kwargs = dict(client_kwargs) at the top of _create_openai_client() ensures each invocation operates on an independent local copy. The http_client key added for TCP keepalive injection is never written back into self._client_kwargs, so the primary and every per-request client each receive their own fresh httpx.Client instance with independent lifetimes.

_replace_primary_openai_client() passes self._client_kwargs directly — this is safe because the copy is now made inside _create_openai_client() itself.

Changes

Test plan

  • Existing test test_create_openai_client_does_not_mutate_input_kwargs passes — verifies the shallow-copy guard is in place
  • Manual: run a long-lived gateway agent across multiple messages and confirm no APIConnectionError: Connection error. after the first response

🤖 Generated with Claude Code

…x.Client

`_create_openai_client()` previously added `http_client` in-place to the
caller's dict. When the caller was `_replace_primary_openai_client()` passing
`self._client_kwargs`, the injected `httpx.Client` instance persisted in that
stored dict. Every subsequent `dict(self._client_kwargs)` shallow-copy (e.g.
for per-request clients) therefore shared the **same** `httpx.Client` instance
as the primary. Closing any request-scoped OpenAI client would silently close
the primary's underlying transport too, causing:

    RuntimeError: Cannot send a request, as the client has been closed

on all API calls after the first — reliably reproduced in long-lived gateway
agents that handle multiple messages. Fixes NousResearch#11249.

The one-line fix (`client_kwargs = dict(client_kwargs)` at the top of the
method) ensures each invocation operates on an independent local copy, so the
`http_client` key added for TCP keepalive injection is never written back into
`self._client_kwargs`. Updated the existing kwargs-isolation test docstring to
explicitly document this failure mode and the contract the fix enforces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 25, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Note: #11056 (same fix for shared httpx.Client mutation) was already merged. This PR may be stale — verify if the fix already landed on main.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the thorough write-up and the clear reproduction description — this was a real bug.

This is an automated hermes-sweeper review.

The fix proposed here landed one day before this PR was opened, via PR #11056 (merged 2026-04-16):

  • client_kwargs = dict(client_kwargs) is already at run_agent.py line 4851 (commit 896e7b03e), with a comment referencing the same fix: enable TCP keepalives to detect dead provider connections #10933 root-cause class
  • tests/run_agent/test_create_openai_client_kwargs_isolation.py already contains test_create_openai_client_does_not_mutate_input_kwargs, which pins the contract you describe
  • @alt-glitch also noted this in the PR discussion

Closing as implemented_on_main — no action needed.

@teknium1 teknium1 closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

3 participants