mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
* feat(proxy): supervise the opt-in Rust forwarding server With LITELLM_RUST on, `litellm` and `python -m gateway.launch` start a supervisor that runs the Rust gateway on the public port and Python on a private loopback listener. Python still serves every route and sees each request as if the client had connected to it directly. Rust sends the client's TCP peer in `x-litellm-rust-peer`. The supervised server turns server-level proxy headers off, and a middleware restores the peer and then applies the operator's own trust, so client IPs, trusted proxies and every IP-based setting behave the same with Rust on or off. The supervisor is transparent to Python. Signals pass through, Python's exit status is the result, and both children shut down when the supervisor's end of their stdin closes, so nothing outlives it. Rust starts only once Python listens. When forwarding cannot start (no gateway binary, TLS, a Unix socket, Hypercorn or Granian), the entry point warns and serves from Python alone. Every forwarding decision lives in litellm/proxy/rust_forward, paired with the gateway-forward crate. OS differences sit behind a Platform protocol, with Windows supported for local CLI runs. AGENTS.md files in the touched folders state the boundary. The `--rust_gateway` flag is gone in favor of the environment variable. * refactor(proxy): switch Rust forwarding on with LITELLM_RUST_FIRST Forwarding gets its own switch, independent of LITELLM_RUST, which keeps selecting only the Rust paths inside the SDK. Neither turns the other on. --------- Co-authored-by: narmstrong-berri <narmstrong@berri.ai>
168 lines
6.6 KiB
Python
168 lines
6.6 KiB
Python
"""Gateway supervisor: assemble DATABASE_URL, start the in-container PgBouncer, then run uvicorn.
|
|
|
|
``gateway/main.py`` assembles ``DATABASE_URL`` inside every uvicorn worker, which
|
|
is fine for a plain Postgres URL but not for the pooler: PgBouncer must be
|
|
started exactly once per pod, before the workers fork, and the workers must be
|
|
handed the loopback URL it listens on. A pre-existing ``DATABASE_URL`` wins in
|
|
``DatabaseURLSettings.apply_to_env`` under password auth, and one marked pooled
|
|
wins under token auth too, so exporting it here is enough for every worker to
|
|
pick the pooled URL up unchanged.
|
|
|
|
LiteLLM imports wait for ``main``: every uvicorn worker imports this module again
|
|
before it can answer its first 5 s health ping, so a slow host would kill each one.
|
|
Workers then load the gateway app through ``worker_app`` without garbage collection
|
|
pauses, which hold the GIL long enough to miss a ping.
|
|
|
|
Run with:
|
|
python -m gateway.launch --workers 4 --host 0.0.0.0 --port 4000
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
from typing_extensions import assert_never
|
|
from uvicorn.main import main as uvicorn_main
|
|
|
|
from litellm_cli.main import without_gc_pauses
|
|
|
|
if TYPE_CHECKING:
|
|
from litellm.proxy.db.db_url_settings import DatabaseURLSettings
|
|
from litellm.proxy.db.pgbouncer import PgBouncerError, PgBouncerSettings
|
|
from litellm.proxy.rust_forward.peer import ASGIApp
|
|
from litellm.proxy.rust_forward.server import PythonServer
|
|
|
|
GATEWAY_APP: Final = "gateway.launch:worker_app"
|
|
KEEPALIVE_FLAG: Final = "--timeout-keep-alive"
|
|
|
|
|
|
def uvicorn_argv(argv: Sequence[str], environ: Mapping[str, str]) -> tuple[str, ...]:
|
|
"""Honor ``KEEPALIVE_TIMEOUT`` like ``proxy_cli.py`` does, unless the flag was passed explicitly."""
|
|
keepalive: Final = environ.get("KEEPALIVE_TIMEOUT")
|
|
if keepalive is None or any(arg == KEEPALIVE_FLAG or arg.startswith(f"{KEEPALIVE_FLAG}=") for arg in argv):
|
|
return (GATEWAY_APP, "--factory", *argv)
|
|
return (GATEWAY_APP, "--factory", *argv, KEEPALIVE_FLAG, keepalive)
|
|
|
|
|
|
def pool_database_url(
|
|
settings: "DatabaseURLSettings",
|
|
pgbouncer: "PgBouncerSettings",
|
|
environ: Mapping[str, str],
|
|
) -> "str | PgBouncerError | None":
|
|
"""Start the in-container PgBouncer and return its loopback URL, or None when ``pgbouncer.enabled`` is off.
|
|
|
|
The upstream URL is whatever ``apply_to_env`` assembled from the discrete
|
|
``DATABASE_*`` vars (or an operator-pinned ``DATABASE_URL``). Under token
|
|
auth the pooler mints and renews the upstream token itself.
|
|
"""
|
|
from litellm.proxy.db.pgbouncer import PgBouncerError, start_in_container_pgbouncer
|
|
|
|
if not pgbouncer.enabled:
|
|
return None
|
|
upstream_url: Final = environ.get("DATABASE_URL")
|
|
if upstream_url is None:
|
|
return PgBouncerError("LITELLM_PGBOUNCER_ENABLED is set but no DATABASE_URL could be assembled")
|
|
return start_in_container_pgbouncer(pgbouncer, upstream_url, token_auth=settings.token_auth())
|
|
|
|
|
|
def worker_app() -> "ASGIApp":
|
|
with without_gc_pauses():
|
|
from gateway.main import app
|
|
from litellm.proxy.rust_forward.mode import rust_forward_mode
|
|
from litellm.proxy.rust_forward.peer import wrap
|
|
return wrap(app, rust_forward_mode(os.environ))
|
|
|
|
|
|
def _serve(argv: Sequence[str]) -> None:
|
|
uvicorn_main(tuple(argv), prog_name="uvicorn")
|
|
|
|
|
|
def python_server(argv: Sequence[str]) -> "PythonServer":
|
|
"""What uvicorn would serve the public port with, read from the same flags and environment it reads."""
|
|
from litellm.proxy.rust_forward.server import PythonServer, Server, UvicornCliParams
|
|
from litellm.proxy.shutdown.graceful_shutdown_manager import GracefulShutdownManager
|
|
|
|
params: Final = UvicornCliParams.model_validate(
|
|
uvicorn_main.make_context("uvicorn", list(uvicorn_argv(argv, os.environ))).params
|
|
)
|
|
return PythonServer(
|
|
server=Server.UVICORN,
|
|
listener=params.listener,
|
|
host=params.host,
|
|
port=params.port,
|
|
keepalive_seconds=params.timeout_keep_alive,
|
|
graceful_timeout_seconds=GracefulShutdownManager.get_timeout()
|
|
if params.timeout_graceful_shutdown is None
|
|
else params.timeout_graceful_shutdown,
|
|
trusted_proxies=params.trusted_proxies,
|
|
)
|
|
|
|
|
|
def configure_database() -> None:
|
|
from litellm.proxy.db.db_url_settings import DatabaseURLSettings
|
|
from litellm.proxy.db.pgbouncer import PgBouncerError, PgBouncerSettings, export_pooled_database_url
|
|
|
|
settings: Final = DatabaseURLSettings.from_env()
|
|
settings.apply_to_env()
|
|
pooled_url: Final = pool_database_url(settings, PgBouncerSettings(), os.environ)
|
|
if isinstance(pooled_url, PgBouncerError):
|
|
sys.exit(f"LiteLLM gateway: in-container pgbouncer could not start: {pooled_url.reason}")
|
|
if pooled_url is not None:
|
|
export_pooled_database_url(pooled_url)
|
|
|
|
|
|
def _serve_python_alone(argv: Sequence[str], serve: Callable[[Sequence[str]], None]) -> None:
|
|
configure_database()
|
|
serve(uvicorn_argv(argv, os.environ))
|
|
|
|
|
|
def _python_server_or_exit(argv: Sequence[str]) -> "PythonServer":
|
|
"""Report ``--help``, ``--version`` and a bad flag the way uvicorn's own CLI does, since it never gets to."""
|
|
import click
|
|
|
|
try:
|
|
return python_server(argv)
|
|
except click.exceptions.Exit as stopped:
|
|
sys.exit(stopped.exit_code)
|
|
except click.ClickException as error:
|
|
error.show()
|
|
sys.exit(error.exit_code)
|
|
|
|
|
|
def main(argv: Sequence[str], serve: Callable[[Sequence[str]], None] = _serve) -> None:
|
|
from pydantic import ValidationError
|
|
|
|
from litellm.proxy.rust_forward.mode import (
|
|
Mode,
|
|
Supervised,
|
|
environment_error,
|
|
rust_forward_mode,
|
|
stop_with_supervisor,
|
|
uvicorn_flags,
|
|
)
|
|
from litellm.proxy.rust_forward.supervisor import supervise
|
|
from litellm.proxy.rust_forward.supervisor.preflight import StartupError
|
|
|
|
try:
|
|
mode: Final = rust_forward_mode(os.environ)
|
|
except ValidationError as error:
|
|
sys.exit(environment_error(error))
|
|
match mode:
|
|
case Mode.DISABLED:
|
|
_serve_python_alone(argv, serve)
|
|
case Mode.SUPERVISOR:
|
|
supervised: Final = supervise(_python_server_or_exit(argv), before_start=configure_database)
|
|
if isinstance(supervised, StartupError):
|
|
_serve_python_alone(argv, serve)
|
|
else:
|
|
sys.exit(supervised)
|
|
case Supervised():
|
|
stop_with_supervisor(mode)
|
|
serve((*uvicorn_argv(argv, os.environ), *uvicorn_flags(mode)))
|
|
case _:
|
|
assert_never(mode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|