-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[Bug]: _is_openai_client_closed false positive — openai SDK is_closed is a method, not a property #4377
Description
Description
_is_openai_client_closed in run_agent.py always returns True for real OpenAI client instances due to a type mismatch with the openai SDK.
Root cause
run_agent.py, AIAgent._is_openai_client_closed() (around line 3488):
@staticmethod
def _is_openai_client_closed(client: Any) -> bool:
from unittest.mock import Mock
if isinstance(client, Mock):
return False
if bool(getattr(client, "is_closed", False)): # <-- BUG
return True
http_client = getattr(client, "_client", None)
return bool(getattr(http_client, "is_closed", False))In the openai SDK (tested on v2.26.0, constraint is >=2.21.0,<3), OpenAI.is_closed is a method, not a property:
# openai/_base_client.py
class SyncAPIClient:
def is_closed(self) -> bool:
return self._client.is_closedSo getattr(client, "is_closed", False) returns the bound method object, and bool(<bound method>) is always True. The check short-circuits before reaching the _client.is_closed fallback (which works correctly since httpx.Client.is_closed is a plain bool property).
Impact
Every API call:
- Falsely detects the shared client as closed
- Creates a new shared client (TCP + TLS handshake to provider)
- Closes the old healthy client
- Creates a per-request client (another TCP + TLS handshake)
- Closes the per-request client after the stream
Steps 1-3 are entirely unnecessary — the shared client is alive. This adds ~100-200ms of connection overhead per API call.
Why tests don't catch it
test_openai_client_lifecycle.py uses FakeRequestClient which doesn't define is_closed as an attribute. So getattr(fake_client, "is_closed", False) returns the default False, falls through to _client.is_closed (a SimpleNamespace attribute), and works correctly. The bug only manifests with real OpenAI instances.
Reproduction
from openai import OpenAI
client = OpenAI(api_key="test", base_url="https://openrouter.ai/api/v1")
val = getattr(client, "is_closed", False)
print(repr(val)) # <bound method SyncAPIClient.is_closed of ...>
print(bool(val)) # True <-- always truthy, even though client is openSuggested fix
In run_agent.py, the _is_openai_client_closed static method should call is_closed() when it's callable instead of checking the truthiness of the method object:
@staticmethod
def _is_openai_client_closed(client: Any) -> bool:
from unittest.mock import Mock
if isinstance(client, Mock):
return False
is_closed = getattr(client, "is_closed", None)
if callable(is_closed):
if is_closed():
return True
elif bool(is_closed):
return True
http_client = getattr(client, "_client", None)
if http_client is not None:
return bool(getattr(http_client, "is_closed", False))
return FalseThe test in test_openai_client_lifecycle.py should also add a case using a real OpenAI client (or a fake with is_closed as a method) to prevent regression.
Environment
- openai SDK: 2.26.0 (bug applies to any version where
is_closedis a method — all of>=2.21.0) - hermes-agent: v2026.3.23
- Observed via gateway adapter (custom
AgentRunnerwrappingAIAgent)