mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-01 07:03:02 +00:00
* fix(proxy): default the uvicorn worker healthcheck timeout to 60 s so multi-worker boots survive a slow import * fix(proxy): warn on uvicorn below 0.37 when multi-worker boots cannot get the longer healthcheck uvicorn 0.33 to 0.36 has no timeout_worker_healthcheck, so the 60 s default never reaches it and a slow worker start still crash-loops with no hint. Print an upgrade warning when the proxy runs more than one uvicorn worker on such a version, keep granian out of the uvicorn-only path, and type the new tests. * fix(proxy): name --run_granian in the healthcheck help text and add per-worker grace guidance to the helm chart * fix(proxy): keep the litellm command light so spawned workers answer uvicorn's first health ping Each worker uvicorn spawns re-runs the parent's main script before it starts answering health pings. The litellm console script imported the whole SDK at the top, so on a busy or CPU-limited host a new worker missed uvicorn's 5 s ping and was killed in a loop. The script now runs litellm_cli.main:proxy_server, which imports the proxy only when called, and gateway.launch imports LiteLLM inside main the same way. This replaces the earlier 60 s healthcheck default, so uvicorn's 5 s healthcheck and SIGTERM timing stay as they are. * fix(proxy): load the worker app before its log config and without GC pauses * fix(gateway): ship litellm_cli in the gateway image gateway.launch imports without_gc_pauses from litellm_cli, which the gateway image never copied, so python -m gateway.launch failed at import. Also build the gateway image when the launcher or litellm_cli changes. * fix(proxy): apply the worker log config before the proxy app import, as earlier releases did --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
223 lines
7.6 KiB
Python
223 lines
7.6 KiB
Python
"""Console entry points for `litellm`, `lite` and `litellm-proxy`.
|
|
|
|
`litellm` imports the proxy only when it runs. Every worker that `--num_workers` spawns
|
|
imports this module again before it can answer uvicorn's first health ping, which fails
|
|
within 5 s, so importing the SDK here makes slow hosts kill each new worker in a loop.
|
|
Each worker then loads the proxy app through `proxy_worker_app`, while its ping thread runs.
|
|
A full garbage collection during that load holds the GIL long enough to miss a ping, so
|
|
collection waits until the app is loaded and never revisits it. The log config is applied
|
|
after the SDK loads but before the proxy app does, as in earlier releases, so a config that
|
|
disables existing loggers silences the SDK's loggers and leaves the proxy app's enabled.
|
|
|
|
The CLI is a thin HTTP client, but it lives inside the `litellm` package, so a plain
|
|
import would run the whole SDK `__init__` before Click parses argv. `lite` and
|
|
`litellm-proxy` import the CLI against an uninitialized `litellm` package object instead. CLI code under
|
|
`litellm.proxy.client`, and whatever it imports, loads without the SDK whenever it is
|
|
first imported. The first attribute read on the SDK, or the first import of any other
|
|
new code under `litellm`, initializes the SDK in place before that code runs, so code
|
|
that needs the SDK sees it exactly as a plain `import litellm` would leave it. An init that
|
|
raises leaves the SDK uninitialized, so the next use runs it again and raises the same error.
|
|
"""
|
|
|
|
import builtins
|
|
import contextvars
|
|
import gc
|
|
import importlib
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import threading
|
|
import types
|
|
from collections.abc import Callable, Generator, Mapping, Sequence
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
if TYPE_CHECKING:
|
|
from fastapi import FastAPI
|
|
|
|
SDK_PACKAGE: Final = "litellm"
|
|
CLI_PACKAGE: Final = "litellm.proxy.client"
|
|
_INITIALIZATION_LOCK: Final = threading.RLock()
|
|
_BUILTIN_IMPORT: Final = builtins.__import__
|
|
_IMPORT_MODULE: Final = importlib.import_module
|
|
_LOADING_CLI_CODE: Final = contextvars.ContextVar("_LOADING_CLI_CODE", default=False)
|
|
|
|
|
|
def _is_dunder(name: str) -> bool:
|
|
return name.startswith("__") and name.endswith("__")
|
|
|
|
|
|
class _SDKUntilFirstUse(types.ModuleType):
|
|
def __getattribute__(self, name: str) -> object:
|
|
if not _is_dunder(name):
|
|
_initialize(self)
|
|
return types.ModuleType.__getattribute__(self, name)
|
|
|
|
|
|
class _SDKInitializing(types.ModuleType):
|
|
def __getattr__(self, name: str) -> object:
|
|
with _INITIALIZATION_LOCK:
|
|
if type(self) is _SDKInitializing:
|
|
raise AttributeError(f"partially initialized module {SDK_PACKAGE!r} has no attribute {name!r}")
|
|
return getattr(self, name)
|
|
|
|
|
|
def _become(sdk: types.ModuleType, state: type[types.ModuleType]) -> None:
|
|
sdk.__class__ = state # rebind-ok: every holder of the litellm module must see its state change in place
|
|
|
|
|
|
def _restore_interpreter_imports() -> None:
|
|
if builtins.__import__ is _import_initializing_sdk:
|
|
builtins.__import__ = _BUILTIN_IMPORT
|
|
if importlib.import_module is _import_module_initializing_sdk:
|
|
importlib.import_module = _IMPORT_MODULE
|
|
|
|
|
|
def _initialize(sdk: types.ModuleType) -> None:
|
|
with _INITIALIZATION_LOCK:
|
|
if type(sdk) is not _SDKUntilFirstUse:
|
|
return
|
|
spec: Final = sdk.__spec__
|
|
if spec is None or spec.loader is None:
|
|
raise ModuleNotFoundError(f"No module named {SDK_PACKAGE!r}", name=SDK_PACKAGE)
|
|
_become(sdk, _SDKInitializing)
|
|
try:
|
|
spec.loader.exec_module(sdk)
|
|
except BaseException:
|
|
_become(sdk, _SDKUntilFirstUse)
|
|
raise
|
|
_become(sdk, types.ModuleType)
|
|
_restore_interpreter_imports()
|
|
|
|
|
|
def _in_package(target: str, package: str) -> bool:
|
|
return target == package or target.startswith(f"{package}.")
|
|
|
|
|
|
def _needs_more_of(loaded: types.ModuleType, name: str) -> bool:
|
|
if name == "*":
|
|
return type(loaded) is _SDKUntilFirstUse
|
|
return name not in vars(loaded)
|
|
|
|
|
|
def _loads_new_sdk_code(target: str, fromlist: Sequence[str]) -> bool:
|
|
if not _in_package(target, SDK_PACKAGE):
|
|
return False
|
|
loaded: Final = sys.modules.get(target)
|
|
if loaded is None:
|
|
return True
|
|
return any(_needs_more_of(loaded, name) for name in fromlist)
|
|
|
|
|
|
def _load_cli_code(load: Callable[[], types.ModuleType]) -> types.ModuleType:
|
|
token: Final = _LOADING_CLI_CODE.set(True)
|
|
try:
|
|
return load()
|
|
finally:
|
|
_LOADING_CLI_CODE.reset(token)
|
|
|
|
|
|
def _imported(target: str | None, fromlist: Sequence[str], load: Callable[[], types.ModuleType]) -> types.ModuleType:
|
|
if _LOADING_CLI_CODE.get() or target is None or not _loads_new_sdk_code(target, fromlist):
|
|
return load()
|
|
if _in_package(target, CLI_PACKAGE):
|
|
return _load_cli_code(load)
|
|
sdk: Final = sys.modules.get(SDK_PACKAGE)
|
|
if sdk is not None:
|
|
_initialize(sdk)
|
|
return load()
|
|
|
|
|
|
def _import_target(name: str, importer_globals: Mapping[str, object] | None, level: int) -> str | None:
|
|
if level == 0:
|
|
return name
|
|
package: Final = (importer_globals or {}).get("__package__")
|
|
if not isinstance(package, str) or not package:
|
|
return None
|
|
return importlib.util.resolve_name("." * level + name, package)
|
|
|
|
|
|
def _import_initializing_sdk(
|
|
name: str,
|
|
globals: Mapping[str, object] | None = None,
|
|
locals: Mapping[str, object] | None = None,
|
|
fromlist: Sequence[str] | None = (),
|
|
level: int = 0,
|
|
) -> types.ModuleType:
|
|
return _imported(
|
|
_import_target(name, globals, level),
|
|
fromlist or (),
|
|
lambda: _BUILTIN_IMPORT(name, globals, locals, fromlist or (), level),
|
|
)
|
|
|
|
|
|
def _import_module_target(name: str, package: str | None) -> str | None:
|
|
if not name.startswith("."):
|
|
return name
|
|
if not package:
|
|
return None
|
|
return importlib.util.resolve_name(name, package)
|
|
|
|
|
|
def _import_module_initializing_sdk(name: str, package: str | None = None) -> types.ModuleType:
|
|
return _imported(_import_module_target(name, package), (), lambda: _IMPORT_MODULE(name, package))
|
|
|
|
|
|
def _install_sdk_until_first_use() -> None:
|
|
sdk_spec: Final = importlib.util.find_spec(SDK_PACKAGE)
|
|
if sdk_spec is None:
|
|
raise ModuleNotFoundError(f"No module named {SDK_PACKAGE!r}", name=SDK_PACKAGE)
|
|
sdk: Final = importlib.util.module_from_spec(sdk_spec)
|
|
sdk.__class__ = _SDKUntilFirstUse
|
|
sys.modules[SDK_PACKAGE] = sdk
|
|
|
|
from litellm._process_defaults import apply_process_defaults
|
|
|
|
apply_process_defaults()
|
|
builtins.__import__ = _import_initializing_sdk
|
|
importlib.import_module = _import_module_initializing_sdk
|
|
|
|
|
|
def _entry_points() -> tuple[Callable[[], None], Callable[[], None]]:
|
|
if SDK_PACKAGE not in sys.modules:
|
|
_install_sdk_until_first_use()
|
|
|
|
from litellm.proxy.client.cli.main import cli as lite
|
|
from litellm.proxy.client.cli.main import litellm_proxy_cli as deprecated_litellm_proxy
|
|
|
|
return lite, deprecated_litellm_proxy
|
|
|
|
|
|
def cli() -> None:
|
|
lite, _ = _entry_points()
|
|
lite()
|
|
|
|
|
|
def litellm_proxy_cli() -> None:
|
|
_, deprecated_litellm_proxy = _entry_points()
|
|
deprecated_litellm_proxy()
|
|
|
|
|
|
def proxy_server() -> None:
|
|
from litellm.proxy.proxy_cli import run_server
|
|
|
|
run_server()
|
|
|
|
|
|
@contextmanager
|
|
def without_gc_pauses() -> Generator[None, None, None]:
|
|
gc.disable()
|
|
try:
|
|
yield
|
|
finally:
|
|
gc.freeze()
|
|
gc.enable()
|
|
|
|
|
|
def proxy_worker_app() -> "FastAPI":
|
|
with without_gc_pauses():
|
|
from litellm.proxy.proxy_cli import ProxyInitializationHelpers
|
|
|
|
ProxyInitializationHelpers.apply_worker_log_config(os.environ)
|
|
from litellm.proxy.proxy_server import app
|
|
return app
|