From e2303a3925362821a5608ae77293e956576b2802 Mon Sep 17 00:00:00 2001 From: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:38:40 -0700 Subject: [PATCH] fix(proxy): keep show_banner() SDK-free, pick the JSON banner in initialize() Since #46803 the `lite` shell calls show_banner() against an uninitialized litellm package, and reading litellm.json_logs inside it initialized the whole SDK at the banner (0.14 s to 0.83 s for a bare `lite` here, the seconds that PR removed on small VMs). show_banner() is back to its main version; the proxy's initialize() now emits the JSON banner record itself and calls show_banner() only in plain mode. The banner unit tests move to the initialize() call site. --- litellm/proxy/common_utils/banner.py | 6 ---- litellm/proxy/proxy_server.py | 9 +++-- tests/unit/proxy/common_utils/test_banner.py | 36 ------------------- ...test_proxy_server_endpoints_and_startup.py | 34 ++++++++++++++++++ 4 files changed, 41 insertions(+), 44 deletions(-) delete mode 100644 tests/unit/proxy/common_utils/test_banner.py diff --git a/litellm/proxy/common_utils/banner.py b/litellm/proxy/common_utils/banner.py index 0abe997214b..25ad5e295f6 100644 --- a/litellm/proxy/common_utils/banner.py +++ b/litellm/proxy/common_utils/banner.py @@ -11,12 +11,6 @@ LITELLM_BANNER: Final = """ ██╗ ██╗████████╗ def show_banner(): """Display the LiteLLM CLI banner.""" - import litellm - from litellm._logging import print_startup_message - - if litellm.json_logs: - print_startup_message(LITELLM_BANNER) - return try: import click diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d7de86fa1c7..1bacbccaa9c 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9550,9 +9550,14 @@ async def initialize( master_key, \ user_custom_auth, \ prisma_client - from litellm.proxy.common_utils.banner import show_banner + from litellm.proxy.common_utils.banner import LITELLM_BANNER, show_banner - show_banner() + # show_banner() also serves the `lite` shell, which runs against an uninitialized SDK (#46803), so + # the JSON-mode choice stays out of it. + if litellm.json_logs: + print_startup_message(LITELLM_BANNER) + else: + show_banner() if os.getenv("LITELLM_DONT_SHOW_FEEDBACK_BOX", "").lower() != "true": generate_feedback_box() user_model = model diff --git a/tests/unit/proxy/common_utils/test_banner.py b/tests/unit/proxy/common_utils/test_banner.py deleted file mode 100644 index 3c2b140b2ee..00000000000 --- a/tests/unit/proxy/common_utils/test_banner.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The CLI banner is terminal art in plain mode and one JSON record when json_logs is on.""" - -import json -import sys -from typing import Final - -import litellm -from litellm._logging import turn_on_json -from litellm.proxy.common_utils.banner import LITELLM_BANNER, show_banner - - -def test_show_banner_prints_the_banner_between_blank_lines_in_plain_mode(monkeypatch, capfd): - """Operators without JSON logs see the banner exactly as before.""" - monkeypatch.setattr(litellm, "json_logs", False) - - show_banner() - - assert capfd.readouterr() == (f"\n{LITELLM_BANNER}\n\n", "") - - -def test_show_banner_is_one_json_record_in_json_mode(monkeypatch, capfd): - """With JSON logs on, the six banner rows are one INFO record with their indentation kept, so the - first line a worker writes parses like every other.""" - monkeypatch.setattr(litellm, "json_logs", True) - monkeypatch.setattr(sys, "excepthook", sys.excepthook) # turn_on_json swaps it; put it back at teardown - turn_on_json() - - show_banner() - - out, err = capfd.readouterr() - assert err == "" - lines: Final = [raw for raw in out.splitlines() if raw.strip()] - assert len(lines) == 1, lines - record: Final = json.loads(lines[0]) - assert record["message"] == LITELLM_BANNER - assert record["level"] == "INFO" diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index 235bab10900..1da5e5372ad 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -48,6 +48,7 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.hooks.parallel_request_limiter_v3 import RequestRateLimiterStash from litellm.proxy.proxy_server import app, aws_error_type_exception_handler, initialize, openai_exception_handler from litellm.utils import _invalidate_model_cost_lowercase_map +from litellm.proxy.common_utils.banner import LITELLM_BANNER from tests._master_key import MASTER_KEY PUBLICLY_KNOWN_KEY: Final = "sk-" + "1234" @@ -15725,3 +15726,36 @@ def test_feedback_box_is_one_json_record_in_json_mode(monkeypatch, capfd): assert "Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new" in record["message"] assert "Thank you for using LiteLLM! - Krrish & Ishaan" in record["message"] assert "\x1b[" not in record["message"] + + +@pytest.mark.asyncio +async def test_initialize_prints_the_banner_between_blank_lines_in_plain_mode(monkeypatch, capfd): + """Operators without JSON logs see the banner exactly as before, through show_banner()'s flushing + click.echo.""" + monkeypatch.setattr(litellm, "json_logs", False) + monkeypatch.setenv("LITELLM_DONT_SHOW_FEEDBACK_BOX", "true") + + await proxy_server_module.initialize() + + out, err = capfd.readouterr() + assert err == "" + assert out.startswith(f"\n{LITELLM_BANNER}\n\n"), out + + +@pytest.mark.asyncio +async def test_initialize_prints_the_banner_as_one_json_record_in_json_mode(monkeypatch, capfd): + """With JSON logs on, the six banner rows are one INFO record with their indentation kept, so the + first line a worker writes parses like every other.""" + monkeypatch.setattr(litellm, "json_logs", True) + monkeypatch.setattr(sys, "excepthook", sys.excepthook) # turn_on_json swaps it; put it back at teardown + monkeypatch.setenv("LITELLM_DONT_SHOW_FEEDBACK_BOX", "true") + turn_on_json() + + await proxy_server_module.initialize() + + out, err = capfd.readouterr() + assert err == "" + records: Final = [json.loads(raw) for raw in out.splitlines() if raw.strip()] + banner_records: Final = [record for record in records if record["message"] == LITELLM_BANNER] + assert len(banner_records) == 1, records + assert banner_records[0]["level"] == "INFO"