From 76a17d937e020ca1fdc0921e031ade699edd680d Mon Sep 17 00:00:00 2001 From: Isaac Date: Sat, 24 Oct 2026 13:24:38 +0800 Subject: [PATCH] fix(custom_httpx): resolve aiohttp transport type without importing in finalizer paths The function-level import of LiteLLMAiohttpTransport in _dispose_wrapped_aiohttp_session() and _aiohttp_session_bound_elsewhere() always raises ImportError when AsyncHTTPHandler.__del__ runs during interpreter shutdown (sys.meta_path is already None). The bare except Exception: pass in __del__ swallows it, so the disposal silently never happens and the wrapped aiohttp ClientSession/TCPConnector fire "Unclosed client session"/"Unclosed connector" warnings at GC - exactly what the disposal was written to prevent. Hoisting the import to module scope is not viable (circular import via aiohttp_transport -> secret_managers.main -> caching -> gcs_bucket_base -> http_handler during litellm/__init__). Instead, both methods already hold the live transport instance, so resolve the type from the runtime class name and skip the import entirely. --- litellm/llms/custom_httpx/http_handler.py | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index cffeb44f12c..ae353b04f1a 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -1135,12 +1135,15 @@ class AsyncHTTPHandler: def _aiohttp_session_bound_elsewhere(self, loop: asyncio.AbstractEventLoop) -> bool: """True when the wrapped aiohttp session is bound to a loop other than ``loop`` — awaiting ``aclose()`` here would touch that loop's internals.""" - from litellm.llms.custom_httpx.aiohttp_transport import ( - LiteLLMAiohttpTransport, - ) - + # 2026-10-24 patch 0006: class-name check instead of isinstance -- the + # original function-level import always raises ImportError when the + # handler is GC'd during interpreter shutdown (sys.meta_path is already + # None), which silently aborted the disposal inside __del__'s bare + # except and let aiohttp fire "Unclosed client session"/"Unclosed + # connector" warnings at GC. The instance is in hand, so its runtime + # class carries all the information isinstance needed. transport: Final = getattr(self._client, "_transport", None) - if not isinstance(transport, LiteLLMAiohttpTransport): + if type(transport).__name__ != "LiteLLMAiohttpTransport": return False session: Final = transport.client if not isinstance(session, ClientSession) or session.closed: @@ -1158,12 +1161,15 @@ class AsyncHTTPHandler: "Unclosed client session" / "Unclosed connector" warnings fire at garbage collection. """ - from litellm.llms.custom_httpx.aiohttp_transport import ( - LiteLLMAiohttpTransport, - ) - + # 2026-10-24 patch 0006: class-name check instead of the function-level + # import -- that import always raises ImportError when this runs during + # interpreter shutdown (sys.meta_path is already None), silently + # aborting the disposal inside __del__'s bare except and leaving the + # wrapped aiohttp session to fire "Unclosed client session" / + # "Unclosed connector" warnings at GC. The instance is in hand, so its + # runtime class carries all the information isinstance needed. transport: Final = getattr(self._client, "_transport", None) - if not isinstance(transport, LiteLLMAiohttpTransport): + if type(transport).__name__ != "LiteLLMAiohttpTransport": return # A shared session (e.g. the proxy's) is never this handler's to close. if not getattr(transport, "_owns_session", False):