mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(proxy): fall back to default asyncio loop when uvloop import fails (#20933)
LiteLLM Proxy fails to start on Python 3.14.2 because the pinned
``uvloop==0.21.0`` (in ``[project.optional-dependencies] proxy``)
can't import on 3.14 — Python 3.14 removed the
``BaseDefaultEventLoopPolicy`` symbol uvloop 0.21 imports during its
own initialisation. The user-facing crash is::
File "uvicorn/loops/uvloop.py", line 3, in <module>
import uvloop
File "uvloop/__init__.py", line 7, in <module>
from asyncio.events import BaseDefaultEventLoopPolicy
ImportError: cannot import name 'BaseDefaultEventLoopPolicy'
from 'asyncio.events'
44 users 👍'd the issue (#20933, 10 + the workaround comments).
Current workaround is ``pip install uvloop==0.22.1 --force`` AFTER
``pip install litellm[proxy]``, which is fragile (next ``pip install
--upgrade`` reverts it) and requires uv users to fight with
``override-dependencies`` in ``pyproject.toml``.
This patch makes ``ProxyInitializationHelpers._get_loop_type()`` —
the one place that hands uvicorn ``loop="uvloop"`` — probe the
import before returning the string. If uvloop can't be imported on
the current Python, return ``None`` and let uvicorn pick the default
asyncio loop. Three lines of real code change.
Users wanting uvloop's perf back on a previously-broken Python
install ``uvloop>=0.22.1`` themselves; the probe picks the new wheel
up automatically. No ``pyproject.toml`` change is included — that
would force a ``uv.lock`` regen across the whole resolution graph,
which is out of scope for this issue.
Test plan
---------
``tests/test_litellm/proxy/test_proxy_cli.py`` gains three
``_get_loop_type`` tests:
- returns ``"uvloop"`` on Linux when uvloop imports cleanly
- returns ``None`` on Windows / cygwin (existing behaviour, locked)
- returns ``None`` when ``import uvloop`` raises ImportError
(simulates the Python 3.14 + uvloop 0.21 case via
``patch("builtins.__import__")``)
All three pass plus 31 pre-existing tests in the same file.
Closes #20933.
This commit is contained in:
parent
50df072d95
commit
986ff76a83
2 changed files with 60 additions and 3 deletions
|
|
@ -398,9 +398,29 @@ class ProxyInitializationHelpers:
|
|||
|
||||
@staticmethod
|
||||
def _get_loop_type():
|
||||
"""Helper function to determine the event loop type based on platform"""
|
||||
"""Helper function to determine the event loop type based on platform.
|
||||
|
||||
Returns ``None`` (let uvicorn pick the default asyncio loop) when:
|
||||
|
||||
- the platform doesn't ship uvloop (Windows + cygwin); OR
|
||||
- uvloop is installed but can't be imported on the current
|
||||
Python — see GH #20933, where the pinned ``uvloop==0.21.0``
|
||||
fails on Python 3.14.2 with ``ImportError: cannot import
|
||||
name 'BaseDefaultEventLoopPolicy' from 'asyncio.events'``.
|
||||
|
||||
Probing via ``import uvloop`` here is cheap (one-time at proxy
|
||||
startup) and lets the proxy boot under any Python version even
|
||||
when the lock-file-pinned uvloop happens to be incompatible.
|
||||
Users wanting uvloop's perf back on a previously-broken Python
|
||||
install ``uvloop>=0.22.1`` themselves; the probe picks the new
|
||||
wheel up automatically.
|
||||
"""
|
||||
if sys.platform in ("win32", "cygwin", "cli"):
|
||||
return None # Let uvicorn choose the default loop on Windows
|
||||
return None
|
||||
try:
|
||||
import uvloop # noqa: F401
|
||||
except ImportError:
|
||||
return None
|
||||
return "uvloop"
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ sys.path.insert(
|
|||
0, os.path.abspath("../../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
|
||||
import builtins
|
||||
import types
|
||||
|
||||
from litellm.proxy.proxy_cli import ProxyInitializationHelpers
|
||||
|
|
@ -82,6 +81,44 @@ class TestProxyInitializationHelpers:
|
|||
)
|
||||
mock_client.chat.completions.create.assert_called()
|
||||
|
||||
@patch("sys.platform", "linux")
|
||||
def test_get_loop_type_returns_uvloop_when_available(self):
|
||||
"""On Linux/macOS with a working uvloop install, return "uvloop"."""
|
||||
# uvloop is in the proxy extras and importable in this test env.
|
||||
assert ProxyInitializationHelpers._get_loop_type() == "uvloop"
|
||||
|
||||
@patch("sys.platform", "win32")
|
||||
def test_get_loop_type_returns_none_on_windows(self):
|
||||
"""Windows / cygwin don't ship uvloop — let uvicorn pick the default."""
|
||||
assert ProxyInitializationHelpers._get_loop_type() is None
|
||||
|
||||
@patch("sys.platform", "linux")
|
||||
def test_get_loop_type_returns_none_when_uvloop_unimportable(self):
|
||||
"""GH #20933: uvloop 0.21.0 fails to import on Python 3.14.2 with
|
||||
``ImportError: cannot import name 'BaseDefaultEventLoopPolicy'
|
||||
from 'asyncio.events'``. Before the fix this propagated up
|
||||
through ``uvicorn.run`` and crashed proxy startup. The probe in
|
||||
``_get_loop_type`` now catches the ImportError and falls back
|
||||
to ``None`` so uvicorn picks the default asyncio loop.
|
||||
|
||||
Implementation note: setting ``sys.modules["uvloop"] = None``
|
||||
is the documented Python idiom for forcing the next
|
||||
``import uvloop`` statement to raise ImportError (see CPython
|
||||
importlib source). This routes through the real ``try / except``
|
||||
block in ``_get_loop_type`` rather than mocking it away, so
|
||||
coverage tools see the fallback branch executed.
|
||||
"""
|
||||
# Drop any cached real-uvloop module so the patched None takes effect.
|
||||
original = sys.modules.get("uvloop", _MISSING := object())
|
||||
sys.modules["uvloop"] = None # type: ignore[assignment]
|
||||
try:
|
||||
assert ProxyInitializationHelpers._get_loop_type() is None
|
||||
finally:
|
||||
if original is _MISSING:
|
||||
sys.modules.pop("uvloop", None)
|
||||
else:
|
||||
sys.modules["uvloop"] = original
|
||||
|
||||
def test_get_default_unvicorn_init_args(self):
|
||||
# Test without log_config
|
||||
args = ProxyInitializationHelpers._get_default_unvicorn_init_args(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue