diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index 9f5b19d8d02..a623be49a0c 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -16,6 +16,7 @@ from litellm.integrations.langfuse.langfuse_otel_attributes import ( from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads +from litellm.secret_managers.main import str_to_bool from litellm.types.integrations.langfuse_otel import ( LangfuseSpanAttributes, ) @@ -54,6 +55,12 @@ def _preset_cache_key(kwargs: Mapping[str, object]) -> object: return litellm.cache.get_preset_cache_key_from_kwargs(**kwargs) +def _mask_flag(value: object) -> bool: + """A mask flag as the classic langfuse callback reads it: "true" / "false" strings parse, anything else by truthiness.""" + parsed: Final = str_to_bool(value) if isinstance(value, str) else None + return bool(value) if parsed is None else parsed + + def _caller_tag_list(value: object) -> tuple[str, ...]: """Caller tags as Langfuse ingestion reads a raw tags value: a list, a JSON array string, a comma list or one tag.""" parsed: Final = ( @@ -248,7 +255,7 @@ class LangfuseOtelLogger(OpenTelemetry): safe_set_attribute( span, LangfuseSpanAttributes.TRACE_INPUT.value, - REDACTED_BY_LITELLM if metadata.get("mask_input") else input_json, + REDACTED_BY_LITELLM if _mask_flag(metadata.get("mask_input")) else input_json, ) output_json: Final = LangfuseOtelLogger._observation_output(response_obj) @@ -258,7 +265,7 @@ class LangfuseOtelLogger(OpenTelemetry): safe_set_attribute( span, LangfuseSpanAttributes.TRACE_OUTPUT.value, - REDACTED_BY_LITELLM if metadata.get("mask_output") else output_json, + REDACTED_BY_LITELLM if _mask_flag(metadata.get("mask_output")) else output_json, ) @staticmethod diff --git a/tests/integration/observability/test_langfuse_otel_trace_fields.py b/tests/integration/observability/test_langfuse_otel_trace_fields.py index c4dcc55e503..365798850b2 100644 --- a/tests/integration/observability/test_langfuse_otel_trace_fields.py +++ b/tests/integration/observability/test_langfuse_otel_trace_fields.py @@ -337,6 +337,26 @@ def test_mask_flags_redact_the_derived_trace_io(v1: Rig) -> None: assert attributes.get("langfuse.trace.name") == "litellm-acompletion", attributes +@pytest.mark.parametrize( + ("headers", "masked"), + [ + pytest.param({"langfuse_mask_input": "true", "langfuse_mask_output": "true"}, True, id="true"), + pytest.param({"langfuse_mask_input": "false", "langfuse_mask_output": "false"}, False, id="false"), + ], +) +def test_mask_flag_headers_parse_as_booleans(v1: Rig, headers: Mapping[str, str], masked: bool) -> None: + marker: Final = "lit8281-mask-header-" + uuid.uuid4().hex + _chat(v1, marker, headers=headers) + attributes: Final = v1.sink.one(marker).attributes + expected: Final = ( + (REDACTED, REDACTED) + if masked + else (attributes.get("langfuse.observation.input"), attributes.get("langfuse.observation.output")) + ) + assert (attributes.get("langfuse.trace.input"), attributes.get("langfuse.trace.output")) == expected, attributes + assert marker in str(attributes.get("langfuse.observation.input")), attributes + + @pytest.mark.parametrize( "tags", [ diff --git a/tests/unit/integrations/test_langfuse_otel.py b/tests/unit/integrations/test_langfuse_otel.py index a202573a7c8..598ab8e03f1 100644 --- a/tests/unit/integrations/test_langfuse_otel.py +++ b/tests/unit/integrations/test_langfuse_otel.py @@ -1127,6 +1127,9 @@ class TestDerivedTraceFields: ({"mask_input": True}, {"langfuse.trace.input"}), ({"mask_output": True}, {"langfuse.trace.output"}), ({"mask_input": True, "mask_output": True}, {"langfuse.trace.input", "langfuse.trace.output"}), + ({"mask_input": "true", "mask_output": "TRUE"}, {"langfuse.trace.input", "langfuse.trace.output"}), + ({"mask_input": "false", "mask_output": "False"}, set()), + ({"mask_input": False, "mask_output": None}, set()), ], ) def test_mask_flags_redact_the_derived_trace_io(self, mask_flags, masked_trace_keys):