mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #38296 from BerriAI/litellm_fix_otel_provider_error_stack_trace
Some checks are pending
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Postgres Tests / proxy-security (push) Waiting to run
Postgres Tests / schema-migration (push) Waiting to run
Postgres Tests / proxy-behavior (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests / integrations (push) Waiting to run
Unit Tests / All Other Providers (push) Waiting to run
Unit Tests / Vertex AI (push) Waiting to run
Unit Tests / misc (push) Waiting to run
Unit Tests / caching-local (push) Waiting to run
Unit Tests / core-utils (push) Waiting to run
Unit Tests / enterprise-package (push) Waiting to run
Unit Tests / enterprise-routing (push) Waiting to run
Unit Tests / proxy-auth (push) Waiting to run
Unit Tests / proxy-endpoints (push) Waiting to run
Unit Tests / proxy-extras (push) Waiting to run
Unit Tests / proxy-infra (push) Waiting to run
Unit Tests / proxy-server (push) Waiting to run
Unit Tests / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Some checks are pending
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Postgres Tests / proxy-security (push) Waiting to run
Postgres Tests / schema-migration (push) Waiting to run
Postgres Tests / proxy-behavior (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests / integrations (push) Waiting to run
Unit Tests / All Other Providers (push) Waiting to run
Unit Tests / Vertex AI (push) Waiting to run
Unit Tests / misc (push) Waiting to run
Unit Tests / caching-local (push) Waiting to run
Unit Tests / core-utils (push) Waiting to run
Unit Tests / enterprise-package (push) Waiting to run
Unit Tests / enterprise-routing (push) Waiting to run
Unit Tests / proxy-auth (push) Waiting to run
Unit Tests / proxy-endpoints (push) Waiting to run
Unit Tests / proxy-extras (push) Waiting to run
Unit Tests / proxy-infra (push) Waiting to run
Unit Tests / proxy-server (push) Waiting to run
Unit Tests / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
fix(logging): keep tracebacks for provider-originated 4xx errors
This commit is contained in:
commit
3e2927de9a
4 changed files with 187 additions and 1 deletions
|
|
@ -58,15 +58,56 @@ def safe_divide(
|
|||
return numerator / denominator
|
||||
|
||||
|
||||
def _is_litellm_limit_rejection(exception: BaseException) -> bool:
|
||||
from litellm.exceptions import RateLimitErrorCategory
|
||||
|
||||
litellm_limit_categories: Final = frozenset(
|
||||
(RateLimitErrorCategory.LITELLM_RATE_LIMIT.value, RateLimitErrorCategory.LITELLM_BATCH_RATE_LIMIT.value)
|
||||
)
|
||||
return getattr(exception, "category", None) in litellm_limit_categories
|
||||
|
||||
|
||||
def _is_proxy_rejection(exception: BaseException) -> bool:
|
||||
if _is_litellm_limit_rejection(exception):
|
||||
return True
|
||||
try:
|
||||
from starlette.exceptions import HTTPException
|
||||
except ImportError:
|
||||
return False
|
||||
return isinstance(exception, HTTPException)
|
||||
|
||||
|
||||
def _is_provider_originated(exception: BaseException) -> bool:
|
||||
if _is_proxy_rejection(exception):
|
||||
return False
|
||||
if getattr(exception, "llm_provider", None):
|
||||
return True
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
return isinstance(exception, BaseLLMException)
|
||||
|
||||
|
||||
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 is an upstream or deployment problem, so it is never an expected
|
||||
client error and keeps its traceback: a mapped litellm exception carries
|
||||
``llm_provider``, and the raw ``BaseLLMException`` that provider handlers
|
||||
raise before mapping (the /v1/messages route surfaces it as-is) is one too.
|
||||
The proxy's own limiters raise ``HTTPException`` subclasses that also carry
|
||||
an ``llm_provider``, so any ``HTTPException`` stays a proxy rejection, and
|
||||
so does any exception whose unified rate-limit ``category`` names litellm's
|
||||
own limiter (``BudgetExceededError`` is a plain ``Exception`` that the auth
|
||||
handler decorates with the requested model's provider).
|
||||
|
||||
ProxyException stores the status on .code (as a str), HTTPException and
|
||||
litellm exceptions on .status_code.
|
||||
"""
|
||||
if exception is None:
|
||||
return False
|
||||
if _is_provider_originated(exception):
|
||||
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,50 @@ 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_unmapped_provider_auth_failure_span_carries_stack_trace():
|
||||
"""Regression for LIT-6163 on /v1/messages: that route logs the provider's
|
||||
raw exception (no llm_provider), and its error span keeps the stack trace."""
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
from litellm.llms.anthropic.common_utils import AnthropicError
|
||||
|
||||
try:
|
||||
raise AnthropicError(status_code=401, message='{"type":"authentication_error","message":"API key is invalid."}')
|
||||
except AnthropicError 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"] == "AnthropicError"
|
||||
assert span.attributes["litellm.provider.error.code"] == "401"
|
||||
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,59 @@ 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
|
||||
from litellm.llms.anthropic.common_utils import AnthropicError
|
||||
from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError
|
||||
|
||||
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
|
||||
|
||||
unmapped_provider_failure = AnthropicError(status_code=401, message='{"type":"authentication_error"}')
|
||||
assert is_expected_client_error(unmapped_provider_failure) is False
|
||||
|
||||
proxy_rate_limit = ProxyRateLimitError(
|
||||
detail={"error": "Max parallel requests reached"}, model="claude-haiku-4-5", llm_provider="anthropic"
|
||||
)
|
||||
assert proxy_rate_limit.llm_provider == "anthropic"
|
||||
assert is_expected_client_error(proxy_rate_limit) is True
|
||||
|
||||
class RouterRejection(Exception):
|
||||
def __init__(self):
|
||||
self.status_code = 429
|
||||
self.llm_provider = ""
|
||||
|
||||
assert is_expected_client_error(RouterRejection()) is True
|
||||
|
||||
def test_budget_rejection_decorated_with_provider_is_expected(self):
|
||||
"""The auth handler stamps the requested model's provider onto the proxy's
|
||||
own BudgetExceededError before logging it, which must not turn a key-over-budget
|
||||
429 into a provider error that keeps its traceback."""
|
||||
from litellm.exceptions import BudgetExceededError, RateLimitError, RateLimitErrorCategory
|
||||
from litellm.litellm_core_utils.core_helpers import is_expected_client_error
|
||||
|
||||
over_budget = BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic")
|
||||
assert over_budget.llm_provider == "anthropic"
|
||||
assert is_expected_client_error(over_budget) is True
|
||||
|
||||
litellm_limit = RateLimitError(
|
||||
message="key over rpm", llm_provider="anthropic", model="claude-haiku-4-5",
|
||||
category=RateLimitErrorCategory.LITELLM_RATE_LIMIT,
|
||||
)
|
||||
assert is_expected_client_error(litellm_limit) is True
|
||||
|
||||
vendor_limit = RateLimitError(
|
||||
message="rate limited upstream", llm_provider="anthropic", model="claude-haiku-4-5",
|
||||
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
|
||||
)
|
||||
assert is_expected_client_error(vendor_limit) is False
|
||||
|
|
|
|||
|
|
@ -5714,6 +5714,51 @@ 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_get_error_information_keeps_traceback_for_unmapped_provider_4xx():
|
||||
"""Regression for LIT-6163 on /v1/messages: that route logs the provider's
|
||||
raw BaseLLMException (no llm_provider), which still keeps its traceback."""
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
from litellm.llms.anthropic.common_utils import AnthropicError
|
||||
|
||||
assert litellm.log_client_error_tracebacks is False
|
||||
raw_provider_exc = _raise_and_catch(AnthropicError(status_code=401, message='{"type":"authentication_error"}'))
|
||||
result = StandardLoggingPayloadSetup.get_error_information(raw_provider_exc)
|
||||
assert result["error_code"] == "401"
|
||||
assert result["error_class"] == "AnthropicError"
|
||||
assert "test_litellm_logging" in result["traceback"]
|
||||
|
||||
|
||||
def test_get_error_information_skips_traceback_for_budget_rejection_with_provider():
|
||||
"""A key-over-budget 429 is the proxy's own rejection even after the auth
|
||||
handler stamps the requested model's provider onto it, so it stays cheap."""
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
|
||||
assert litellm.log_client_error_tracebacks is False
|
||||
over_budget = _raise_and_catch(litellm.BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic"))
|
||||
result = StandardLoggingPayloadSetup.get_error_information(over_budget)
|
||||
assert result["error_code"] == "429"
|
||||
assert result["llm_provider"] == "anthropic"
|
||||
assert 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