mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(logging): keep tracebacks for provider-originated 4xx errors
is_expected_client_error treated every HTTP 4xx as a rejection the proxy issued itself, so a 401 or 429 the provider returned lost its traceback in the standard logging payload and the OTel error span dropped litellm.provider.error.stack_trace. An exception carrying llm_provider is an upstream or deployment problem and keeps its traceback; the proxy's own pre-call rejections still skip it
This commit is contained in:
parent
72b8b47ee8
commit
68f5cbd522
4 changed files with 71 additions and 1 deletions
|
|
@ -60,13 +60,19 @@ def safe_divide(
|
|||
|
||||
def is_expected_client_error(exception: BaseException | None) -> bool:
|
||||
"""
|
||||
True when the exception maps to an HTTP 4xx status.
|
||||
True when the proxy itself rejected the request with an HTTP 4xx before any
|
||||
provider call (bad key, budget, unknown model, guardrail). A 4xx returned by
|
||||
a provider (the exception carries ``llm_provider``) is an upstream or
|
||||
deployment problem, so it is never an expected client error and keeps its
|
||||
traceback.
|
||||
|
||||
ProxyException stores the status on .code (as a str), HTTPException and
|
||||
litellm exceptions on .status_code.
|
||||
"""
|
||||
if exception is None:
|
||||
return False
|
||||
if getattr(exception, "llm_provider", None):
|
||||
return False
|
||||
code: Final[object] = getattr(exception, "code", None)
|
||||
status_code: Final[object] = code if code is not None else getattr(exception, "status_code", None)
|
||||
if status_code is None or isinstance(status_code, bool):
|
||||
|
|
|
|||
|
|
@ -330,6 +330,30 @@ def test_real_llm_failure_still_emitted():
|
|||
assert span.status.status_code is StatusCode.ERROR
|
||||
|
||||
|
||||
def test_provider_auth_failure_span_carries_stack_trace():
|
||||
"""Regression for LIT-6163: a 401 the provider returned is not an expected
|
||||
client error, so the error span built from the real failure payload keeps
|
||||
``litellm.provider.error.stack_trace`` alongside code and llm_provider."""
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
|
||||
try:
|
||||
raise AuthenticationError(
|
||||
message="AnthropicException - API key is invalid.", llm_provider="anthropic", model="claude-haiku-4-5"
|
||||
)
|
||||
except AuthenticationError as caught:
|
||||
error_information = StandardLoggingPayloadSetup.get_error_information(caught)
|
||||
logger, exporter = _logger()
|
||||
payload = _payload(status="failure", custom_llm_provider="anthropic", error_information=error_information)
|
||||
_emit_llm(logger, _kwargs(payload=payload), fail=True)
|
||||
(span,) = exporter.get_finished_spans()
|
||||
assert span.status.status_code is StatusCode.ERROR
|
||||
assert span.attributes["error.type"] == "AuthenticationError"
|
||||
assert span.attributes["litellm.provider.error.code"] == "401"
|
||||
assert span.attributes["litellm.provider.error.llm_provider"] == "anthropic"
|
||||
assert "test_otel_v2_logger" in span.attributes["litellm.provider.error.stack_trace"]
|
||||
|
||||
|
||||
def test_idempotent_on_repeat_callback():
|
||||
"""The carrier is the dedup: once the async callback closes the span and
|
||||
clears the carrier, a second callback firing emits nothing."""
|
||||
|
|
|
|||
|
|
@ -278,3 +278,25 @@ class TestIsExpectedClientError:
|
|||
assert is_expected_client_error(WithCode("invalid_request_error")) is False
|
||||
assert is_expected_client_error(Exception("no status")) is False
|
||||
assert is_expected_client_error(None) is False
|
||||
|
||||
def test_provider_originated_4xx_is_not_expected(self):
|
||||
"""Regression for LIT-6163: a 4xx the provider returned is an upstream or
|
||||
deployment problem, so it keeps its traceback; only the proxy's own
|
||||
pre-call rejections (no llm_provider) are expected client errors."""
|
||||
from litellm.exceptions import AuthenticationError, RateLimitError
|
||||
from litellm.litellm_core_utils.core_helpers import is_expected_client_error
|
||||
|
||||
provider_auth_failure = AuthenticationError(
|
||||
message="AnthropicException - API key is invalid.", llm_provider="anthropic", model="claude-haiku-4-5"
|
||||
)
|
||||
assert is_expected_client_error(provider_auth_failure) is False
|
||||
|
||||
provider_rate_limit = RateLimitError(message="rate limited upstream", llm_provider="openai", model="gpt-4o")
|
||||
assert is_expected_client_error(provider_rate_limit) is False
|
||||
|
||||
class RouterRejection(Exception):
|
||||
def __init__(self):
|
||||
self.status_code = 429
|
||||
self.llm_provider = ""
|
||||
|
||||
assert is_expected_client_error(RouterRejection()) is True
|
||||
|
|
|
|||
|
|
@ -5714,6 +5714,24 @@ def test_get_error_information_skips_traceback_for_expected_4xx(monkeypatch):
|
|||
assert "test_litellm_logging" in result["traceback"]
|
||||
|
||||
|
||||
def test_get_error_information_keeps_traceback_for_provider_4xx():
|
||||
"""Regression for LIT-6163: a 4xx the provider returned (invalid deployment
|
||||
key, upstream validation) is an operator problem, so its traceback must
|
||||
survive the expected-client-error gate and reach every payload consumer."""
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
|
||||
assert litellm.log_client_error_tracebacks is False
|
||||
provider_exc = _raise_and_catch(
|
||||
litellm.AuthenticationError(
|
||||
message="AnthropicException - API key is invalid.", llm_provider="anthropic", model="claude-haiku-4-5"
|
||||
)
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.get_error_information(provider_exc)
|
||||
assert result["error_code"] == "401"
|
||||
assert result["llm_provider"] == "anthropic"
|
||||
assert "test_litellm_logging" in result["traceback"]
|
||||
|
||||
|
||||
def test_failure_handler_helper_fn_builds_payload_once_per_exception():
|
||||
"""Regression for LIT-6043: async and sync failure handlers both call
|
||||
_failure_handler_helper_fn for the same failed request; the standardized
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue