mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
fix(opentelemetry): normalize list-valued guardrail_mode for span dedupe (#28486)
`_emit_once` builds the per-request dedupe key as
`(self.__class__.__name__, id(self), *scope)` and looks it up in a
dict. When the guardrail is configured with a list-valued mode
(e.g. `mode: ["pre_call", "post_call"]`), `_create_guardrail_span`
passes that list straight into `scope`, so the resulting tuple is
unhashable. `dict.get` then raises `TypeError: unhashable type:
'list'` and the proxy returns HTTP/500 for every guardrailed
request when OTel is enabled.
Normalize `guardrail_mode` to a tuple at the call site so the
hashable-scope contract documented on `_emit_once` is honored,
without changing what is emitted as the span attribute.
Regression test exercises the failing code path; it raised before
this change and passes after.
Refs #28486. Bisected to 1c4e4d4 ("Fix 3 OpenTelemetry tracing
bugs in proxy integration (#27757)").
This commit is contained in:
parent
b60d4677cd
commit
b012ffe74b
2 changed files with 37 additions and 1 deletions
|
|
@ -1565,12 +1565,19 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
# ``_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")
|
||||
if isinstance(guardrail_mode, list):
|
||||
guardrail_mode = tuple(guardrail_mode)
|
||||
if not self._emit_once(
|
||||
kwargs,
|
||||
"guardrail",
|
||||
guardrail_information.get("guardrail_name"),
|
||||
start_time_float,
|
||||
guardrail_information.get("guardrail_mode"),
|
||||
guardrail_mode,
|
||||
):
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,35 @@ class TestOpenTelemetryGuardrails(unittest.TestCase):
|
|||
]
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue