From 719e3fee9c7f9d97f85eb768267aaa83315d32dd Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:24:31 -0700 Subject: [PATCH] fix(cli): strip control characters from a 401 body before redaction and keep a reason when nothing printable is left --- .../proxy/client/cli/commands/auto_sign_in.py | 8 +++-- .../proxy/client/cli/test_auto_sign_in.py | 34 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/client/cli/commands/auto_sign_in.py b/litellm/proxy/client/cli/commands/auto_sign_in.py index 79894abdebb..cd718c1f1b5 100644 --- a/litellm/proxy/client/cli/commands/auto_sign_in.py +++ b/litellm/proxy/client/cli/commands/auto_sign_in.py @@ -210,11 +210,15 @@ def _attempt_with_newer_stored_key( return _attempt_with_key(run, ctx, stored_key) +def _printable(text: str) -> str: + return _TERMINAL_CONTROL_CHARACTERS.sub("", text).strip() + + def _proxy_reason(error: UnauthorizedError) -> str: original: Final = error.orig_exception response: Final = original.response if isinstance(original, requests.HTTPError) else None - body: Final = response.text.strip() if response is not None else "" - reason: Final = _TERMINAL_CONTROL_CHARACTERS.sub("", redact_string(body) if body else str(error)) + body: Final = response.text if response is not None else "" + reason: Final = redact_string(_printable(body) or _printable(str(error))) return reason if len(reason) <= _PROXY_REASON_CHARS else f"{reason[:_PROXY_REASON_CHARS]}..." diff --git a/tests/unit/proxy/client/cli/test_auto_sign_in.py b/tests/unit/proxy/client/cli/test_auto_sign_in.py index 661019e0677..e2851132899 100644 --- a/tests/unit/proxy/client/cli/test_auto_sign_in.py +++ b/tests/unit/proxy/client/cli/test_auto_sign_in.py @@ -656,7 +656,9 @@ def test_terminal_control_characters_in_a_401_body_never_reach_the_terminal( requests_mock: Mocker, secret_vault_factory: type[FakeSecretVault] ) -> None: requests_mock.post( - f"{BASE_URL}/key/generate", status_code=401, text="\x1b]0;title\x07refused\x1b[2J\r\nsecond line\x9b31m" + f"{BASE_URL}/key/generate", + status_code=401, + text="\x1b]0;title\x07refused\x1b[2J\rnext\tcolumn\nsecond line\x9b31m", ) sign_in: Final = FakeBrowserSignIn(_credential()) @@ -664,11 +666,39 @@ def test_terminal_control_characters_in_a_401_body_never_reach_the_terminal( assert result.exit_code == 1 assert "refused" in result.stderr - assert "second line" in result.stderr + assert "next\tcolumn\nsecond line" in result.stderr assert not {"\x1b", "\x07", "\x9b", "\r"} & set(result.stderr) assert sign_in.calls == [] +def test_a_key_split_by_a_control_character_in_a_401_body_stays_redacted( + requests_mock: Mocker, secret_vault_factory: type[FakeSecretVault] +) -> None: + key: Final = "sk-" + "a1b2c3d4" * 4 + requests_mock.post(f"{BASE_URL}/key/generate", status_code=401, text=f"refused {key[:12]}\x07{key[12:]}") + + result: Final = _lite( + ["keys", "generate"], secret_vault_factory(), FakeBrowserSignIn(_credential()), ["--api-key", "sk-explicit"] + ) + + assert result.exit_code == 1 + assert "refused" in result.stderr + assert key not in result.stderr + + +def test_a_401_body_of_only_control_characters_still_gives_a_reason( + requests_mock: Mocker, secret_vault_factory: type[FakeSecretVault] +) -> None: + requests_mock.post(f"{BASE_URL}/key/generate", status_code=401, text="\x1b\x07\r\x9b") + + result: Final = _lite( + ["keys", "generate"], secret_vault_factory(), FakeBrowserSignIn(_credential()), ["--api-key", "sk-explicit"] + ) + + assert result.exit_code == 1 + assert result.stderr.partition("The proxy said: ")[2].strip() + + def test_a_failed_sign_in_names_the_reason_and_points_at_lite_login( requests_mock: Mocker, secret_vault_factory: type[FakeSecretVault] ) -> None: