Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions litellm/integrations/otel/mappers/genai.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class GenAIMapper:
f"{LiteLLM.COST_PREFIX}margin_percent": lambda d: d.cost.margin_percent,
f"{LiteLLM.COST_PREFIX}margin_total_amount": lambda d: d.cost.margin_total_amount,
LiteLLM.REQUEST_STREAMING: lambda d: d.is_streaming,
LiteLLM.REQUEST_ROUTE: lambda d: d.identity.request_route or None,
}

_TOOL_ATTRS: dict[str, Callable[[ToolDefinition], AttrValue | None]] = {
Expand Down
4 changes: 4 additions & 0 deletions litellm/integrations/otel/model/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class RequestIdentity:
# completes (routing has picked a deployment), so it's absent from the
# auth-time seed and filled only from the payload.
provider_model: str | None = None
# The normalized proxy route (``/v1/chat/completions``), set by auth after the
# Baggage seed, so like ``provider_model`` it is only filled from the payload.
request_route: str | None = None
metadata: Mapping[str, str] = field(default_factory=dict)

@classmethod
Expand All @@ -87,6 +90,7 @@ def from_payload(cls, payload: StandardLoggingPayload) -> RequestIdentity:
key_hash=as_str(raw_meta.get("user_api_key_hash")),
end_user=as_str(payload.get("end_user")) or as_str(raw_meta.get("user_api_key_end_user_id")),
provider_model=resolve_provider_model(payload),
request_route=as_str(raw_meta.get("user_api_key_request_route")),
metadata=metadata,
)

Expand Down
1 change: 1 addition & 0 deletions litellm/integrations/otel/model/semconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ class LiteLLM:
# ``litellm_params.model``), distinct from the user-facing ``gen_ai.request.model``.
PROVIDER_MODEL: Final = "litellm.provider.model"
REQUEST_STREAMING: Final = "litellm.request.streaming"
REQUEST_ROUTE: Final = "litellm.request.route"
TOOLS_DECLARED: Final = "litellm.request.tools.declared"
GUARDRAIL_NAME: Final = "litellm.guardrail.name"
GUARDRAIL_MODE: Final = "litellm.guardrail.mode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,26 @@ def test_request_identity_falls_back_to_legacy_team_keys():
assert ident.team_alias == "legacy"


def test_llm_span_carries_proxy_request_route():
"""The LLM span records the proxy route the request arrived on, so it can be
filtered by endpoint (``/v1/responses`` vs ``/v1/chat/completions``) without
joining back to the root SERVER span's ``http.route``."""
data = LLMCallSpanData.from_standard_logging_payload(
_sample_payload(metadata={"user_api_key_request_route": "/v1/responses"})
)
attrs = GenAIMapper().map(data)

Comment thread
greptile-apps[bot] marked this conversation as resolved.
assert data.identity.request_route == "/v1/responses"
assert attrs[LiteLLM.REQUEST_ROUTE] == "/v1/responses"


def test_llm_span_omits_request_route_off_the_proxy():
"""An SDK call has no inbound route, so the key is absent rather than empty."""
attrs = GenAIMapper().map(LLMCallSpanData.from_standard_logging_payload(_sample_payload(metadata={})))

assert LiteLLM.REQUEST_ROUTE not in attrs


def test_guardrail_span_data_block_carries_verdict_and_error():
from litellm.integrations.otel.model.payloads import GuardrailSpanData

Expand Down
Loading