fix(streaming): route Anthropic stream cleanup to the Anthropic client, not OpenAI rebuild (#28161) - #53926
Merged
Merged
Conversation
… OpenAI client interruptible_streaming_api_call() has three connection-pool cleanup sites that called _replace_primary_openai_client() unconditionally. For api_mode=anthropic_messages this has two consequences: 1. _replace_primary_openai_client() fails (OPENAI_API_KEY unset on Anthropic-only configs), so dead connections are never purged. 2. The stale-stream detector's outer-poll site (L1977) is the only mechanism that can interrupt the worker thread while it blocks in for event in stream:. Because the Anthropic client is never closed, the thread stays blocked until the 900 s httpx read-timeout fires, producing a visible 15-minute hang for Telegram/gateway users on claude-opus-4-7. Fix: mirror the existing interrupt-path pattern (L1989-1997) at all three cleanup sites — if api_mode == "anthropic_messages", call _anthropic_client.close() + _rebuild_anthropic_client() instead of _replace_primary_openai_client(). _rebuild_anthropic_client() handles both direct Anthropic and Bedrock-hosted Claude correctly, unlike the inline build_anthropic_client() calls in open PR #14430. PR #14430 (open) covers only the outer stale-detector site (L1977). PR #23678 (open) covers only the inner retry sites (L1774, L1833). This PR covers all three sites and uses _rebuild_anthropic_client() for Bedrock parity. Fixes #28161
…ld path The existing test_anthropic_stream_parser_valueerror_retries_before_delivery asserted mock_replace.call_count == 1 — i.e. it passed precisely because the buggy OpenAI rebuild was invoked on the Anthropic path. Repoint it to assert the corrected close+rebuild-Anthropic behavior (#28161).
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
11 |
unresolved-import |
2 |
unresolved-attribute |
1 |
First entries
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `list[dict[str, Any]]`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `list[str] | None`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `int | float | None`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `list[str]`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `bool`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `IterationBudget`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:41: [unresolved-attribute] unresolved-attribute: Unresolved attribute `api_mode` on type `AIAgent`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `int`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `((str, dict[Unknown, Unknown], /) -> None) | None`, found `str | bool`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `int | float`, found `str | bool`
✅ Fixed issues: none
Unchanged: 6069 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Collaborator
Saturated competing cluster for #28161 (all open): #28240, #33855, #44076, #47797 — all fix the unconditional |
This was referenced Jun 28, 2026
Closed
Closed
Closed
2 tasks
This was referenced Jul 14, 2026
Open
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
Stream-cleanup on Anthropic-native sessions no longer rebuilds the OpenAI primary client — eliminating a ~15-minute hang on stuck streams.
Root cause: three stream-cleanup paths in
agent/chat_completion_helpers.pycalled_replace_primary_openai_client()unconditionally. Onapi_mode == "anthropic_messages"(Anthropic-only configs, noOPENAI_API_KEY) that rebuild fails with a missing-credentials error AND the wedged in-flight httpx stream is never closed, so the worker thread blocks on the dead socket until the 900s httpx read-timeout fires. The_interrupt_requestedbranch in the same function already did the right thing for Anthropic (_anthropic_client.close()+_rebuild_anthropic_client()); the three cleanup sites now mirror it.Changes
agent/chat_completion_helpers.py: branch each of the three pool-cleanup sites onapi_mode— Anthropic mode closes + rebuilds the Anthropic client; everything else rebuilds the OpenAI primary client as before:stream_mid_tool_retry_pool_cleanupstream_retry_pool_cleanupstale_stream_pool_cleanuptests/run_agent/test_28161_anthropic_stream_pool_cleanup.py: new tests for the two reachable Anthropic cleanup sites (stream-retry, stale-stream) asserting the Anthropic close+rebuild fires and the OpenAI rebuild does not.tests/run_agent/test_streaming.py: re-pointtest_anthropic_stream_parser_valueerror_retries_before_delivery— it previously asserted_replace_primary_openai_clientwas called once (passing because the bug was live). It now asserts the corrected close+rebuild-Anthropic behavior.Validation
_replace_primary_openai_clientcalls on Anthropic pathE2E (real imports,
OPENAI_API_KEYunset,api_mode=anthropic_messages): a transient stream error triggers cleanup →_replace_primary_openai_clientcalled 0 times,_rebuild_anthropic_client+_anthropic_client.close()each called once, retry succeeds.Salvaged from #28240 by @EloquentBrush0x (earliest dedicated fix for this issue); conflict resolved against current
main's newer request-client cleanup helper, sibling tests + the bug-encoding assertion fixed on top.Infographic