From ae9aad8160618719cc547f5912fb07288725f8ba Mon Sep 17 00:00:00 2001 From: Arthi Arumugam Date: Mon, 27 Jul 2026 16:22:10 +0530 Subject: [PATCH 1/2] fix(proxy): start on consoles that cannot encode the startup banner show_banner is the first thing run_server does, and the banner is box-drawing characters. On a console whose encoding cannot represent them, which is the default on Windows, click.echo raises UnicodeEncodeError and the proxy exits with "Application startup failed" before it binds a port. Setting PYTHONIOENCODING=utf-8 works around it. The banner is decorative, so fall back to a plain "LiteLLM" when the stream cannot encode it rather than taking the server down with it. --- litellm/proxy/common_utils/banner.py | 2 ++ .../proxy/common_utils/test_banner.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/test_litellm/proxy/common_utils/test_banner.py diff --git a/litellm/proxy/common_utils/banner.py b/litellm/proxy/common_utils/banner.py index 9983f128378..8f9590fa8a8 100644 --- a/litellm/proxy/common_utils/banner.py +++ b/litellm/proxy/common_utils/banner.py @@ -15,3 +15,5 @@ def show_banner(): click.echo(f"\n{LITELLM_BANNER}\n") except ImportError: print("\n") # noqa: T201 + except UnicodeEncodeError: + print("\n LiteLLM\n") # noqa: T201 diff --git a/tests/test_litellm/proxy/common_utils/test_banner.py b/tests/test_litellm/proxy/common_utils/test_banner.py new file mode 100644 index 00000000000..ce0915720ad --- /dev/null +++ b/tests/test_litellm/proxy/common_utils/test_banner.py @@ -0,0 +1,36 @@ +import io +import os +import sys +from contextlib import redirect_stdout + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.proxy.common_utils.banner import show_banner + + +def _cp1252_stdout() -> io.TextIOWrapper: + """A stdout that cannot encode the banner, like a Windows console on the default code page.""" + return io.TextIOWrapper(io.BytesIO(), encoding="cp1252", newline="") + + +class TestShowBanner: + def test_show_banner_survives_a_stdout_that_cannot_encode_it(self): + """The banner is decorative; an unencodable console must not stop the proxy from starting.""" + stream = _cp1252_stdout() + + with redirect_stdout(stream): + show_banner() + + stream.flush() + printed = stream.buffer.getvalue().decode("cp1252") + assert "LiteLLM" in printed + + def test_show_banner_prints_the_full_banner_when_stdout_can_encode_it(self): + stream = io.TextIOWrapper(io.BytesIO(), encoding="utf-8", newline="") + + with redirect_stdout(stream): + show_banner() + + stream.flush() + printed = stream.buffer.getvalue().decode("utf-8") + assert "██" in printed From cc62676e25a7319adbaac9ce1ea6b921cf07e4f9 Mon Sep 17 00:00:00 2001 From: arthi-arumugam-git Date: Sat, 8 Aug 2026 14:03:39 +0530 Subject: [PATCH 2/2] fix(proxy): use click.echo for the ASCII fallback so no T201 suppression is added The lint gate counts RUF100 across the tree and this branch took the total to 101 against a limit of 100. The added line carried a T201 suppression copied from the line above it, and the two ruff configs disagree about whether that suppression is needed: ruff.toml enables T20 so a bare print needs it, while ruff-strict.toml does not enable T20 and so counts it as dead. Reaching that handler means the import succeeded and only the encode failed, so click is already bound and is the channel the banner itself went out on. Using it sidesteps the disagreement rather than picking a side. Behaviour is unchanged: a cp1252 stdout still gets the plain ASCII name and a utf-8 one still gets the full banner. The pre-existing suppression on the line above is left alone, which puts the total back at 100. --- litellm/proxy/common_utils/banner.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/common_utils/banner.py b/litellm/proxy/common_utils/banner.py index 4636ee9aa3f..dd203352b05 100644 --- a/litellm/proxy/common_utils/banner.py +++ b/litellm/proxy/common_utils/banner.py @@ -18,4 +18,9 @@ def show_banner(): except ImportError: print("\n") # noqa: T201 except UnicodeEncodeError: - print("\n LiteLLM\n") # noqa: T201 + # Reaching here means the import succeeded and only the encode failed, so `click` is + # bound and is the same channel the banner just went out on. Using it rather than + # `print` keeps the line free of a T201 suppression, which the two ruff configs + # disagree about: ruff.toml enables T20 and needs one, ruff-strict.toml does not + # enable it and counts one as dead under RUF100. + click.echo("\n LiteLLM\n")