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
21 changes: 9 additions & 12 deletions litellm/integrations/otel/model/semconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,21 @@ class Error:
"""OTel-defined error attribute keys, from the semconv ``error.*`` registry.
``MESSAGE`` is marked *Deprecated* upstream in favor of domain-specific
error message keys plus ``exception.message`` on the exception event, but
is still defined and stamped by litellm's v1 integration; keeping it here
for byte-for-byte parity."""
litellm still stamps it."""

TYPE: Final = "error.type"
MESSAGE: Final = "error.message"


class LiteLLMError:
"""LiteLLM-specific error attribute keys. Emitted under the ``error.*``
namespace (not ``litellm.*``) for byte-for-byte compat with the v1
integration in ``opentelemetry.py``; consumers reading these keys on v1
spans read the same keys on v2 spans. OTel semconv does not define any of
these three, and per its extension rules a namespace may carry additional
vendor keys as long as they don't collide with defined names."""

CODE: Final = "error.code"
STACK_TRACE: Final = "error.stack_trace"
LLM_PROVIDER: Final = "error.llm_provider"
"""Detail keys for the mapped provider exception of a failed LLM call.
OTel semconv does not define these, so they live under the ``litellm.*``
vendor namespace rather than squatting on the semconv-owned ``error.*``
namespace."""

CODE: Final = "litellm.provider.error.code"
STACK_TRACE: Final = "litellm.provider.error.stack_trace"
LLM_PROVIDER: Final = "litellm.provider.error.llm_provider"


class ExceptionEvent:
Expand Down
28 changes: 13 additions & 15 deletions tests/test_litellm/integrations/otel/test_otel_v2_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,7 @@ def test_error_details_stamped_as_span_attributes_for_labels_ingest():
"""OTel-defined keys and litellm-specific detail keys both ride span
attributes so backends that flatten attrs into label indexes (Elastic APM
``labels.*``, Datadog span tags) render them. The exception event with the
full untruncated message stays alongside — both places, matching v1's
shape."""
full untruncated message stays alongside."""
from litellm.integrations.otel.model.semconv import Error, ExceptionEvent, LiteLLMError
from litellm.integrations.otel.emitter import SpanEmitter

Expand Down Expand Up @@ -638,8 +637,8 @@ def test_error_details_stamped_as_span_attributes_for_labels_ingest():
# OTel-defined keys (from the ``error.*`` semconv registry).
assert span.attributes[Error.TYPE] == "litellm.BadRequestError"
assert span.attributes[Error.MESSAGE] == "400: violated moderation policy"
# LiteLLM-specific detail keys — vendor-namespaced under ``error.*``
# for v1-parity, not defined by OTel semconv.
# LiteLLM-specific detail keys, under the ``litellm.provider.error.*``
# vendor namespace, not defined by OTel semconv.
assert span.attributes[LiteLLMError.CODE] == "400"
assert span.attributes[LiteLLMError.STACK_TRACE] == "File proxy_server.py line 8570 ..."
assert span.attributes[LiteLLMError.LLM_PROVIDER] == "openai"
Expand All @@ -666,19 +665,18 @@ def test_error_details_omitted_when_span_error_carries_only_message():
assert LiteLLMError.LLM_PROVIDER not in span.attributes


def test_v2_error_attribute_keys_match_v1_error_attributes_byte_for_byte():
"""v1 (``opentelemetry.py``) and v2 (``otel/`` package) stamp identical
span-attribute keys so consumers reading ``labels.error_message`` don't
care which integration produced the span. Renaming either side is a
breaking change for downstream dashboards; this test locks the vocabulary."""
from litellm.integrations._types.open_inference import ErrorAttributes
def test_error_attribute_keys_are_pinned():
"""``error.type`` and ``error.message`` come from the semconv ``error.*``
registry; the litellm-specific detail keys are vendor keys under
``litellm.provider.error.*``. Pins the exact strings so the emitted
vocabulary can't drift silently."""
from litellm.integrations.otel.model.semconv import Error, LiteLLMError

assert Error.TYPE == ErrorAttributes.ERROR_TYPE
assert Error.MESSAGE == ErrorAttributes.ERROR_MESSAGE
assert LiteLLMError.CODE == ErrorAttributes.ERROR_CODE
assert LiteLLMError.STACK_TRACE == ErrorAttributes.ERROR_STACK_TRACE
assert LiteLLMError.LLM_PROVIDER == ErrorAttributes.ERROR_LLM_PROVIDER
assert Error.TYPE == "error.type"
assert Error.MESSAGE == "error.message"
assert LiteLLMError.CODE == "litellm.provider.error.code"
assert LiteLLMError.STACK_TRACE == "litellm.provider.error.stack_trace"
assert LiteLLMError.LLM_PROVIDER == "litellm.provider.error.llm_provider"


def test_error_message_falls_back_to_error_type_when_message_absent():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ def test_attribute_keys_are_unique_across_namespaces():
from litellm.integrations.otel import MCP, Client, JsonRpc, LiteLLMError, Network

# prefixes are allowed to be substrings; exact keys must not collide.
# ``LiteLLMError`` shares the ``error.*`` prefix with ``Error`` by design
# (v1-parity); the assert below is the guarantee they never overlap.
exact = set()
for cls in (GenAI, Error, LiteLLMError, Server, HTTP, DB, MCP, JsonRpc, Network, Client):
for key in _all_constants(cls):
Expand Down
Loading