From f5dc4407538e87b6e91a125092f6fdeee90e2344 Mon Sep 17 00:00:00 2001 From: weiguangli-io Date: Sat, 2 May 2026 20:29:43 +0800 Subject: [PATCH] fix(langfuse_otel): use safe_dumps in set_messages to handle pydantic Message objects Calling json.dumps() directly on the messages list failed with 'Object of type Message is not JSON serializable' when users passed litellm.Message (a pydantic BaseModel) objects instead of plain dicts. Replace json.dumps() with safe_dumps() which recursively calls model_dump() on any pydantic BaseModel instances before serializing. Fixes #26977 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../langfuse/langfuse_otel_attributes.py | 3 +- .../langfuse/test_langfuse_otel_attributes.py | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/integrations/langfuse/test_langfuse_otel_attributes.py diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index fb4a0a6a36c..89171b5cb40 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -14,6 +14,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, @@ -96,7 +97,7 @@ class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): prompt["tools"] = tools input = 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/test_litellm/integrations/langfuse/test_langfuse_otel_attributes.py b/tests/test_litellm/integrations/langfuse/test_langfuse_otel_attributes.py new file mode 100644 index 00000000000..79bdea366cf --- /dev/null +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_otel_attributes.py @@ -0,0 +1,98 @@ +""" +Tests for LangfuseLLMObsOTELAttributes, specifically verifying that pydantic +Message objects in the messages list are correctly serialized without raising +TypeError. Regression test for: + https://github.com/BerriAI/litellm/issues/26977 +""" + +import json +import os +import sys +from unittest.mock import MagicMock + +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.integrations.langfuse.langfuse_otel_attributes import ( + LangfuseLLMObsOTELAttributes, +) +from litellm.types.utils import Message + + +def test_set_messages_with_pydantic_message_objects(): + """ + Regression test for issue #26977: + set_messages should not raise 'Object of type Message is not JSON serializable' + when messages is a list of litellm.Message (pydantic) objects. + """ + span = MagicMock() + kwargs = { + "messages": [Message(role="user", content="hello")], + "optional_params": {}, + } + + # Should not raise TypeError + LangfuseLLMObsOTELAttributes.set_messages(span, kwargs) + + # Verify that set_attribute was called with a valid JSON string + span.set_attribute.assert_called_once() + key, value = span.set_attribute.call_args[0] + assert key == "langfuse.observation.input" + + # The value must be parseable JSON + parsed = json.loads(value) + assert "messages" in parsed + assert parsed["messages"][0]["role"] == "user" + assert parsed["messages"][0]["content"] == "hello" + + +def test_set_messages_with_dict_messages(): + """ + Ensure that plain dict messages still work correctly after the fix. + """ + span = MagicMock() + kwargs = { + "messages": [{"role": "user", "content": "hello"}], + "optional_params": {}, + } + + LangfuseLLMObsOTELAttributes.set_messages(span, kwargs) + + span.set_attribute.assert_called_once() + key, value = span.set_attribute.call_args[0] + assert key == "langfuse.observation.input" + + parsed = json.loads(value) + assert parsed["messages"][0]["role"] == "user" + + +def test_set_messages_with_tools(): + """ + Ensure tools are included in the serialized input and pydantic messages + are handled correctly together. + """ + span = MagicMock() + kwargs = { + "messages": [Message(role="user", content="what's the weather?")], + "optional_params": { + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "parameters": {"type": "object"}, + }, + } + ] + }, + } + + LangfuseLLMObsOTELAttributes.set_messages(span, kwargs) + + span.set_attribute.assert_called_once() + key, value = span.set_attribute.call_args[0] + parsed = json.loads(value) + assert "tools" in parsed + assert parsed["tools"][0]["function"]["name"] == "get_weather" + assert parsed["messages"][0]["role"] == "user"