diff --git a/litellm/proxy/client/cli/commands/auto_sign_in.py b/litellm/proxy/client/cli/commands/auto_sign_in.py index 10f5955bbac..79894abdebb 100644 --- a/litellm/proxy/client/cli/commands/auto_sign_in.py +++ b/litellm/proxy/client/cli/commands/auto_sign_in.py @@ -1,4 +1,5 @@ import os +import re import subprocess import sys import time @@ -41,6 +42,7 @@ _DISPLAY_VARIABLES: Final = ("DISPLAY", "WAYLAND_DISPLAY") _SIGNED_IN_LOGIN_WITHOUT_KEY_ROW: Final = HTTPStatus.NOT_FOUND _WAIT_FOR_ANOTHER_SIGN_IN_SECONDS: Final = LOGIN_TIMEOUT_SECONDS + 60 _PROXY_REASON_CHARS: Final = 500 +_TERMINAL_CONTROL_CHARACTERS: Final = re.compile(r"[\x00-\x08\x0b-\x1f\x7f-\x9f]") def _runs_in_ci(environ: Mapping[str, str]) -> bool: @@ -87,7 +89,7 @@ def _locate_browser( def _shows_a_window(browser: webbrowser.BaseBrowser) -> bool: - return Path(browser.name).name not in _TEXT_MODE_BROWSERS + return Path(getattr(browser, "name", "")).name not in _TEXT_MODE_BROWSERS @dataclass(frozen=True, slots=True) @@ -212,7 +214,7 @@ 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 = redact_string(body) if body else str(error) + reason: Final = _TERMINAL_CONTROL_CHARACTERS.sub("", redact_string(body) if body else 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 869066e1404..661019e0677 100644 --- a/tests/unit/proxy/client/cli/test_auto_sign_in.py +++ b/tests/unit/proxy/client/cli/test_auto_sign_in.py @@ -307,6 +307,18 @@ def test_an_ssh_session_without_a_display_never_opens_a_browser_on_the_remote_sc assert requests_mock.call_count == 0 +class BrowserWithoutAName(webbrowser.BaseBrowser): + def __init__(self) -> None: + self._name = "default" + + +def test_a_browser_object_without_a_name_still_starts_the_sign_in(requests_mock: Mocker) -> None: + sign_in: Final = PkceBrowserSignIn(find_browser=lambda _name: BrowserWithoutAName(), environ={}) + + assert sign_in.unavailable_because() is None + assert requests_mock.call_count == 0 + + @pytest.mark.parametrize( "environ", [ @@ -640,6 +652,23 @@ def test_a_long_401_body_is_cut_before_it_reaches_the_terminal( assert sign_in.calls == [] +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" + ) + sign_in: Final = FakeBrowserSignIn(_credential()) + + result: Final = _lite(["keys", "generate"], secret_vault_factory(), sign_in, ["--api-key", "sk-explicit"]) + + assert result.exit_code == 1 + assert "refused" in result.stderr + assert "second line" in result.stderr + assert not {"\x1b", "\x07", "\x9b", "\r"} & set(result.stderr) + assert sign_in.calls == [] + + def test_a_failed_sign_in_names_the_reason_and_points_at_lite_login( requests_mock: Mocker, secret_vault_factory: type[FakeSecretVault] ) -> None: