fix(langfuse_otel): serialize pydantic Message objects in set_messages#26978
fix(langfuse_otel): serialize pydantic Message objects in set_messages#26978taxfree-python wants to merge 1 commit into
Conversation
Greptile SummaryThis PR fixes a Confidence Score: 4/5Safe to merge; the fix is narrow and correct with good test coverage, only a minor style suggestion remains. Only a P2 finding (prefer No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/integrations/langfuse/langfuse_otel_attributes.py | Adds pydantic-to-dict normalization in set_messages before json.dumps; fix is correct and targeted, minor duck-typing concern with hasattr vs isinstance(m, BaseModel). |
| tests/test_litellm/integrations/test_langfuse_otel.py | Two new mocked unit tests covering pydantic and plain-dict message paths; no real network calls, well-structured, and fit with the existing test class. |
Reviews (1): Last reviewed commit: "fix(langfuse_otel): serialize pydantic M..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
10fd95f to
692c239
Compare
`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 BerriAI#13672.
692c239 to
51d940b
Compare
|
CI status update:
Happy to adjust further if maintainers can either (a) point me at the correct base branch, or (b) push the branch internally to bypass the fork guard. |
Fixes #26977. Refs #13672.
Problem
LangfuseLLMObsOTELAttributes.set_messagesdoesjson.dumps({\"messages\": kwargs.get(\"messages\"), ...})directly. When the caller passeslist[litellm.Message]— a documented public API surface (litellm.Message) —json.dumpsraisesTypeError: Object of type Message is not JSON serializable, the OpenInference attribute setter bails, and every LLM call spams:Beyond the noise, the failure has real telemetry impact: the trace is still exported but with the input payload missing, and the span's kind is degraded from
GENERATIONto a generic span (theOPENINFERENCE_SPAN_KINDattribute is set later in the sametry/exceptinarize/_utils.set_attributes, so it gets skipped along withset_messageswhen the latter raises).Fix
set_messagesnow normalises pydantic objects to dicts via.model_dump()beforejson.dumps. Plain-dict messages keep working unchanged — only the previously-broken pydantic path is touched.Tests
Two new tests in
tests/test_litellm/integrations/test_langfuse_otel.py:test_set_messages_handles_pydantic_message_objects— passinglist[litellm.Message]no longer raises and the dumped JSON contains the expected role/content fields.test_set_messages_passes_through_plain_dicts— the existing dict path still works.End-to-end verification
Reproduced and verified against a self-hosted Langfuse instance with
bedrock/jp.anthropic.claude-haiku-4-5-20251001-v1:0:[Arize/Phoenix] Failed ...loginputTOOL(fallback)GENERATIONVerified by querying
/api/public/traces/{id}after each call.Out of scope
The same
json.dumps(messages)pattern exists inweave/weave_otel.py, and a related-but-distinct issue (.get()against pydantic objects) exists inarize/_utils.py. Both have different fix shapes and are kept separate. Happy to file follow-ups if useful.