From 04432e2a2cd22b6685ff1f55e1f1cfbfc5647626 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 6 May 2026 02:55:37 +0000 Subject: [PATCH] proxy: hot-reload config YAML when --reload is set Uvicorn's --reload only watches *.py by default, so editing the --config YAML did not restart the proxy. _get_reload_options() now extends reload_dirs/reload_includes with the config file's directory and basename when --config is provided. --- litellm/proxy/proxy_cli.py | 28 ++++++++++++++++++-- tests/test_litellm/proxy/test_proxy_cli.py | 30 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 71aeea67884..5907df2e11b 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -169,6 +169,28 @@ class ProxyInitializationHelpers: ) return uvicorn_args + @staticmethod + def _get_reload_options(config_path: Optional[str]) -> dict: + """ + Build uvicorn reload kwargs so --reload also reloads when the YAML + config file (passed via --config) changes. + + Uvicorn defaults to watching only `*.py` files in CWD; we extend the + watch set to include the config file's directory and filename. + """ + options: dict = {"reload": True} + if not config_path: + return options + config_abs = os.path.abspath(config_path) + config_dir = os.path.dirname(config_abs) + cwd = os.path.abspath(os.getcwd()) + reload_dirs = [cwd] + if config_dir and config_dir != cwd: + reload_dirs.append(config_dir) + options["reload_dirs"] = reload_dirs + options["reload_includes"] = ["*.py", os.path.basename(config_abs)] + return options + @staticmethod def _init_hypercorn_server( app: FastAPI, @@ -619,7 +641,7 @@ class ProxyInitializationHelpers: "--reload", is_flag=True, default=False, - help="Enable uvicorn hot reload (dev only). Incompatible with --num_workers>1, --run_gunicorn, and --run_hypercorn.", + help="Enable uvicorn hot reload (dev only). Also reloads when the --config YAML file changes. Incompatible with --num_workers>1, --run_gunicorn, and --run_hypercorn.", ) def run_server( # noqa: PLR0915 host, @@ -1028,7 +1050,9 @@ def run_server( # noqa: PLR0915 uvicorn_args["loop"] = loop_type if reload: - uvicorn_args["reload"] = True + uvicorn_args.update( + ProxyInitializationHelpers._get_reload_options(config) + ) uvicorn.run( **uvicorn_args, diff --git a/tests/test_litellm/proxy/test_proxy_cli.py b/tests/test_litellm/proxy/test_proxy_cli.py index 6fbce4a5458..1089c12e298 100644 --- a/tests/test_litellm/proxy/test_proxy_cli.py +++ b/tests/test_litellm/proxy/test_proxy_cli.py @@ -133,6 +133,36 @@ class TestProxyInitializationHelpers: ) assert args["timeout_worker_healthcheck"] == 15 + def test_get_reload_options_no_config(self): + opts = ProxyInitializationHelpers._get_reload_options(None) + assert opts == {"reload": True} + + def test_get_reload_options_with_config_in_cwd(self, tmp_path, monkeypatch): + config_file = tmp_path / "config.yaml" + config_file.write_text("model_list: []\n") + monkeypatch.chdir(tmp_path) + + opts = ProxyInitializationHelpers._get_reload_options("config.yaml") + + assert opts["reload"] is True + assert opts["reload_dirs"] == [str(tmp_path)] + assert opts["reload_includes"] == ["*.py", "config.yaml"] + + def test_get_reload_options_with_config_outside_cwd(self, tmp_path, monkeypatch): + cwd_dir = tmp_path / "work" + cwd_dir.mkdir() + elsewhere = tmp_path / "configs" + elsewhere.mkdir() + config_file = elsewhere / "proxy.yaml" + config_file.write_text("model_list: []\n") + monkeypatch.chdir(cwd_dir) + + opts = ProxyInitializationHelpers._get_reload_options(str(config_file)) + + assert opts["reload"] is True + assert opts["reload_dirs"] == [str(cwd_dir), str(elsewhere)] + assert opts["reload_includes"] == ["*.py", "proxy.yaml"] + @patch("asyncio.run") @patch("builtins.print") def test_init_hypercorn_server(self, mock_print, mock_asyncio_run):