diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index a96fac32c2a..dfdd3f127e5 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -211,6 +211,17 @@ class LangfuseOtelLogger(OpenTelemetry): "arguments": arguments_obj, } output_items_data.append(langfuse_tool_call) + elif item_type == "custom_tool_call": + # Custom (freeform) tools carry a raw string `input` instead of JSON `arguments` + output_items_data.append( + { + "id": getattr(item, "id", ""), + "name": getattr(item, "name", ""), + "call_id": getattr(item, "call_id", ""), + "type": "custom_tool_call", + "input": getattr(item, "input", ""), + } + ) if output_items_data: safe_set_attribute( span, diff --git a/tests/unit/integrations/test_langfuse_otel.py b/tests/unit/integrations/test_langfuse_otel.py index 0a9ce55fe16..346cec3a806 100644 --- a/tests/unit/integrations/test_langfuse_otel.py +++ b/tests/unit/integrations/test_langfuse_otel.py @@ -1005,6 +1005,59 @@ class TestLangfuseOtelResponsesAPI: assert output_data[0]["name"] == "get_weather" assert output_data[0]["arguments"] == {} + def test_responses_api_with_custom_tool_calls(self): + """Test Langfuse OTEL logger with Responses API custom_tool_call output.""" + from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes + from litellm.types.responses.main import CustomToolCallOutputItem + + # Create Responses API response with a custom (freeform) tool call + response_obj = ResponsesAPIResponse( + id="response-790", + created_at=1625247700, + output=[ + CustomToolCallOutputItem( + id="ctc-123", + type="custom_tool_call", + name="exec", + call_id="call-def", + input="console.log('hello')", + status="completed", + ) + ], + ) + + kwargs = { + "call_type": "responses", + "messages": [{"role": "user", "content": "Print hello"}], + "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 be set" + output_data = json.loads(output_calls[0].args[2]) + assert isinstance(output_data, list) + assert len(output_data) == 1 + assert output_data[0]["type"] == "custom_tool_call" + assert output_data[0]["id"] == "ctc-123" + assert output_data[0]["name"] == "exec" + assert output_data[0]["call_id"] == "call-def" + assert output_data[0]["input"] == "console.log('hello')" + if __name__ == "__main__": pytest.main([__file__])