From 51d940b497bc71e6c339701d4404dca7a13b3d88 Mon Sep 17 00:00:00 2001 From: taxfree-python Date: Fri, 1 May 2026 20:14:22 +0900 Subject: [PATCH] fix(langfuse_otel): serialize pydantic Message objects in set_messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LangfuseLLMObsOTELAttributes.set_messages` does `json.dumps({"messages": kwargs.get("messages"), ...})` directly. When the caller passes `list[litellm.Message]` — a documented public API exposed via the top-level `litellm.Message` export — `json.dumps` raises `TypeError: Object of type Message is not JSON serializable`, the entire OpenInference attribute setter bails out, and every LLM call logs: [Arize/Phoenix] Failed to set OpenInference span attributes: Object of type Message is not JSON serializable The span still gets exported but with the input payload missing and its kind degraded from `GENERATION` to a generic span (the kind attribute is set later in the same try/except, so it gets skipped on failure too). Fix: convert pydantic models to dicts via `.model_dump()` before `json.dumps`. Plain-dict messages keep working unchanged. Same pattern (`json.dumps(messages)`) exists in `weave/weave_otel.py` and a related-but-distinct issue (using `.get()` against pydantic objects) exists in `arize/_utils.py`; those are out of scope here. Refs #13672. --- .../langfuse/langfuse_otel_attributes.py | 13 +++- .../integrations/test_langfuse_otel.py | 60 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index fb4a0a6a36c..3bb6acd0948 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -86,7 +86,18 @@ class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): @staticmethod @override def set_messages(span: "Span", kwargs: Dict[str, Any]): - prompt = {"messages": kwargs.get("messages")} + # Normalize litellm.Message (pydantic) -> dict so json.dumps works. + # Callers can pass list[litellm.Message] via the documented public + # export; without this, json.dumps raises TypeError and the whole + # OpenInference attribute setter bails — surfacing as the spammy + # "Failed to set OpenInference span attributes: Object of type + # Message is not JSON serializable" error and dropping the input + # payload from the span. + raw_messages = kwargs.get("messages") or [] + messages = [ + m.model_dump() if isinstance(m, BaseModel) else m for m in raw_messages + ] + prompt: Dict[str, Any] = {"messages": messages} optional_params = kwargs.get("optional_params", {}) functions = optional_params.get("functions") tools = optional_params.get("tools") diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index 44853d9dce5..9d256e6ccf8 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -115,6 +115,66 @@ class TestLangfuseOtelIntegration: mock_span, mock_kwargs, mock_response, LangfuseLLMObsOTELAttributes ) + def test_set_messages_handles_pydantic_message_objects(self): + """Pydantic ``litellm.Message`` objects in ``messages`` must not raise. + + Regression test for the spammy "Object of type Message is not JSON + serializable" error that previously fired on every LLM call when the + caller passed ``list[litellm.Message]`` (a documented public API) + rather than ``list[dict]``. + """ + from litellm.integrations.langfuse.langfuse_otel_attributes import ( + LangfuseLLMObsOTELAttributes, + ) + from litellm.types.utils import Message + + captured: dict = {} + + def _capture(span, key, value): + captured[key] = value + + kwargs = { + "messages": [ + Message(role="user", content="hello"), + Message(role="assistant", content="hi back"), + ] + } + + with patch( + "litellm.integrations.langfuse.langfuse_otel_attributes.safe_set_attribute", + side_effect=_capture, + ): + # Must not raise — previously raised TypeError inside json.dumps. + LangfuseLLMObsOTELAttributes.set_messages(MagicMock(), kwargs) + + payload = json.loads(captured["langfuse.observation.input"]) + 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 (the existing path) must keep working unchanged.""" + from litellm.integrations.langfuse.langfuse_otel_attributes import ( + LangfuseLLMObsOTELAttributes, + ) + + captured: dict = {} + + def _capture(span, key, value): + captured[key] = value + + kwargs = {"messages": [{"role": "user", "content": "hello"}]} + + with patch( + "litellm.integrations.langfuse.langfuse_otel_attributes.safe_set_attribute", + side_effect=_capture, + ): + LangfuseLLMObsOTELAttributes.set_messages(MagicMock(), kwargs) + + payload = json.loads(captured["langfuse.observation.input"]) + assert payload["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()