Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion litellm/integrations/opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,12 +1565,22 @@ def _create_guardrail_span(
# ``_handle_failure``) and re-reads the (mutating) entry list
# each time. Dedupe at entry granularity so a single real
# guardrail invocation produces exactly one span per handler.
#
# ``guardrail_mode`` is user-configured and may be a list
# (e.g. ``mode: ["pre_call", "post_call"]``); ``_emit_once``
# requires hashable scope parts, so normalize lists to tuples.
guardrail_mode = guardrail_information.get("guardrail_mode")
hashable_guardrail_mode: Any = (
tuple(guardrail_mode)
if isinstance(guardrail_mode, list)
else guardrail_mode
)
if not self._emit_once(
kwargs,
"guardrail",
guardrail_information.get("guardrail_name"),
start_time_float,
guardrail_information.get("guardrail_mode"),
hashable_guardrail_mode,
):
continue

Expand Down
29 changes: 29 additions & 0 deletions tests/test_litellm/integrations/test_opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ def test_guardrail_response_none_is_skipped(self, mock_datetime):
]
self.assertNotIn("guardrail_response", attribute_keys)

def test_create_guardrail_span_with_list_mode_is_hashable(self):
"""Regression: when ``guardrail_mode`` is configured as a list
(e.g. ``mode: ["pre_call", "post_call"]``), ``_emit_once`` builds
a dedupe-key tuple containing the mode; using the list directly
raised ``TypeError: unhashable type: 'list'`` and produced an
HTTP/500 for every guardrailed request. The mode must be
normalized to a hashable form before lookup.
"""
otel = OpenTelemetry()
otel.tracer = MagicMock()
mock_span = MagicMock()
otel.tracer.start_span.return_value = mock_span

guardrail_info = {
"guardrail_name": "custom-guardrail",
"guardrail_mode": ["pre_call", "post_call"],
"start_time": 1609459200.0,
"end_time": 1609459201.0,
}
kwargs = {
"standard_logging_object": {"guardrail_information": [guardrail_info]}
}

# Must not raise ``TypeError: unhashable type: 'list'``.
otel._create_guardrail_span(kwargs=kwargs, context=None)

otel.tracer.start_span.assert_called_once()
mock_span.set_attribute.assert_any_call("guardrail_name", "custom-guardrail")


class TestOpenTelemetryTeamAttributesOnChildSpans(unittest.TestCase):
"""team_id / team_alias must land on every child span of a
Expand Down
Loading