fix(agent): copy client_kwargs before mutating to prevent shared httpx.Client - #11369
Closed
yeyitech wants to merge 1 commit into
Closed
fix(agent): copy client_kwargs before mutating to prevent shared httpx.Client#11369yeyitech wants to merge 1 commit into
yeyitech wants to merge 1 commit into
Conversation
…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>
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. |
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):
Closing as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #11249.
_create_openai_client()previously added thehttp_clientkey in-place into theclient_kwargsdict passed by the caller. When the caller was_replace_primary_openai_client()passingself._client_kwargsdirectly, that mutated dict persisted across calls. Every subsequentdict(self._client_kwargs)shallow-copy (made for per-request clients) therefore shared the samehttpx.Clientinstance that was created for the primary client.Closing any request-scoped OpenAI client would silently close the primary's underlying transport too, causing:
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. Thehttp_clientkey added for TCP keepalive injection is never written back intoself._client_kwargs, so the primary and every per-request client each receive their own freshhttpx.Clientinstance with independent lifetimes._replace_primary_openai_client()passesself._client_kwargsdirectly — this is safe because the copy is now made inside_create_openai_client()itself.Changes
run_agent.py: updated comment in_create_openai_client()to explicitly reference # Bug Report:APIConnectionError: Connection error.on every API call — httpx.Client shared between primary and request clients due to dict mutation in_create_openai_client()#11249 alongside fix: enable TCP keepalives to detect dead provider connections #10933 (same root-cause class of bug)tests/run_agent/test_create_openai_client_kwargs_isolation.py: extended docstring to document the # Bug Report:APIConnectionError: Connection error.on every API call — httpx.Client shared between primary and request clients due to dict mutation in_create_openai_client()#11249 failure mode and the contract the fix enforcesTest plan
test_create_openai_client_does_not_mutate_input_kwargspasses — verifies the shallow-copy guard is in placeAPIConnectionError: Connection error.after the first response🤖 Generated with Claude Code