From 7c3c242ecdae0ecfa7cf9b72f20ef6831a49b703 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 16 Jul 2026 09:39:32 -0700 Subject: [PATCH] fix(cli): terminate ephemeral proxy if write_pid_record itself fails write_pid_record ran before the try/except that terminates the proxy and clears the pid record on failure, so a write_pid_record error (disk full, permission error creating ~/.litellm/autorouter/) left the just-launched proxy running with no pid record on disk at all -- exactly the state lite autoroute down needs a pid record to recover from. Moved it inside the try block alongside poll_liveliness. --- .../client/cli/commands/autoroute/commands.py | 4 +-- .../client/cli/autoroute/test_commands.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/client/cli/commands/autoroute/commands.py b/litellm/proxy/client/cli/commands/autoroute/commands.py index 428daeefa60..84a001e356a 100644 --- a/litellm/proxy/client/cli/commands/autoroute/commands.py +++ b/litellm/proxy/client/cli/commands/autoroute/commands.py @@ -101,11 +101,11 @@ def up() -> None: port = allocate_free_port() base_url = f"http://127.0.0.1:{port}" process = launch_proxy(CONFIG_PATH, port, LOG_PATH) - write_pid_record(PidRecord(pid=process.pid, port=port, config_path=str(CONFIG_PATH), log_path=str(LOG_PATH))) try: + write_pid_record(PidRecord(pid=process.pid, port=port, config_path=str(CONFIG_PATH), log_path=str(LOG_PATH))) poll_liveliness(base_url, LOG_PATH, process) - except ProcessLaunchError as e: + except (ProcessLaunchError, OSError) as e: terminate(process.pid) clear_pid_record() raise click.ClickException(str(e)) diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py b/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py index e0b5d3a71af..16307c4f9c4 100644 --- a/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py @@ -203,6 +203,37 @@ class TestUpCommand: assert not backup_path.exists() assert json.loads(claude_settings_path.read_text()) == original_settings + def test_terminates_ephemeral_proxy_when_write_pid_record_fails(self, monkeypatch, tmp_path): + """write_pid_record runs before poll_liveliness -- if it raises (disk full, permission + error creating ~/.litellm/autorouter/), the just-launched proxy must still be terminated, + not left running with no pid record for `lite autoroute down` to ever find.""" + config_path, _log_path, claude_settings_path, backup_path, _pid_record_path = _patch_paths( + monkeypatch, tmp_path + ) + config_path.write_text(yaml.safe_dump({"model_list": []})) + original_settings = {"theme": "dark"} + claude_settings_path.write_text(json.dumps(original_settings)) + + fake_process = FakeProcess(pid=888) + terminate_calls = [] + + def _raise_os_error(*args, **kwargs): + raise OSError("disk full") + + monkeypatch.setattr(commands_module, "launch_proxy", lambda *a, **k: fake_process) + monkeypatch.setattr(commands_module, "write_pid_record", _raise_os_error) + monkeypatch.setattr(commands_module, "allocate_free_port", lambda: 34567) + monkeypatch.setattr(commands_module, "terminate", lambda pid, **k: terminate_calls.append(pid)) + monkeypatch.setattr(commands_module.secrets, "token_urlsafe", lambda n: "fixed-master-key") + + result = self.runner.invoke(up) + + assert result.exit_code != 0 + assert "disk full" in result.output + assert terminate_calls == [888] + assert not backup_path.exists() + assert json.loads(claude_settings_path.read_text()) == original_settings + def test_terminates_ephemeral_proxy_when_claude_settings_is_corrupt(self, monkeypatch, tmp_path): """The health check can pass and the proxy can come up fine, but if ~/.claude/settings.json turns out to be corrupt, the just-started proxy must not be left