This commit is contained in:
Rachit 2026-11-07 03:39:32 +00:00 • committed by GitHub
commit 8525412620
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 186 additions and 29 deletions

View file

@ -542,12 +542,16 @@ def _set_request_attributes(
litellm_params.get("custom_llm_provider", "Unknown"),
)
if optional_params.get("max_tokens"):
safe_set_attribute(span, "llm.request.max_tokens", optional_params.get("max_tokens"))
if optional_params.get("temperature"):
safe_set_attribute(span, "llm.request.temperature", optional_params.get("temperature"))
if optional_params.get("top_p"):
safe_set_attribute(span, "llm.request.top_p", optional_params.get("top_p"))
for source_keys, request_attribute in (
(("max_tokens", "max_completion_tokens"), "llm.request.max_tokens"),
(("temperature",), "llm.request.temperature"),
(("top_p",), "llm.request.top_p"),
):
for source_key in source_keys:
request_value = optional_params.get(source_key)
if request_value is not None:
safe_set_attribute(span, request_attribute, request_value)
break
safe_set_attribute(span, "llm.is_streaming", str(optional_params.get("stream", False)))

View file

@ -2552,29 +2552,16 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
value=provider_name,
)
# The maximum number of tokens the LLM generates for a request.
if optional_params.get("max_tokens"):
self.safe_set_attribute(
span=span,
key=SpanAttributes.LLM_REQUEST_MAX_TOKENS.value,
value=optional_params.get("max_tokens"),
)
# The temperature setting for the LLM request.
if optional_params.get("temperature"):
self.safe_set_attribute(
span=span,
key=SpanAttributes.LLM_REQUEST_TEMPERATURE.value,
value=optional_params.get("temperature"),
)
# The top_p sampling setting for the LLM request.
if optional_params.get("top_p"):
self.safe_set_attribute(
span=span,
key=SpanAttributes.LLM_REQUEST_TOP_P.value,
value=optional_params.get("top_p"),
)
for source_keys, request_attribute in (
(("max_tokens", "max_completion_tokens"), SpanAttributes.LLM_REQUEST_MAX_TOKENS.value),
(("temperature",), SpanAttributes.LLM_REQUEST_TEMPERATURE.value),
(("top_p",), SpanAttributes.LLM_REQUEST_TOP_P.value),
):
for source_key in source_keys:
request_value = optional_params.get(source_key)
if request_value is not None:
self.safe_set_attribute(span=span, key=request_attribute, value=request_value)
break
if self._gen_ai_semconv_latest_experimental:
# Semconv emits gen_ai.request.stream (only when streaming) via

View file

