mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(langfuse_otel): serialize pydantic Message objects in set_messages
`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.
This commit is contained in:
parent
934ecdca78
commit
51d940b497
2 changed files with 72 additions and 1 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue