diff --git a/litellm/litellm_core_utils/realtime_streaming.py b/litellm/litellm_core_utils/realtime_streaming.py index 984934daaac..b448cb7c9ff 100644 --- a/litellm/litellm_core_utils/realtime_streaming.py +++ b/litellm/litellm_core_utils/realtime_streaming.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Final, NoReturn, Protocol, TypedDict, cas from typing_extensions import ReadOnly import litellm -from litellm._logging import verbose_logger +from litellm._logging import _redact_string, verbose_logger from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig from litellm.types.llms.openai import ( @@ -1567,12 +1567,14 @@ class RealTimeStreaming: await asyncio.gather(forward_task, client_task, return_exceptions=True) async def _close_client(self, close: BackendClose) -> None: + redacted_message: Final = _redact_string(close.message) + redacted_reason: Final = _redact_string(close.reason) try: if close.code != 1000: - await self.websocket.send_text(realtime_error_event(close.message, error_type="server_error")) + await self.websocket.send_text(realtime_error_event(redacted_message, error_type="server_error")) await self.websocket.close( code=client_close_code(close.code), - reason=websocket_close_reason(close.reason, fallback=close.message), + reason=websocket_close_reason(redacted_reason, fallback=redacted_message), ) except Exception as e: # noqa: BLE001 # the client may already be gone; the session is over either way verbose_logger.debug("Could not relay the upstream close to the client: %s", e) diff --git a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py index 09757c35570..bcfacf16205 100644 --- a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py +++ b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py @@ -3229,6 +3229,24 @@ async def test_bidirectional_forward_relays_upstream_policy_close_to_client(): client_ws.close.assert_awaited_once_with(code=1008, reason=_UPSTREAM_REFUSAL) +@pytest.mark.asyncio +async def test_upstream_close_reason_with_a_secret_is_redacted_before_reaching_the_client(): + """LIT-6973: the relayed close mirrors the handshake path and scrubs credential + patterns, so an upstream error echoing a token never reaches the client verbatim.""" + secret: Final = "sk-live-abcdef0123456789abcdef0123" + client_ws: Final = _client_ws_that_never_sends() + upstream_close: Final = ConnectionClosed(Close(1008, f"auth failed for {secret}"), None) + session: Final = _relay_session(client_ws, _backend_ws_closing_with(upstream_close)) + + await session.run() + + (error_event,) = _error_events_sent_to(client_ws) + assert secret not in error_event["error"]["message"] + relayed_reason: Final = client_ws.close.await_args.kwargs["reason"] + assert secret not in relayed_reason + assert "REDACTED" in relayed_reason + + @pytest.mark.asyncio async def test_bidirectional_forward_maps_abnormal_upstream_close_to_internal_error(): client_ws: Final = _client_ws_that_never_sends()