@ -185,6 +185,88 @@ def test_arize_set_attributes():
span.set_attribute.assert_any_call(SpanAttributes.LLM_TOKEN_COUNT_PROMPT, 40)
def test_arize_records_zero_valued_sampling_params():
"""A sampling param the caller set to 0 still has to reach the span.
temperature=0 is what you send for deterministic output, so dropping it
hides the single most common setting from the trace.
"""
from unittest.mock import MagicMock
from litellm.types.utils import ModelResponse
span = MagicMock()
kwargs = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Basic Request Content"}],
"standard_logging_object": {
"model_parameters": {},
"metadata": {},
"call_type": "completion",
},
"optional_params": {"max_tokens": 0, "temperature": 0, "top_p": 0},
"litellm_params": {"custom_llm_provider": "openai"},
}
response_obj = ModelResponse(
usage={"total_tokens": 100, "completion_tokens": 60, "prompt_tokens": 40},
choices=[Choices(message={"role": "assistant", "content": "Basic Response Content"})],
model="gpt-4o",
id="chatcmpl-ID",
)
ArizeLogger.set_arize_attributes(span, kwargs, response_obj)
span.set_attribute.assert_any_call("llm.request.temperature", 0)
span.set_attribute.assert_any_call("llm.request.top_p", 0)
span.set_attribute.assert_any_call("llm.request.max_tokens", 0)
@pytest.mark.parametrize(
"optional_params, expected",
[
({"max_completion_tokens": 256}, 256),
({"max_tokens": 128}, 128),
({"max_tokens": 128, "max_completion_tokens": 256}, 128),
({"max_tokens": 0, "max_completion_tokens": 256}, 0),
],
)
def test_arize_records_max_tokens_from_either_request_field(optional_params, expected):
"""Callers send max_completion_tokens instead of max_tokens, so the span has to
read both. max_tokens wins when present, including when it is 0, which matches
how LLMRequestParams.from_model_parameters resolves the two.
"""
from unittest.mock import MagicMock
from litellm.types.utils import ModelResponse
span = MagicMock()
kwargs = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Basic Request Content"}],
"standard_logging_object": {
"model_parameters": {},
"metadata": {},
"call_type": "completion",
},
"optional_params": optional_params,
"litellm_params": {"custom_llm_provider": "openai"},
}
response_obj = ModelResponse(
usage={"total_tokens": 100, "completion_tokens": 60, "prompt_tokens": 40},
choices=[Choices(message={"role": "assistant", "content": "Basic Response Content"})],
model="gpt-4o",
id="chatcmpl-ID",
)
ArizeLogger.set_arize_attributes(span, kwargs, response_obj)
span.set_attribute.assert_any_call("llm.request.max_tokens", expected)
def test_arize_set_attributes_responses_api():
"""
Test setting attributes for Responses API with mixed output (reasoning + message).

View file

@ -690,6 +690,90 @@ class TestOpenTelemetryCostBreakdown(unittest.TestCase):
assert all(not key.startswith("gen_ai.cost.") for key, _value in call_args_list)
class TestOpenTelemetryZeroValuedRequestParams(unittest.TestCase):
def test_zero_valued_sampling_params_reach_the_span(self):
"""A sampling param the caller set to 0 still has to reach the span.
temperature=0 is what you send for deterministic output, so dropping it
hides the single most common setting from the trace. The semconv path
already records these, so the legacy path has to agree.
"""
otel = OpenTelemetry()
mock_span = MagicMock()
kwargs = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"optional_params": {"max_tokens": 0, "temperature": 0, "top_p": 0},
"litellm_params": {"custom_llm_provider": "openai"},
"standard_logging_object": {
"id": "test-id",
"call_type": "completion",
"metadata": {},
},
}
response_obj = {
"id": "test-response-id",
"model": "gpt-4",
"choices": [],
"usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
}
otel.set_attributes(span=mock_span, kwargs=kwargs, response_obj=response_obj)
from litellm.proxy._types import SpanAttributes
recorded = dict(call[0] for call in mock_span.set_attribute.call_args_list if len(call[0]) == 2)
assert recorded.get(SpanAttributes.LLM_REQUEST_TEMPERATURE.value) == 0
assert recorded.get(SpanAttributes.LLM_REQUEST_TOP_P.value) == 0
assert recorded.get(SpanAttributes.LLM_REQUEST_MAX_TOKENS.value) == 0
@parameterized.expand(
[
({"max_completion_tokens": 256}, 256),
({"max_tokens": 128}, 128),
({"max_tokens": 128, "max_completion_tokens": 256}, 128),
({"max_tokens": 0, "max_completion_tokens": 256}, 0),
]
)
def test_max_tokens_is_read_from_either_request_field(self, optional_params, expected):
"""Callers send max_completion_tokens instead of max_tokens, so the span has to
read both. max_tokens wins when present, including when it is 0, which matches
how LLMRequestParams.from_model_parameters resolves the two.
"""
from litellm.proxy._types import SpanAttributes
otel = OpenTelemetry()
mock_span = MagicMock()
kwargs = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"optional_params": optional_params,
"litellm_params": {"custom_llm_provider": "openai"},
"standard_logging_object": {
"id": "test-id",
"call_type": "completion",
"metadata": {},
},
}
response_obj = {
"id": "test-response-id",
"model": "gpt-4",
"choices": [],
"usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
}
otel.set_attributes(span=mock_span, kwargs=kwargs, response_obj=response_obj)
recorded = dict(call[0] for call in mock_span.set_attribute.call_args_list if len(call[0]) == 2)
assert recorded.get(SpanAttributes.LLM_REQUEST_MAX_TOKENS.value) == expected
class TestOpenTelemetryProviderInitialization(unittest.TestCase):
"""Test suite for verifying provider initialization respects existing providers"""