This commit is contained in:
Yash Singh 2026-11-04 05:21:11 +00:00 • committed by GitHub
commit 9db8e855b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 78 additions and 2 deletions

View file

@ -58,6 +58,7 @@ from litellm.integrations.otel.model.semconv import (
Metric,
Network,
NetworkTransport,
OpenAI,
RpcSystem,
Server,
resolve_operation,
@ -102,6 +103,7 @@ __all__ = [
"Metric",
"Network",
"NetworkTransport",
"OpenAI",
"OpenTelemetryV2Config",
"ProxyRequestSpanData",
"RequestContext",

View file

@ -34,6 +34,7 @@ from litellm.integrations.otel.model.semconv import (
GenAI,
JsonRpc,
LiteLLM,
OpenAI,
RpcSystem,
Server,
)
@ -63,6 +64,8 @@ class GenAIMapper:
GenAI.RESPONSE_ID: lambda d: d.response_id,
GenAI.RESPONSE_FINISH_REASONS: lambda d: list(d.finish_reasons) if d.finish_reasons else None,
GenAI.RESPONSE_TIME_TO_FIRST_CHUNK: lambda d: d.time_to_first_chunk_seconds,
OpenAI.REQUEST_SERVICE_TIER: lambda d: d.requested_service_tier,
OpenAI.RESPONSE_SERVICE_TIER: lambda d: d.served_service_tier,
GenAI.USAGE_INPUT_TOKENS: lambda d: d.usage.input_tokens,
GenAI.USAGE_OUTPUT_TOKENS: lambda d: d.usage.output_tokens,
GenAI.USAGE_CACHE_CREATION_INPUT_TOKENS: lambda d: d.usage.cache_creation_input_tokens,

View file

@ -36,6 +36,7 @@ from litellm.integrations.otel.model.utils import (
as_str_tuple,
)
from litellm.integrations.otel.routing import RoutingAttributeValue, routing_decision_attributes
from litellm.litellm_core_utils.service_tier_utils import get_requested_service_tier, get_served_service_tier
# ``RequestIdentity`` and the request-metadata translation now live in
# :mod:`metadata`; re-exported here so existing ``model.payloads`` imports keep
@ -450,6 +451,8 @@ class LLMCallSpanData:
routing_attributes: Mapping[str, RoutingAttributeValue] = field(default_factory=lambda: MappingProxyType({}))
cache_hit: bool | None = None
saved_cache_cost: float | None = None
requested_service_tier: str | None = None
served_service_tier: str | None = None
@classmethod
def from_standard_logging_payload(
@ -531,6 +534,8 @@ class LLMCallSpanData:
routing_attributes=routing_decision_attributes(decision),
cache_hit=cache_hit,
saved_cache_cost=as_float(payload.get("saved_cache_cost")) if cache_hit else None, # pyright: ignore[reportUnknownMemberType] # TypedDict .get on a str key is partially unknown
requested_service_tier=get_requested_service_tier(payload),
served_service_tier=get_served_service_tier(payload),
)

View file

@ -133,6 +133,14 @@ class GenAI:
PROMPT_NAME: Final = "gen_ai.prompt.name"
class OpenAI:
"""OpenAI-namespace keys from the GenAI semconv registry. Carries the service tier for every
provider, not only OpenAI."""
REQUEST_SERVICE_TIER: Final = "openai.request.service_tier"
RESPONSE_SERVICE_TIER: Final = "openai.response.service_tier"
class MCP:
"""OTel GenAI MCP (Model Context Protocol) span-attribute keys.

View file

@ -183,6 +183,64 @@ def test_llm_call_span_golden():
assert span.status.status_code is StatusCode.UNSET
_REQUEST_TIER: Final = "openai.request.service_tier"
_RESPONSE_TIER: Final = "openai.response.service_tier"
@pytest.mark.parametrize(
("overrides", "expected_request_tier", "expected_response_tier"),
[
pytest.param(
{
"model_parameters": {"service_tier": "flex"},
"response": {"id": "resp_1", "service_tier": "flex", "choices": [{"finish_reason": "stop"}]},
},
"flex",
"flex",
id="requested-and-served",
),
pytest.param(
{
"model_parameters": {"service_tier": "auto"},
"response": {"id": "resp_1", "service_tier": "priority", "choices": [{"finish_reason": "stop"}]},
},
"auto",
"priority",
id="provider-picks-the-tier",
),
pytest.param(
{"metadata": {"usage_object": {"service_tier": "standard"}}},
None,
"standard",
id="served-tier-on-usage",
),
pytest.param(
{"model_parameters": {"service_tier": "priority"}},
"priority",
None,
id="requested-only",
),
pytest.param(
{"model_parameters": {"service_tier": "not-a-tier"}},
None,
None,
id="unknown-requested-tier",
),
pytest.param({}, None, None, id="no-tier"),
],
)
def test_llm_call_span_service_tier(
overrides: Mapping[str, object], expected_request_tier: str | None, expected_response_tier: str | None
) -> None:
engine, exporter = _engine()
engine.emit(SpanRole.LLM_CALL, LLMCallSpanData.from_standard_logging_payload(_payload(**overrides)))
(span,) = exporter.get_finished_spans()
assert span.attributes.get(_REQUEST_TIER) == expected_request_tier
assert span.attributes.get(_RESPONSE_TIER) == expected_response_tier
def test_legacy_dual_emit_on():
engine, exporter = _engine(legacy_compat=True)
engine.emit(SpanRole.LLM_CALL, LLMCallSpanData.from_standard_logging_payload(_payload()))

View file

@ -159,11 +159,11 @@ def _all_constants(cls):
def test_attribute_keys_are_unique_across_namespaces():
from litellm.integrations.otel import MCP, Client, JsonRpc, LiteLLMError, Network
from litellm.integrations.otel import MCP, Client, JsonRpc, LiteLLMError, Network, OpenAI
# prefixes are allowed to be substrings; exact keys must not collide.
exact = set()
for cls in (GenAI, Error, LiteLLMError, Server, HTTP, DB, MCP, JsonRpc, Network, Client):
for cls in (GenAI, Error, LiteLLMError, Server, HTTP, DB, MCP, JsonRpc, Network, Client, OpenAI):
for key in _all_constants(cls):
assert key not in exact, f"duplicate attribute key {key}"
exact.add(key)