From ea89aca5393f49f1f5c4b4da02e57d593b04bfda Mon Sep 17 00:00:00 2001 From: Paco Cartones Date: Fri, 28 Aug 2026 17:22:34 +0000 Subject: [PATCH 1/2] test(langfuse): cover empty, whitespace, and truncated tool-call arguments Parametrized regression for the OTel Responses-API path: empty, whitespace-only, and truncated-JSON function-call arguments must degrade to {} without dropping observation.output. Complements the existing redacted-sentinel case with the other invalid-input shapes. --- .../integrations/test_langfuse_otel.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index 0a9ce55fe16..8c7f44b6270 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -1005,6 +1005,58 @@ class TestLangfuseOtelResponsesAPI: assert output_data[0]["name"] == "get_weather" assert output_data[0]["arguments"] == {} + @pytest.mark.parametrize( + "arguments", + ["", " ", '{"location":'], + ids=["empty", "whitespace", "partial-json"], + ) + def test_responses_api_function_call_with_invalid_arguments(self, arguments: str): + """Empty, whitespace-only, and truncated JSON tool-call arguments must degrade to {} + without dropping observation.output, alongside the redacted-sentinel case above.""" + from openai.types.responses import ResponseFunctionToolCall + + from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes + + response_obj = ResponsesAPIResponse( + id="response-invalid-arguments", + created_at=1625247700, + output=[ + ResponseFunctionToolCall( + id="fc-invalid", + type="function_call", + name="get_weather", + call_id="call-invalid", + arguments=arguments, + status="completed", + ) + ], + ) + + kwargs = { + "call_type": "responses", + "messages": [{"role": "user", "content": "What's the weather?"}], + "model": "gpt-4o", + "optional_params": {}, + } + + mock_span = MagicMock() + + with patch( + "litellm.integrations.arize._utils.safe_set_attribute" + ) as mock_safe_set_attribute: + LangfuseOtelLogger._set_langfuse_specific_attributes(mock_span, kwargs, response_obj) + + output_calls = [ + call + for call in mock_safe_set_attribute.call_args_list + if call.args[1] == LangfuseSpanAttributes.OBSERVATION_OUTPUT.value + ] + + assert len(output_calls) > 0, "observation.output should still be set" + output_data = json.loads(output_calls[0].args[2]) + assert output_data[0]["name"] == "get_weather" + assert output_data[0]["arguments"] == {} + if __name__ == "__main__": pytest.main([__file__]) From 3d303a537b4c64416cc337c8567f18c1fb42c4e1 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:46:52 +0000 Subject: [PATCH 2/2] test(langfuse): mark the span-attribute patch as an intentional observable seam Mirror the sibling test's test-quality-ok suppression on the safe_set_attribute patch so the TQ budget gate passes; the seam is the same one every sibling test in this class stubs. --- tests/test_litellm/integrations/test_langfuse_otel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index 8c7f44b6270..bdf6fb33d10 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -1041,7 +1041,7 @@ class TestLangfuseOtelResponsesAPI: mock_span = MagicMock() - with patch( + with patch( # test-quality-ok: the span attribute sink is the observable boundary; sibling tests in this class stub the same seam "litellm.integrations.arize._utils.safe_set_attribute" ) as mock_safe_set_attribute: LangfuseOtelLogger._set_langfuse_specific_attributes(mock_span, kwargs, response_obj)