Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions litellm/litellm_core_utils/litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5135,15 +5135,25 @@ def get_error_information(
tb_lines[:MAXIMUM_TRACEBACK_LINES_TO_LOG]
) # Limit to first 100 lines

# Get additional error details
error_message = str(original_exception)
# Get additional error details.
# Prefer the `.message` attribute (set by ProxyException and every
# litellm.exceptions.* class) over str(exc). ProxyException does not
# call super().__init__() nor define __str__, so str() on it returns
# an empty string — which used to silently strip the human-readable
# message from spend_logs.metadata.error_information and made
# dashboard "LLM Failure" rows untriagable. See e2e/cases/11.
message_attr = getattr(original_exception, "message", None)
if message_attr:
error_message = str(message_attr)
else:
error_message = str(original_exception) if original_exception else ""

return StandardLoggingPayloadErrorInformation(
error_code=error_status,
error_class=error_class,
llm_provider=_llm_provider_in_exception,
traceback=traceback_info,
error_message=error_message if original_exception else "",
error_message=error_message,
)

@staticmethod
Expand Down
54 changes: 54 additions & 0 deletions tests/test_litellm/litellm_core_utils/test_litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,60 @@ def __init__(self, message):
assert result["error_class"] == "NoCodeException"


def test_get_error_information_prefers_message_attribute_over_str():
"""
Regression for empty-error_message-in-spend-logs.

ProxyException sets `self.message` but does NOT call
`super().__init__(message)` nor define `__str__`, so `str(exc)`
returns the empty string. Before the fix, get_error_information
used `str(original_exception)` and silently stripped the
human-readable message from spend_logs.metadata.error_information,
making dashboard "LLM Failure" rows un-triagable.

Asserts the `.message` attribute is consulted first.
"""
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup

# Simulate a ProxyException-shaped exception: .message set, but
# super().__init__() NOT called and no __str__ override.
class ProxyExceptionLike(Exception):
def __init__(self, message, code):
self.message = str(message)
self.code = str(code)
# NOTE: deliberately NOT calling super().__init__(message)

msg = "Authentication Error, Invalid proxy server token passed. key=..."
exc = ProxyExceptionLike(message=msg, code=401)

# Sanity check: this exception type's str() really is empty
assert str(exc) == "", (
"Test premise broken — bare-base Exception now returns message; "
"review whether ProxyException fix landed at the class level instead"
)

result = StandardLoggingPayloadSetup.get_error_information(exc)
assert (
result["error_message"] == msg
), f"expected message from .message attribute, got {result['error_message']!r}"
assert result["error_code"] == "401"
assert result["error_class"] == "ProxyExceptionLike"


def test_get_error_information_falls_back_to_str_when_no_message_attr():
"""
Plain Exception (no `.message` attr) must still produce a useful
error_message via str(exc), preserving prior behavior for
non-litellm exception types.
"""
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup

exc = ValueError("boom")
result = StandardLoggingPayloadSetup.get_error_information(exc)
assert result["error_message"] == "boom"
assert result["error_class"] == "ValueError"


# ──────────────────────────────────────────────────────────────────────
# Tests for _get_assembled_streaming_response non-streaming early return
# ──────────────────────────────────────────────────────────────────────
Expand Down
Loading