From 252395eb70c121a854de098dd6f5bab599707141 Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Tue, 6 Oct 2026 15:54:36 +0800 Subject: [PATCH 1/4] fix(otel): respect per-request message redaction on raw span (#44504) set_raw_request_attributes dumps llm..messages / choices verbatim from the un-redacted payloads, so a request that asked for redaction (x-litellm-enable-message-redaction header, dynamic param, or the global turn_off_message_logging switch) still leaked its content through the raw_gen_ai_request span. Skip creating the raw span when message redaction applies, using the same precedence as the main span. Rebased onto latest upstream main. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/integrations/opentelemetry.py | 10 ++++ tests/unit/integrations/test_opentelemetry.py | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 90383ddb60d..4e7df367072 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1485,6 +1485,16 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): if not self._capture_in_span(): return + # Per-request message redaction must apply to the raw payload span too: + # set_raw_request_attributes dumps llm..messages / choices + # verbatim, so without this guard the very content redaction is meant + # to keep out of traces still leaks through the raw span. This uses the + # same precedence as the main span (dynamic param > redaction headers > + # global turn_off_message_logging). + from litellm.litellm_core_utils.redact_messages import should_redact_message_logging + + if should_redact_message_logging(kwargs): + return litellm_params: Final = kwargs.get("litellm_params", {}) metadata: Final = litellm_params.get("metadata") or {} generation_name: Final = metadata.get("generation_name") diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py index 56049c53736..db64bea4a69 100644 --- a/tests/unit/integrations/test_opentelemetry.py +++ b/tests/unit/integrations/test_opentelemetry.py @@ -7328,3 +7328,55 @@ class TestOpentelemetryUnitTests(BaseLoggingCallbackTest): # 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") + +class TestOpenTelemetryRawSpanRespectsRedaction(unittest.TestCase): + """#44504: per-request message redaction must also skip the raw payload span. + + ``set_raw_request_attributes`` dumps ``llm..messages`` / ``choices`` + verbatim from the un-redacted ``complete_input_dict`` and ``original_response``, + so without this guard a request that asked for redaction (the + ``x-litellm-enable-message-redaction`` header, a dynamic param, or the global + ``turn_off_message_logging`` switch) still leaks its content through the + ``raw_gen_ai_request`` span. + """ + + @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) + def test_skips_raw_span_when_redaction_header_set(self): + """Redaction requested via the per-request header -> raw span is not created.""" + from litellm.integrations.opentelemetry import OpenTelemetry + + otel = OpenTelemetry() + otel.message_logging = True + + mock_tracer = MagicMock() + mock_span = MagicMock() + mock_tracer.start_span.return_value = mock_span + otel.get_tracer_to_use_for_request = MagicMock(return_value=mock_tracer) + otel.set_raw_request_attributes = MagicMock() + otel._to_ns = MagicMock(return_value=1234567890) + + kwargs = {"litellm_params": {"metadata": {"headers": {"x-litellm-enable-message-redaction": "true"}}}} + otel._maybe_log_raw_request(kwargs, {}, datetime.now(), datetime.now(), MagicMock()) + mock_tracer.start_span.assert_not_called() + otel.set_raw_request_attributes.assert_not_called() + + @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) + def test_creates_raw_span_when_no_redaction_requested(self): + """No redaction requested (empty headers dict) -> raw span is still created.""" + from litellm.integrations.opentelemetry import RAW_REQUEST_SPAN_NAME, OpenTelemetry + + otel = OpenTelemetry() + otel.message_logging = True + + mock_tracer = MagicMock() + mock_span = MagicMock() + mock_tracer.start_span.return_value = mock_span + otel.get_tracer_to_use_for_request = MagicMock(return_value=mock_tracer) + otel.set_raw_request_attributes = MagicMock() + otel._to_ns = MagicMock(return_value=1234567890) + + kwargs = {"litellm_params": {"metadata": {"headers": {}}}} + otel._maybe_log_raw_request(kwargs, {}, datetime.now(), datetime.now(), MagicMock()) + mock_tracer.start_span.assert_called_once() + self.assertEqual(mock_tracer.start_span.call_args[1]["name"], RAW_REQUEST_SPAN_NAME) + otel.set_raw_request_attributes.assert_called_once() From ce23baa0bca2d350257b2c6c40b6db8b54269e57 Mon Sep 17 00:00:00 2001 From: apex-mochen <2756823972@qq.com> Date: Fri, 9 Oct 2026 00:24:41 +0800 Subject: [PATCH 2/4] fix(tests): exempt mock-echo assertions from TQ002 in #44504 otel tests Both new regression tests for #44504 assert on the OpenTelemetry tracer mock, which the repository's test-quality TQ002 rule flags as mock-echo. The observable behavior under test is precisely the absence/presence of a raw span, which can only be observed through the tracer mock, so both carry an explicit test-quality-ok exemption. Signed-off-by: apex-mochen <2756823972@qq.com> --- tests/unit/integrations/test_opentelemetry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py index db64bea4a69..ec4f3bc3789 100644 --- a/tests/unit/integrations/test_opentelemetry.py +++ b/tests/unit/integrations/test_opentelemetry.py @@ -7342,6 +7342,7 @@ class TestOpenTelemetryRawSpanRespectsRedaction(unittest.TestCase): @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) def test_skips_raw_span_when_redaction_header_set(self): + # test-quality-ok: negative-path test; the observable behavior is precisely that no raw span is created, which is only assertable on the tracer mock """Redaction requested via the per-request header -> raw span is not created.""" from litellm.integrations.opentelemetry import OpenTelemetry @@ -7362,6 +7363,7 @@ class TestOpenTelemetryRawSpanRespectsRedaction(unittest.TestCase): @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) def test_creates_raw_span_when_no_redaction_requested(self): + # test-quality-ok: positive-path twin of the test above; asserts the span name and raw-attribute hook via the tracer mock, matching the existing file style """No redaction requested (empty headers dict) -> raw span is still created.""" from litellm.integrations.opentelemetry import RAW_REQUEST_SPAN_NAME, OpenTelemetry From a6c01e051ea0f8e8acdfa40799d1b59ab2e885e8 Mon Sep 17 00:00:00 2001 From: apex-mochen <2756823972@qq.com> Date: Fri, 9 Oct 2026 00:26:18 +0800 Subject: [PATCH 3/4] fix(tests): move TQ002 exemptions onto the reported lines Place the test-quality-ok exemptions for the #44504 regression tests on the def line itself, matching the repo convention that a suppression sits on the reported line. Signed-off-by: apex-mochen <2756823972@qq.com> --- tests/unit/integrations/test_opentelemetry.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py index ec4f3bc3789..5f75f2309a2 100644 --- a/tests/unit/integrations/test_opentelemetry.py +++ b/tests/unit/integrations/test_opentelemetry.py @@ -7341,8 +7341,7 @@ class TestOpenTelemetryRawSpanRespectsRedaction(unittest.TestCase): """ @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) - def test_skips_raw_span_when_redaction_header_set(self): - # test-quality-ok: negative-path test; the observable behavior is precisely that no raw span is created, which is only assertable on the tracer mock + def test_skips_raw_span_when_redaction_header_set(self): # test-quality-ok: negative-path test; the observable behavior is precisely that no raw span is created, which is only assertable on the tracer mock """Redaction requested via the per-request header -> raw span is not created.""" from litellm.integrations.opentelemetry import OpenTelemetry @@ -7362,8 +7361,7 @@ class TestOpenTelemetryRawSpanRespectsRedaction(unittest.TestCase): otel.set_raw_request_attributes.assert_not_called() @patch.dict(os.environ, {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": ""}) - def test_creates_raw_span_when_no_redaction_requested(self): - # test-quality-ok: positive-path twin of the test above; asserts the span name and raw-attribute hook via the tracer mock, matching the existing file style + def test_creates_raw_span_when_no_redaction_requested(self): # test-quality-ok: positive-path twin of the test above; asserts the span name and raw-attribute hook via the tracer mock, matching the existing file style """No redaction requested (empty headers dict) -> raw span is still created.""" from litellm.integrations.opentelemetry import RAW_REQUEST_SPAN_NAME, OpenTelemetry From a945c093bb89521c75be840bf877d3ded6d1fd03 Mon Sep 17 00:00:00 2001 From: apex-mochen <2756823972@qq.com> Date: Fri, 9 Oct 2026 13:28:15 +0000 Subject: [PATCH 4/4] fix(otel): keep the raw-span redaction guard inside the type gate The guard added two basedpyright diagnostics over the merge-base counts: reportUnknownVariableType on the import of the callee (whose own signature is untyped upstream) and reportUnknownArgumentType on the untyped request kwargs. Suppress both on their reported lines with a reason, the shape the repo's LIT004 rule requires; no behavior change. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/integrations/opentelemetry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 4e7df367072..2c58542ceec 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1491,9 +1491,11 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): # to keep out of traces still leaks through the raw span. This uses the # same precedence as the main span (dynamic param > redaction headers > # global turn_off_message_logging). - from litellm.litellm_core_utils.redact_messages import should_redact_message_logging + from litellm.litellm_core_utils.redact_messages import ( + should_redact_message_logging, # pyright: ignore[reportUnknownVariableType] # callee is untyped upstream + ) - if should_redact_message_logging(kwargs): + if should_redact_message_logging(kwargs): # pyright: ignore[reportUnknownArgumentType] # untyped by design return litellm_params: Final = kwargs.get("litellm_params", {}) metadata: Final = litellm_params.get("metadata") or {}