Skip to content

[Bug]: _is_openai_client_closed false positive — openai SDK is_closed is a method, not a property #4377

@saulmc

Description

@saulmc

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_closed

So 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:

  1. Falsely detects the shared client as closed
  2. Creates a new shared client (TCP + TLS handshake to provider)
  3. Closes the old healthy client
  4. Creates a per-request client (another TCP + TLS handshake)
  5. 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 open

Suggested 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 False

The 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_closed is a method — all of >=2.21.0)
  • hermes-agent: v2026.3.23
  • Observed via gateway adapter (custom AgentRunner wrapping AIAgent)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions