diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e4d933ac699..a56bb09eb97 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -704,6 +704,12 @@ async def proxy_shutdown_event(): if db_writer_client is not None: await db_writer_client.close() # type: ignore[reportGeneralTypeIssues] + from litellm.llms.custom_httpx.async_client_cleanup import ( + close_litellm_async_clients, + ) + + await close_litellm_async_clients() + # flush remaining langfuse logs if "langfuse" in litellm.success_callback: try: diff --git a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py index 9343dcbc29f..54d30bf96d1 100644 --- a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py +++ b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py @@ -142,6 +142,38 @@ async def test_proxy_shutdown_event_prisma_disconnect_raises_error(monkeypatch): await proxy_shutdown_event() +@pytest.mark.asyncio +async def test_proxy_shutdown_event_closes_cached_aiohttp_sessions(monkeypatch): + """Regression: the shutdown handler must close cached async HTTP clients so + aiohttp does not log 'Unclosed client session'. A BaseLLMAIOHTTPHandler + living in litellm.in_memory_llm_clients_cache holds an open ClientSession; + after proxy_shutdown_event runs the session must be closed.""" + import litellm + from litellm.llms.custom_httpx.aiohttp_handler import BaseLLMAIOHTTPHandler + + fake_jwt = MagicMock() + fake_jwt.close = AsyncMock() + monkeypatch.setattr(ps, "jwt_handler", fake_jwt, raising=False) + monkeypatch.setattr(ps, "prisma_client", None, raising=False) + monkeypatch.setattr(ps, "db_writer_client", None, raising=False) + monkeypatch.setattr(litellm, "cache", None, raising=False) + monkeypatch.setattr(litellm, "success_callback", [], raising=False) + + handler = BaseLLMAIOHTTPHandler() + session = handler._get_async_client_session() + assert not session.closed + + cache_dict = litellm.in_memory_llm_clients_cache.cache_dict + cache_dict["witness-aiohttp-handler"] = handler + try: + await proxy_shutdown_event() + assert session.closed + finally: + cache_dict.pop("witness-aiohttp-handler", None) + if not session.closed: + await session.close() + + # --------------------------------------------------------------------------- # _initialize_shared_aiohttp_session # ---------------------------------------------------------------------------