mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix: revert httpx client caching that caused closed client errors
AsyncHTTPHandler.__del__ was closing httpx clients still in use by AsyncOpenAI/AsyncAzureOpenAI due to independent cache lifecycles. Restores standalone httpx client creation for OpenAI/Azure providers.
This commit is contained in:
parent
8da3a93e6e
commit
20d3cbfdeb
2 changed files with 64 additions and 56 deletions
|
|
@ -22,7 +22,6 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
AsyncHTTPHandler,
|
||||
get_ssl_configuration,
|
||||
)
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
||||
class OpenAIError(BaseLLMException):
|
||||
|
|
@ -205,67 +204,30 @@ class BaseOpenAILLM:
|
|||
if litellm.aclient_session is not None:
|
||||
return litellm.aclient_session
|
||||
|
||||
# Use the global cached client system to prevent memory leaks (issue #14540)
|
||||
# This routes through get_async_httpx_client() which provides TTL-based caching
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
# Get unified SSL configuration
|
||||
ssl_config = get_ssl_configuration()
|
||||
|
||||
try:
|
||||
# Get SSL config and include in params for proper cache key
|
||||
ssl_config = get_ssl_configuration()
|
||||
params = {"ssl_verify": ssl_config} if ssl_config is not None else {}
|
||||
params["disable_aiohttp_transport"] = litellm.disable_aiohttp_transport
|
||||
|
||||
# Get a cached AsyncHTTPHandler which manages the httpx.AsyncClient
|
||||
cached_handler = get_async_httpx_client(
|
||||
llm_provider=LlmProviders.OPENAI, # Cache key includes provider
|
||||
params=params, # Include SSL config in cache key
|
||||
return httpx.AsyncClient(
|
||||
verify=ssl_config,
|
||||
transport=AsyncHTTPHandler._create_async_transport(
|
||||
ssl_context=ssl_config
|
||||
if isinstance(ssl_config, ssl.SSLContext)
|
||||
else None,
|
||||
ssl_verify=ssl_config if isinstance(ssl_config, bool) else None,
|
||||
shared_session=shared_session,
|
||||
)
|
||||
# Return the underlying httpx client from the handler
|
||||
return cached_handler.client
|
||||
except (ImportError, AttributeError, KeyError) as e:
|
||||
# Fallback to creating a client directly if caching system unavailable
|
||||
# This preserves backwards compatibility
|
||||
verbose_logger.debug(
|
||||
f"Client caching unavailable ({type(e).__name__}), using direct client creation"
|
||||
)
|
||||
ssl_config = get_ssl_configuration()
|
||||
return httpx.AsyncClient(
|
||||
verify=ssl_config,
|
||||
transport=AsyncHTTPHandler._create_async_transport(
|
||||
ssl_context=ssl_config
|
||||
if isinstance(ssl_config, ssl.SSLContext)
|
||||
else None,
|
||||
ssl_verify=ssl_config if isinstance(ssl_config, bool) else None,
|
||||
shared_session=shared_session,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_sync_http_client() -> Optional[httpx.Client]:
|
||||
if litellm.client_session is not None:
|
||||
return litellm.client_session
|
||||
|
||||
# Use the global cached client system to prevent memory leaks (issue #14540)
|
||||
from litellm.llms.custom_httpx.http_handler import _get_httpx_client
|
||||
# Get unified SSL configuration
|
||||
ssl_config = get_ssl_configuration()
|
||||
|
||||
try:
|
||||
# Get SSL config and include in params for proper cache key
|
||||
ssl_config = get_ssl_configuration()
|
||||
params = {"ssl_verify": ssl_config} if ssl_config is not None else None
|
||||
|
||||
# Get a cached HTTPHandler which manages the httpx.Client
|
||||
cached_handler = _get_httpx_client(params=params)
|
||||
# Return the underlying httpx client from the handler
|
||||
return cached_handler.client
|
||||
except (ImportError, AttributeError, KeyError) as e:
|
||||
# Fallback to creating a client directly if caching system unavailable
|
||||
verbose_logger.debug(
|
||||
f"Client caching unavailable ({type(e).__name__}), using direct client creation"
|
||||
)
|
||||
ssl_config = get_ssl_configuration()
|
||||
return httpx.Client(
|
||||
verify=ssl_config,
|
||||
follow_redirects=True,
|
||||
)
|
||||
return httpx.Client(
|
||||
verify=ssl_config,
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
|
|
|||
46
tests/test_litellm/llms/test_lifecycle_fix.py
Normal file
46
tests/test_litellm/llms/test_lifecycle_fix.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
Verifies that the httpx client used by AsyncOpenAI is NOT closed
|
||||
when AsyncHTTPHandler instances are garbage collected.
|
||||
"""
|
||||
import asyncio
|
||||
import gc
|
||||
import httpx
|
||||
from litellm.llms.openai.common_utils import BaseOpenAILLM
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
|
||||
|
||||
async def test_httpx_client_not_closed_by_handler_gc():
|
||||
"""
|
||||
Before the fix: _get_async_http_client() returned handler.client,
|
||||
so when handler was GC'd its __del__ closed the client.
|
||||
After the fix: returns a standalone httpx.AsyncClient, no handler involved.
|
||||
"""
|
||||
# Get the client the same way AsyncOpenAI would
|
||||
client = BaseOpenAILLM._get_async_http_client()
|
||||
assert isinstance(client, httpx.AsyncClient)
|
||||
|
||||
# Simulate what the old code did: create an AsyncHTTPHandler and GC it
|
||||
handler = AsyncHTTPHandler()
|
||||
handler_client = handler.client
|
||||
del handler
|
||||
gc.collect()
|
||||
|
||||
# The client from _get_async_http_client should still be open
|
||||
# because it's NOT tied to any AsyncHTTPHandler
|
||||
assert not client.is_closed, "Client was closed prematurely!"
|
||||
|
||||
# Verify it can actually send (build a request without sending)
|
||||
try:
|
||||
req = client.build_request("GET", "https://example.com")
|
||||
print("PASS: Client is still usable after handler GC")
|
||||
except RuntimeError as e:
|
||||
if "closed" in str(e):
|
||||
print(f"FAIL: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
await client.aclose()
|
||||
print("All checks passed!")
|
||||
|
||||
|
||||
asyncio.run(test_httpx_client_not_closed_by_handler_gc())
|
||||
Loading…
Add table
Reference in a new issue