Skip to content

fix(langfuse_otel): serialize pydantic Message objects in set_messages#26978

Open
taxfree-python wants to merge 1 commit into
BerriAI:mainfrom
taxfree-python:fix/langfuse-otel-pydantic-message-serialization
Open

fix(langfuse_otel): serialize pydantic Message objects in set_messages#26978
taxfree-python wants to merge 1 commit into
BerriAI:mainfrom
taxfree-python:fix/langfuse-otel-pydantic-message-serialization

Conversation

@taxfree-python
Copy link
Copy Markdown

Fixes #26977. Refs #13672.

Problem

LangfuseLLMObsOTELAttributes.set_messages does json.dumps({\"messages\": kwargs.get(\"messages\"), ...}) directly. When the caller passes list[litellm.Message] — a documented public API surface (litellm.Message) — json.dumps raises TypeError: Object of type Message is not JSON serializable, the OpenInference attribute setter bails, and every LLM call spams:

LiteLLM:ERROR: _utils.py:415 - [Arize/Phoenix] Failed to set OpenInference span attributes: Object of type Message is not JSON serializable

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 GENERATION to a generic span (the OPENINFERENCE_SPAN_KIND attribute is set later in the same try/except in arize/_utils.set_attributes, so it gets skipped along with set_messages when the latter raises).

Fix

set_messages now normalises pydantic objects to dicts via .model_dump() before json.dumps. Plain-dict messages keep working unchanged — only the previously-broken pydantic path is touched.

raw_messages = kwargs.get(\"messages\") or []
messages = [
    m.model_dump() if hasattr(m, \"model_dump\") else m
    for m in raw_messages
]
prompt: Dict[str, Any] = {\"messages\": messages}

Tests

Two new tests in tests/test_litellm/integrations/test_langfuse_otel.py:

  • test_set_messages_handles_pydantic_message_objects — passing list[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.
$ pytest tests/test_litellm/integrations/test_langfuse_otel.py
22 passed

End-to-end verification

Reproduced and verified against a self-hosted Langfuse instance with bedrock/jp.anthropic.claude-haiku-4-5-20251001-v1:0:

before after
[Arize/Phoenix] Failed ... log every LLM call gone
Langfuse observation input empty full message list
Langfuse observation kind TOOL (fallback) GENERATION
Trace export succeeds succeeds

Verified by querying /api/public/traces/{id} after each call.

Out of scope

The same json.dumps(messages) pattern exists in weave/weave_otel.py, and a related-but-distinct issue (.get() against pydantic objects) exists in arize/_utils.py. Both have different fix shapes and are kept separate. Happy to file follow-ups if useful.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 1, 2026

CLA assistant check
All committers have signed the CLA.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR fixes a TypeError in LangfuseLLMObsOTELAttributes.set_messages that occurred when callers passed list[litellm.Message] (pydantic objects) instead of plain dicts, causing json.dumps to raise and silently drop the input payload and span kind from Langfuse traces. The fix normalises pydantic objects via .model_dump() before serialisation, leaving the plain-dict path unchanged, and adds two focused mocked unit tests.

Confidence Score: 4/5

Safe to merge; the fix is narrow and correct with good test coverage, only a minor style suggestion remains.

Only a P2 finding (prefer isinstance(m, BaseModel) over hasattr(m, "model_dump") given BaseModel is already imported). No logic errors, no security concerns, and the two new tests confirm both the pydantic and dict paths behave as expected.

No files require special attention.

Important Files Changed

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

Comment thread litellm/integrations/langfuse/langfuse_otel_attributes.py
@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 1, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing taxfree-python:fix/langfuse-otel-pydantic-message-serialization (51d940b) with main (934ecdc)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@taxfree-python taxfree-python force-pushed the fix/langfuse-otel-pydantic-message-serialization branch from 10fd95f to 692c239 Compare May 2, 2026 04:41
`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.
@taxfree-python taxfree-python force-pushed the fix/langfuse-otel-pydantic-message-serialization branch from 692c239 to 51d940b Compare May 2, 2026 05:12
@taxfree-python
Copy link
Copy Markdown
Author

CI status update:

  • lint — fixed: applied black==24.10.0 to langfuse_otel_attributes.py (formatting-only change, list comprehension folded onto one line). 22/22 unit tests still pass.
  • ⚠️ Verify PR source branch — failing structurally, not contributor-fixable. The workflow rejects all external-fork PRs to main and tells contributors to "open PRs against the litellm_oss_branch branch instead", but that branch does not exist in the repo. Retargeting to litellm_oss_staging doesn't bypass the check either (see fix(gemini): avoid duplicate model route for full api_base #26982 for an independent confirmation). Same situation noted on fix(gemini): avoid duplicate model route for full api_base #26980.
  • ⚠️ verify (lazy-openapi-snapshot) — workflow itself flags this as Not blocking — the snapshot will regenerate at release if not committed.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants