diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index 1eb3ce1a9c2..92bdd4f36d9 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -15,6 +15,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, @@ -93,7 +94,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 13a49f40d7e..c05fabf030a 100644 --- a/tests/unit/integrations/test_langfuse_otel.py +++ b/tests/unit/integrations/test_langfuse_otel.py @@ -123,6 +123,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()