diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index aeb52a3d239..1580e615f22 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3695,6 +3695,11 @@ class ProxyException(Exception): provider_specific_fields: Optional[dict] = None, ): self.message = str(message) + # Populate Exception.args so str(self) returns the message. + # Without this, logging paths that call str(original_exception) + # (e.g. StandardLoggingPayloadSetup.get_error_information) record an + # empty error_message for ProxyException-based failures. See LIT-3094. + super().__init__(self.message) self.type = type self.param = param self.openai_code = openai_code or code diff --git a/tests/test_litellm/proxy/test_proxy_types.py b/tests/test_litellm/proxy/test_proxy_types.py index dbb952968ed..ed6e7bb8191 100644 --- a/tests/test_litellm/proxy/test_proxy_types.py +++ b/tests/test_litellm/proxy/test_proxy_types.py @@ -87,3 +87,76 @@ def test_user_api_key_auth_hashes_authorization_header_form_of_key(): assert from_header.api_key == baseline.api_key assert from_header.token == baseline.token assert not from_header.api_key.lower().startswith("bearer") + + +# === Regression tests for LIT-3094: ProxyException must populate Exception.args +# so logging integrations using str(exc) record a non-empty error_message. === + + +def test_proxy_exception_str_returns_message(): + """str(ProxyException) must return the stored message, not '' (LIT-3094).""" + from litellm.proxy._types import ProxyException + + msg = "key not allowed to access model" + exc = ProxyException(message=msg, type="auth_error", param=None, code=401) + assert str(exc) == msg + assert exc.args == (msg,) + assert exc.message == msg + + +def test_proxy_exception_populates_standard_logging_error_message(): + """The full logging path used by proxy callbacks must capture the message + instead of recording an empty error_message (LIT-3094 report).""" + from litellm.litellm_core_utils.litellm_logging import ( + StandardLoggingPayloadSetup, + ) + from litellm.proxy._types import ProxyException + + msg = "Authentication Error, Invalid proxy server token passed." + exc = ProxyException(message=msg, type="auth_error", param=None, code=401) + info = StandardLoggingPayloadSetup.get_error_information(original_exception=exc) + assert info["error_message"] == msg + assert info["error_class"] == "ProxyException" + assert info["error_code"] == "401" + + +def test_proxy_exception_to_dict_unchanged(): + """to_dict() shape must remain backwards-compatible after the fix.""" + from litellm.proxy._types import ProxyException + + exc = ProxyException( + message="boom", type="invalid_request_error", param="model", code=400 + ) + d = exc.to_dict() + assert d == { + "message": "boom", + "type": "invalid_request_error", + "param": "model", + "code": "400", + } + + +def test_proxy_exception_routing_code_override_still_works(): + """The 'No healthy deployment available' -> 429 remapping must survive + the super().__init__() addition.""" + from litellm.proxy._types import ProxyException + + exc = ProxyException( + message="No healthy deployment available for model=foo", + type="router_error", + param=None, + code=500, + ) + assert exc.code == "429" + assert str(exc) == "No healthy deployment available for model=foo" + + +def test_proxy_exception_non_string_message_coerced(): + """Non-string `message` must still be coerced to str via self.message = + str(message), and Exception.args must reflect the coerced value.""" + from litellm.proxy._types import ProxyException + + exc = ProxyException(message=42, type="x", param=None, code=400) + assert exc.message == "42" + assert str(exc) == "42" + assert exc.args == ("42",)