From 316ab4938ba2466f9b865cf4b01b8e379e70e45b Mon Sep 17 00:00:00 2001 From: Asaf Vertman Date: Thu, 16 Apr 2026 11:55:56 +0300 Subject: [PATCH] fix(langfuse_otel): clean up review feedback --- .../integrations/langfuse/langfuse_otel.py | 9 ---- .../langfuse/langfuse_otel_attributes.py | 4 +- .../integrations/test_langfuse_otel.py | 49 ++++++++++++++++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index eff237914f8..53351e985f8 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -8,7 +8,6 @@ from litellm._logging import verbose_logger from litellm.integrations.arize import _utils from litellm.integrations.langfuse.langfuse_otel_attributes import ( LangfuseLLMObsOTELAttributes, - get_langfuse_observation_input_by_type, ) from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig from litellm.types.integrations.langfuse_otel import ( @@ -244,14 +243,6 @@ class LangfuseOtelLogger(OpenTelemetry): metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs) LangfuseOtelLogger._set_metadata_attributes(span=span, metadata=metadata) - input_payload = get_langfuse_observation_input_by_type(kwargs) - if input_payload is not None: - safe_set_attribute( - span, - LangfuseSpanAttributes.OBSERVATION_INPUT.value, - safe_dumps(input_payload), - ) - LangfuseOtelLogger._set_observation_output(span=span, response_obj=response_obj) @staticmethod diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index 15c8525f75e..9449e1079c3 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -128,7 +128,9 @@ def get_langfuse_observation_input_by_type( return prompt prompt: dict[str, Any] = {} - if "messages" in kwargs: + # Preserve explicit empty chat message lists without serializing `null` + # when callers pass `messages=None`. + if messages is not None: prompt["messages"] = messages for key in ("functions", "tools"): value = optional_params.get(key) diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index 8c487085c4d..daf0a44e9fe 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -257,21 +257,14 @@ class TestLangfuseOtelIntegration: ], ) - kwargs = { - "messages": [{"role": "user", "content": "What's the weather in Tokyo?"}], - } - with patch( "litellm.integrations.arize._utils.safe_set_attribute" ) as mock_safe_set_attribute: LangfuseOtelLogger._set_langfuse_specific_attributes( - MagicMock(), kwargs, response_obj + MagicMock(), {}, response_obj ) expect_output = { - LangfuseSpanAttributes.OBSERVATION_INPUT.value: [ - {"role": "user", "content": "What's the weather in Tokyo?"} - ], LangfuseSpanAttributes.OBSERVATION_OUTPUT.value: { "role": "assistant", "content": "The weather in Tokyo is sunny.", @@ -572,6 +565,46 @@ class TestLangfuseOtelResponsesAPI: in actual_attributes["langfuse.observation.input"] ) + def test_langfuse_observation_input_skips_null_messages(self): + """Explicit null messages should not serialize as `{\"messages\": null}`.""" + kwargs = { + "messages": None, + "optional_params": { + "tools": [{"type": "function", "name": "list_applications"}] + }, + } + + from litellm.integrations.langfuse.langfuse_otel_attributes import ( + get_langfuse_observation_input_by_type, + ) + + assert get_langfuse_observation_input_by_type(kwargs) == { + "tools": [{"type": "function", "name": "list_applications"}] + } + + def test_langfuse_specific_attributes_do_not_rewrite_observation_input(self): + """Observation input should be owned by set_messages, not rewritten later.""" + from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes + + kwargs = { + "input": [{"role": "user", "content": "hello"}], + "optional_params": {"instructions": "Reply briefly."}, + "litellm_params": {"metadata": {"generation_name": "gen-name"}}, + } + + with patch( + "litellm.integrations.arize._utils.safe_set_attribute" + ) as mock_safe_set_attribute: + LangfuseOtelLogger._set_langfuse_specific_attributes( + MagicMock(), kwargs, None + ) + + actual_keys = [ + call.args[1] for call in mock_safe_set_attribute.call_args_list + ] + assert LangfuseSpanAttributes.GENERATION_NAME.value in actual_keys + assert LangfuseSpanAttributes.OBSERVATION_INPUT.value not in actual_keys + def test_responses_api_metadata_extraction(self): """Test that metadata is correctly extracted from ResponsesAPI kwargs.""" # Clean up any existing module mocks