fix(proxy): keep a non-utf-8 stdout from killing proxy startup

show_banner() is the first statement in run_server, and it wrote a banner drawn
with seven box-drawing codepoints (U+2550 U+2551 U+2554 U+2557 U+255A U+255D
U+2588) straight to stdout. On Windows, Python only encodes stdout as UTF-8 when
it is an attached console, so redirecting to a file or a pipe falls back to the
ANSI code page. cp1252 has none of those seven characters, so the write raised
UnicodeEncodeError and the proxy exited before binding a port. The existing
try/except only covered the click import, not the write.

Pick the banner the stream can actually represent, falling back to a plain-ASCII
wordmark, and guard the write so an undisplayable banner can never take the
server down. The unicode banner is unchanged, so the CLI SSO success page that
embeds it is unaffected.

Fixes #42366
This commit is contained in:
Aishwary Dongre 2026-09-22 13:45:32 +05:30
parent 25af172b85
commit 90c6ff73de
2 changed files with 92 additions and 5 deletions

View file

@ -1,6 +1,6 @@
import sys
from typing import Final
# LiteLLM ASCII banner
LITELLM_BANNER: Final = """ ██╗ ██╗████████╗███████╗██╗ ██╗ ███╗ ███╗
██║ ██║╚══██╔══╝██╔════╝██║ ██║ ████╗ ████║
██║ ██║ ██║ █████╗ ██║ ██║ ██╔████╔██║
@ -8,12 +8,38 @@ LITELLM_BANNER: Final = """ ██╗ ██╗████████╗
███████╗██║ ██║ ███████╗███████╗███████╗██║ ╚═╝ ██║
╚══════╝╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝"""
LITELLM_BANNER_ASCII: Final = r""" _ _____ _______ ______ _ _ __ __
| | |_ _||__ __|| ____|| | | | | \/ |
| | | | | | | |__ | | | | | \ / |
| | | | | | | __| | | | | | |\/| |
| |____ _| |_ | | | |____ | |____ | |____ | | | |
|______||_____| |_| |______||______||______||_| |_|"""
def show_banner():
"""Display the LiteLLM CLI banner."""
def banner_for_encoding(encoding: str | None) -> str:
"""Return the banner variant that ``encoding`` can represent."""
if encoding is None:
return LITELLM_BANNER
try:
LITELLM_BANNER.encode(encoding)
except (UnicodeEncodeError, LookupError):
return LITELLM_BANNER_ASCII
return LITELLM_BANNER
def _echo(text: str) -> None:
try:
import click
click.echo(f"\n{LITELLM_BANNER}\n")
click.echo(text)
except ImportError:
print("\n") # noqa: T201
print(text) # noqa: T201 # the banner is CLI output, and click is what we would print through
def show_banner() -> None:
"""Display the LiteLLM CLI banner. Never raises: the banner must not block startup."""
encoding: Final[str | None] = getattr(sys.stdout, "encoding", None)
try:
_echo(f"\n{banner_for_encoding(encoding)}\n")
except UnicodeEncodeError:
pass

View file

@ -0,0 +1,61 @@
from contextlib import redirect_stdout
from pathlib import Path
import pytest
from litellm.proxy.common_utils.banner import (
LITELLM_BANNER,
LITELLM_BANNER_ASCII,
banner_for_encoding,
show_banner,
)
def _banner_written_to_stdout(encoding: str, destination: Path) -> str:
with destination.open("w", encoding=encoding, errors="strict") as stream, redirect_stdout(stream):
show_banner()
return destination.read_text(encoding=encoding)
@pytest.mark.parametrize("encoding", ["utf-8", "utf-16", "cp1252", "cp437", "cp850", "ascii", "latin-1", "koi8-r"])
def test_banner_for_encoding_returns_a_banner_the_encoding_can_represent(encoding: str):
"""
The banner we hand to stdout must always survive that stream's codec.
Regression for the proxy exiting at startup on Windows when stdout is redirected:
the ANSI code page there is cp1252, which has none of the box-drawing characters
the unicode banner is drawn with.
"""
assert banner_for_encoding(encoding).encode(encoding)
def test_banner_for_encoding_prefers_the_unicode_banner_when_the_codec_allows_it():
assert banner_for_encoding("utf-8") == LITELLM_BANNER
def test_banner_for_encoding_degrades_when_the_codec_cannot_represent_the_unicode_banner():
assert banner_for_encoding("cp1252") == LITELLM_BANNER_ASCII
def test_banner_for_encoding_degrades_for_a_codec_python_does_not_know():
assert banner_for_encoding("not-a-real-codec") == LITELLM_BANNER_ASCII
def test_banner_for_encoding_keeps_the_unicode_banner_when_the_stream_has_no_encoding():
assert banner_for_encoding(None) == LITELLM_BANNER
def test_show_banner_does_not_raise_when_stdout_cannot_encode_the_unicode_banner(tmp_path: Path):
"""
show_banner() runs as the first statement of run_server, so anything it raises takes
the whole proxy down before it binds a port.
"""
written = _banner_written_to_stdout("cp1252", tmp_path / "stdout.log")
assert LITELLM_BANNER_ASCII in written
def test_show_banner_still_writes_the_unicode_banner_to_a_utf8_stdout(tmp_path: Path):
written = _banner_written_to_stdout("utf-8", tmp_path / "stdout.log")
assert LITELLM_BANNER in written