mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
Merge cdae4e044e into e9b1c11098
This commit is contained in:
commit
8525412620
4 changed files with 186 additions and 29 deletions
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue