diff --git a/litellm/__init__.py b/litellm/__init__.py index 4fc71e12700..e6f52f781df 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1650,6 +1650,15 @@ if TYPE_CHECKING: # Track if async client cleanup has been registered (for lazy loading) _async_client_cleanup_registered = False +# Register async client cleanup eagerly so it runs even when only eagerly-imported +# symbols (e.g. `acompletion`, imported via `from .main import *`) are used. +# Without this, __getattr__ is never triggered and SSL connections are left open +# at process exit, causing "Fatal error on SSL transport" / "Event loop is closed" +# tracebacks with asyncio.gather (see issue #23278). +from litellm.llms.custom_httpx.async_client_cleanup import register_async_client_cleanup +register_async_client_cleanup() +_async_client_cleanup_registered = True + # Eager loading for backwards compatibility with VCR and other HTTP recording tools # When LITELLM_DISABLE_LAZY_LOADING is set, lazy-loaded attributes are loaded at import time # For now, this only affects encoding (tiktoken) as it was the only reported issue @@ -1663,13 +1672,6 @@ if os.getenv("LITELLM_DISABLE_LAZY_LOADING", "").lower() in ("1", "true", "yes", def __getattr__(name: str) -> Any: """Lazy import handler with cached registry for improved performance.""" - global _async_client_cleanup_registered - # Register async client cleanup on first access (only once) - if not _async_client_cleanup_registered: - from litellm.llms.custom_httpx.async_client_cleanup import register_async_client_cleanup - register_async_client_cleanup() - _async_client_cleanup_registered = True - # Use cached registry from _lazy_imports instead of importing tuples every time from ._lazy_imports import _get_lazy_import_registry diff --git a/tests/test_litellm/llms/custom_httpx/test_gemini_session_leak.py b/tests/test_litellm/llms/custom_httpx/test_gemini_session_leak.py index 99a1eb427d7..561762936e6 100755 --- a/tests/test_litellm/llms/custom_httpx/test_gemini_session_leak.py +++ b/tests/test_litellm/llms/custom_httpx/test_gemini_session_leak.py @@ -118,6 +118,25 @@ async def test_atexit_cleanup(): return False +def test_async_client_cleanup_registered_at_import_time(): + """ + Regression test for issue #23278: SSL transport errors with asyncio.gather. + + acompletion is imported at module level via `from .main import *`, so + __getattr__ is never triggered when users call litellm.acompletion(). + register_async_client_cleanup() must be called eagerly at import time, + not lazily inside __getattr__, otherwise SSL connections are left open + at process exit causing "Fatal error on SSL transport" errors. + """ + import litellm + + assert litellm._async_client_cleanup_registered is True, ( + "register_async_client_cleanup() must be called at import time. " + "If this fails, SSL connections will leak on process exit when " + "acompletion() is used with asyncio.gather() (issue #23278)." + ) + + def test_new_event_loop_atexit(): """Test that the new atexit handler can create a fresh event loop""" print("\n" + "=" * 70)