mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test: drop the cwd-relative sys.path.insert calls from the test suite
TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.
Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.
Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.
* test: drop the duplicate imports the sys.path sweep exposed to F811
* test(pre-call-utils): restore the os import the new bedrock tests need
210 lines
8.2 KiB
Python
210 lines
8.2 KiB
Python
# What is this?
|
|
## Unit tests for opentelemetry integration
|
|
|
|
# What is this?
|
|
## Unit test for presidio pii masking
|
|
import sys, os, asyncio, time, random
|
|
from datetime import datetime
|
|
import traceback
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
import pytest
|
|
import litellm
|
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
from base_test import BaseLoggingCallbackTest
|
|
from litellm.types.utils import ModelResponse
|
|
|
|
|
|
class TestOpentelemetryUnitTests(BaseLoggingCallbackTest):
|
|
def test_parallel_tool_calls(self, mock_response_obj: ModelResponse):
|
|
tool_calls = mock_response_obj.choices[0].message.tool_calls
|
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
|
from litellm.proxy._types import SpanAttributes
|
|
|
|
kv_pair_dict = OpenTelemetry._tool_calls_kv_pair(tool_calls)
|
|
|
|
assert kv_pair_dict == {
|
|
f"{SpanAttributes.LLM_COMPLETIONS.value}.0.function_call.arguments": '{"city": "New York"}',
|
|
f"{SpanAttributes.LLM_COMPLETIONS.value}.0.function_call.name": "get_weather",
|
|
f"{SpanAttributes.LLM_COMPLETIONS.value}.1.function_call.arguments": '{"city": "New York"}',
|
|
f"{SpanAttributes.LLM_COMPLETIONS.value}.1.function_call.name": "get_news",
|
|
}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_opentelemetry_integration(self):
|
|
"""
|
|
Unit test to confirm external parent otel spans are NOT ended by LiteLLM.
|
|
|
|
External spans (passed via metadata) should be managed by their creators,
|
|
not by LiteLLM. This prevents premature closure of spans from Langfuse,
|
|
user code, or other external observability tools.
|
|
"""
|
|
# Reset all callbacks to ensure clean state
|
|
litellm.logging_callback_manager._reset_all_callbacks()
|
|
|
|
parent_otel_span = MagicMock()
|
|
litellm.callbacks = ["otel"]
|
|
|
|
await litellm.acompletion(
|
|
model="gpt-5-mini",
|
|
messages=[{"role": "user", "content": "Hello, world!"}],
|
|
mock_response="Hey!",
|
|
metadata={"litellm_parent_otel_span": parent_otel_span},
|
|
)
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
# Verify external span was NOT ended by LiteLLM
|
|
# External spans should only be closed by their creators
|
|
parent_otel_span.end.assert_not_called()
|
|
|
|
def test_get_span_context_detects_active_span(self):
|
|
"""
|
|
Unit test: _get_span_context() should auto-detect active spans from global context.
|
|
|
|
Active spans should be automatically detected without explicit metadata
|
|
"""
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
|
|
|
# Setup: Create TracerProvider and tracer
|
|
tracer_provider = TracerProvider()
|
|
trace.set_tracer_provider(tracer_provider)
|
|
tracer = trace.get_tracer(__name__)
|
|
|
|
# Create OpenTelemetry integration
|
|
otel_integration = OpenTelemetry()
|
|
|
|
# Act: Create an active span and test detection
|
|
with tracer.start_as_current_span("test_parent") as parent_span:
|
|
parent_span_context = parent_span.get_span_context()
|
|
|
|
# Call _get_span_context without explicit parent in metadata
|
|
kwargs = {"litellm_params": {"metadata": {}}}
|
|
detected_context, detected_span = otel_integration._get_span_context(kwargs)
|
|
|
|
# Assert: Should detect the active span
|
|
assert (
|
|
detected_span is not None
|
|
), "Should detect active span from global context"
|
|
assert (
|
|
detected_span is parent_span
|
|
), "Detected span should be the active parent span"
|
|
|
|
detected_span_context = detected_span.get_span_context()
|
|
assert (
|
|
detected_span_context.trace_id == parent_span_context.trace_id
|
|
), "Detected span should have same trace_id as parent"
|
|
assert (
|
|
detected_span_context.span_id == parent_span_context.span_id
|
|
), "Detected span should have same span_id as parent"
|
|
|
|
def test_record_exception_on_span(self):
|
|
"""
|
|
Test that _record_exception_on_span properly records exception information.
|
|
|
|
This test verifies that StandardLoggingPayloadErrorInformation is properly
|
|
extracted and set as span attributes using ErrorAttributes constants.
|
|
"""
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
|
from litellm.integrations._types.open_inference import ErrorAttributes
|
|
|
|
# Setup: Create TracerProvider and tracer
|
|
tracer_provider = TracerProvider()
|
|
trace.set_tracer_provider(tracer_provider)
|
|
tracer = trace.get_tracer(__name__)
|
|
|
|
# Create OpenTelemetry integration
|
|
otel_integration = OpenTelemetry()
|
|
|
|
# Create a mock span
|
|
mock_span = MagicMock()
|
|
|
|
# Create test exception
|
|
test_exception = ValueError("Test error message")
|
|
|
|
# Create kwargs with exception and error_information
|
|
kwargs = {
|
|
"exception": test_exception,
|
|
"standard_logging_object": {
|
|
"error_information": {
|
|
"error_code": "500",
|
|
"error_class": "ValueError",
|
|
"llm_provider": "openai",
|
|
"traceback": "Traceback (most recent call last)...",
|
|
"error_message": "Test error message",
|
|
},
|
|
"error_str": "Test error message",
|
|
},
|
|
}
|
|
|
|
# Act: Record exception on span
|
|
otel_integration._record_exception_on_span(span=mock_span, kwargs=kwargs)
|
|
|
|
# Assert: span.record_exception should be called with the exception
|
|
mock_span.record_exception.assert_called_once_with(test_exception)
|
|
|
|
# Assert: Error attributes should be set using ErrorAttributes constants
|
|
expected_calls = [
|
|
(ErrorAttributes.ERROR_CODE, "500"),
|
|
(ErrorAttributes.ERROR_TYPE, "ValueError"),
|
|
(ErrorAttributes.ERROR_MESSAGE, "Test error message"),
|
|
(ErrorAttributes.ERROR_LLM_PROVIDER, "openai"),
|
|
(ErrorAttributes.ERROR_STACK_TRACE, "Traceback (most recent call last)..."),
|
|
]
|
|
|
|
# Check that set_attribute was called with expected values
|
|
actual_calls = [call.args for call in mock_span.set_attribute.call_args_list]
|
|
|
|
for expected_call in expected_calls:
|
|
assert (
|
|
expected_call in actual_calls
|
|
), f"Expected set_attribute call {expected_call} not found in actual calls: {actual_calls}"
|
|
|
|
def test_record_exception_on_span_with_fallback(self):
|
|
"""
|
|
Test that _record_exception_on_span falls back to error_str when error_information is None.
|
|
"""
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
|
from litellm.integrations._types.open_inference import ErrorAttributes
|
|
|
|
# Setup: Create TracerProvider and tracer
|
|
tracer_provider = TracerProvider()
|
|
trace.set_tracer_provider(tracer_provider)
|
|
tracer = trace.get_tracer(__name__)
|
|
|
|
# Create OpenTelemetry integration
|
|
otel_integration = OpenTelemetry()
|
|
|
|
# Create a mock span
|
|
mock_span = MagicMock()
|
|
|
|
# Create test exception
|
|
test_exception = ValueError("Test error message")
|
|
|
|
# Create kwargs without error_information (should fallback to error_str)
|
|
kwargs = {
|
|
"exception": test_exception,
|
|
"standard_logging_object": {
|
|
"error_information": None,
|
|
"error_str": "Fallback error message",
|
|
},
|
|
}
|
|
|
|
# Act: Record exception on span
|
|
otel_integration._record_exception_on_span(span=mock_span, kwargs=kwargs)
|
|
|
|
# Assert: span.record_exception should be called
|
|
mock_span.record_exception.assert_called_once_with(test_exception)
|
|
|
|
# Assert: error.message should be set from error_str using ErrorAttributes constant
|
|
mock_span.set_attribute.assert_called_with(
|
|
ErrorAttributes.ERROR_MESSAGE, "Fallback error message"
|
|
)
|