diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index 2873dba93ed..ee8b8f3ba82 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -16,6 +16,7 @@ from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes impor BaseLLMObsOTELAttributes, safe_set_attribute, ) +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.types.llms.openai import HttpxBinaryResponseContent, ResponsesAPIResponse from litellm.types.utils import ( EmbeddingResponse, @@ -98,7 +99,7 @@ class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): prompt["tools"] = tools input: Final = prompt - safe_set_attribute(span, "langfuse.observation.input", json.dumps(input)) + safe_set_attribute(span, "langfuse.observation.input", safe_dumps(input)) @staticmethod @override diff --git a/tests/unit/integrations/test_langfuse_otel.py b/tests/unit/integrations/test_langfuse_otel.py index a2803719c75..a04e1c2cf37 100644 --- a/tests/unit/integrations/test_langfuse_otel.py +++ b/tests/unit/integrations/test_langfuse_otel.py @@ -139,6 +139,46 @@ class TestLangfuseOtelIntegration: "langfuse.observation.type", "generation" ) + def test_set_messages_serializes_pydantic_message_objects(self): + """list[litellm.Message] in messages must not raise and must keep role and content.""" + from litellm.integrations.langfuse.langfuse_otel_attributes import ( + LangfuseLLMObsOTELAttributes, + ) + from litellm.types.utils import Message + + mock_span: Final = MagicMock() + kwargs: Final = { + "messages": [ + Message(role="user", content="hello"), + Message(role="assistant", content="hi back"), + ] + } + + LangfuseLLMObsOTELAttributes.set_messages(mock_span, kwargs) + + key, value = mock_span.set_attribute.call_args.args + assert key == "langfuse.observation.input" + payload = json.loads(value) + assert payload["messages"][0]["role"] == "user" + assert payload["messages"][0]["content"] == "hello" + assert payload["messages"][1]["role"] == "assistant" + assert payload["messages"][1]["content"] == "hi back" + + def test_set_messages_passes_through_plain_dicts(self): + """Plain-dict messages serialize unchanged.""" + from litellm.integrations.langfuse.langfuse_otel_attributes import ( + LangfuseLLMObsOTELAttributes, + ) + + mock_span: Final = MagicMock() + kwargs: Final = {"messages": [{"role": "user", "content": "hello"}]} + + LangfuseLLMObsOTELAttributes.set_messages(mock_span, kwargs) + + key, value = mock_span.set_attribute.call_args.args + assert key == "langfuse.observation.input" + assert json.loads(value) == {"messages": [{"role": "user", "content": "hello"}]} + def test_set_langfuse_environment_attribute(self): """Test that Langfuse environment is set correctly when environment variable is present.""" mock_span = MagicMock()