From bb8b8f48c8b204398f0b2bdf8ea46ab903ea40d6 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 15 Jul 2026 22:45:19 +0000 Subject: [PATCH] fix(cli): print friendly error instead of traceback when proxy is unreachable Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/client/cli/main.py | 17 ++++++++++++- .../proxy/client/cli/test_global_options.py | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/client/cli/main.py b/litellm/proxy/client/cli/main.py index 4de3ff5fc87..e8c5695249d 100644 --- a/litellm/proxy/client/cli/main.py +++ b/litellm/proxy/client/cli/main.py @@ -3,6 +3,7 @@ from typing import Optional # third party imports import click +import requests from litellm._version import version as litellm_version from litellm.proxy.client.health import HealthManagementClient @@ -22,6 +23,20 @@ from .commands.users import users from .interface import interactive_shell +class ConnectionAwareGroup(click.Group): + def invoke(self, ctx: click.Context) -> object: + try: + return super().invoke(ctx) + except requests.exceptions.ConnectionError as e: + base_url = ctx.obj.get("base_url") if isinstance(ctx.obj, dict) else None + target = f" at {base_url}" if base_url else "" + raise click.ClickException( + f"Could not connect to the LiteLLM proxy{target}. " + "Make sure the server is running and reachable, or point the CLI at it " + "with --base-url or the LITELLM_PROXY_URL environment variable." + ) from e + + def print_version(base_url: str, api_key: Optional[str]): """Print CLI and server version info.""" click.echo(f"LiteLLM Proxy CLI Version: {litellm_version}") @@ -38,7 +53,7 @@ def print_version(base_url: str, api_key: Optional[str]): click.echo(f"Could not retrieve server version: {e}") -@click.group(invoke_without_command=True) +@click.group(cls=ConnectionAwareGroup, invoke_without_command=True) @click.option( "--version", "-v", diff --git a/tests/test_litellm/proxy/client/cli/test_global_options.py b/tests/test_litellm/proxy/client/cli/test_global_options.py index 53b7e4dbc29..ced00f51e36 100644 --- a/tests/test_litellm/proxy/client/cli/test_global_options.py +++ b/tests/test_litellm/proxy/client/cli/test_global_options.py @@ -4,6 +4,7 @@ import sys from unittest.mock import Mock, patch import pytest +import requests from click.testing import CliRunner sys.path.insert( @@ -65,6 +66,30 @@ def test_base_url_trailing_slash_normalized(cli_runner): ) +@pytest.mark.parametrize( + "args", + [ + ["models", "list"], + ["keys", "list"], + ["http", "request", "GET", "/models"], + ], +) +def test_connection_error_prints_friendly_message(cli_runner, args): + """A dead proxy must yield a friendly 'Error:' line and exit 1, not a raw traceback.""" + base_url = "http://127.0.0.1:59999" + with patch( + "requests.sessions.Session.request", + side_effect=requests.exceptions.ConnectionError("connection refused"), + ): + result = cli_runner.invoke(cli, ["--base-url", base_url, *args]) + + assert result.exit_code == 1 + assert "Traceback" not in result.output + assert f"Could not connect to the LiteLLM proxy at {base_url}" in result.output + # The exception must be swallowed into a click exit, never surfaced raw. + assert not isinstance(result.exception, requests.exceptions.ConnectionError) + + def test_cli_version_command(cli_runner): """Test that 'version' command prints the correct version, server URL, and server version, and exits successfully""" with (