Skip to content

fix(otel): map Langfuse trace user, session, name, and tags in the v2 mapper - #35210

Closed
devin-ai-integration[bot] wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_otel_v2_langfuse_trace_controls
Closed

devin-ai-integration[bot] wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_otel_v2_langfuse_trace_controls

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • OTel v2 langfuse mapper dropped trace user, session, name, tags
  • Switching from langfuse_otel to the v2 mapper lost trace attribution

How it solves it:

  • Parse the caller's trace controls into a typed RequestAnnotations
  • Langfuse mapper stamps user.id, session.id, trace name and tags
  • Caller metadata reaches langfuse.trace.metadata.* only via an operator allowlist

Relevant issues

Fixes #35192

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Live proxy, real Anthropic call, console span exporter, capture_message_content: no_content, so no prompt or response content is captured

model_list:
  - model_name: test-model
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
callback_settings:
  otel:
    exporter: console
    mapper_names: [langfuse]
    capture_message_content: no_content
    langfuse_trace_metadata_keys: [environment]
litellm_settings:
  callbacks: [otel]
LITELLM_OTEL_V2=true LITELLM_MASTER_KEY=sk-1234 python litellm/proxy/proxy_cli.py --config otel_langfuse_v2.yaml --port 4000 2>&1 | tee litellm.log

curl -sS http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' \
  -H 'Content-Type: application/json' -d '{
  "model": "test-model",
  "messages": [{"role": "user", "content": "hello"}],
  "metadata": {"trace_user_id": "user-1", "session_id": "session-1",
               "trace_name": "chat-request", "tags": ["test"],
               "environment": "staging", "internal_note": "not-allowlisted"}}'

Before, at 4d54324515 and with langfuse_trace_metadata_keys dropped from the config since the field does not exist yet, the exported span carried the generation but none of the trace controls:

"langfuse.observation.type": "generation",
"langfuse.observation.model.name": "test-model",
"langfuse.observation.metadata.provider": "anthropic",
"langfuse.observation.id": "0f874f0a-fce2-48fc-b00b-508c118c7f02",
"langfuse.observation.usage_details": "{\"input\": 8, \"output\": 16, \"total\": 24}",
"langfuse.observation.cost_details": "{\"total\": 8.800000000000001e-05}",

After, at 60194646fc:

"langfuse.observation.type": "generation",
"langfuse.observation.model.name": "test-model",
"langfuse.observation.metadata.provider": "anthropic",
"langfuse.observation.id": "6d8fb066-21fc-4531-b988-aaed40ab009d",
"user.id": "user-1",
"session.id": "session-1",
"langfuse.trace.name": "chat-request",
"langfuse.trace.tags": ["test", "User-Agent: curl", "User-Agent: curl/7.81.0"],
"langfuse.observation.usage_details": "{\"input\": 8, \"output\": 16, \"total\": 24}",
"langfuse.observation.cost_details": "{\"total\": 8.800000000000001e-05}",
"langfuse.trace.metadata.environment": "staging",

internal_note is absent because it is not in langfuse_trace_metadata_keys, while environment is

Type

🐛 Bug Fix

Changes

RequestAnnotations (new, in model/metadata.py) is the typed view of the caller-supplied labels of a request, sitting next to the proxy-authoritative RequestIdentity. On the proxy, StandardLoggingMetadata drops every key it does not declare, so the named controls only survive in the verbatim metadata.requester_metadata snapshot; tags instead come from the payload's already-merged request_tags

@dataclass(frozen=True)
class RequestAnnotations:
    session_id: str | None = None
    trace_name: str | None = None
    user_id: str | None = None                    # metadata.trace_user_id
    tags: tuple[str, ...] = ()                    # payload.request_tags
    requester_metadata: Mapping[str, str] = ...   # scalars only, carried raw

It hangs off RequestContext and LLMCallSpanData, so it is parsed once per request and needs no content capture

The Langfuse mapper gains four extractor-table entries:

"user.id":              lambda d: d.annotations.user_id or d.identity.end_user or None,
"session.id":           lambda d: d.annotations.session_id or None,
"langfuse.trace.name":  lambda d: d.annotations.trace_name or None,
"langfuse.trace.tags":  lambda d: list(d.annotations.tags) or None,

These are trace-level keys carried on the generation observation, which is how Langfuse derives a trace from an OTLP span; absent controls stay absent rather than being emitted empty

langfuse.trace.metadata.<key> is emitted only for keys in the new langfuse_trace_metadata_keys config field (env var LITELLM_OTEL_LANGFUSE_TRACE_METADATA_KEYS), empty by default, so a request can never push arbitrary metadata into the backend. That matches the shape of the existing Baggage allowlists. To wire it through, resolve_mappers(names, config) now hands each mapper factory the config, and behaviour is unchanged when it is omitted

One deliberate difference from langfuse_otel: tags come from request_tags, the payload's single merged source of request, key/team and header tags, so user-agent tags show up in langfuse.trace.tags too

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/42496f6660644a69859036d4300764e7

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment on lines +153 to +159
metadata = payload.get("metadata")
requester = metadata.get("requester_metadata") if metadata else None
requester_meta: Mapping[str, object] = requester if isinstance(requester, Mapping) else {}
return cls(
session_id=as_str(requester_meta.get("session_id")),
trace_name=as_str(requester_meta.get("trace_name")),
user_id=as_str(requester_meta.get("trace_user_id")),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 SDK trace controls are dropped

When an SDK call supplies trace_user_id, session_id, or trace_name directly in its metadata, this parser reads only the proxy-created requester_metadata snapshot, causing the Langfuse span to omit user.id, session.id, and langfuse.trace.name.

Knowledge Base Used: Logging & Observability Integrations

Comment on lines 104 to +107
return {
**collect(cls._LLM_CALL_ATTRS, data),
**collect(cls._BLOB_ATTRS, data),
f"{TRACE_METADATA_PREFIX}{key}": value
for key, value in data.annotations.requester_metadata.items()
if key in self._trace_metadata_keys

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Caller metadata overrides team identity

When team_id or team_alias is allowlisted, this final metadata merge replaces the corresponding proxy-authoritative attribute with the caller's requester_metadata value, causing the Langfuse trace to be attributed to the wrong team.

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds typed request annotations and config-aware Langfuse mapping for trace user, session, name, tags, and allowlisted metadata.

  • Carries parsed request annotations through RequestContext and LLMCallSpanData.
  • Adds a Langfuse trace-metadata allowlist and passes OTel configuration into mapper factories.
  • Extends mapper and source-of-truth tests for the new attributes and defaults.

Confidence Score: 2/5

The PR should not merge until trace controls work on non-proxy logging paths and caller metadata cannot replace authoritative team attribution.

The new parser depends on a proxy-created metadata snapshot, while the mapper’s final metadata merge permits configured requester keys to overwrite authenticated team fields.

Files Needing Attention: litellm/integrations/otel/model/metadata.py, litellm/integrations/otel/mappers/langfuse.py, litellm/integrations/otel/README.md

Important Files Changed

Filename Overview
litellm/integrations/otel/model/metadata.py Adds typed annotations, but the proxy-specific requester_metadata source drops trace controls on current non-proxy logging paths.
litellm/integrations/otel/mappers/langfuse.py Adds the requested Langfuse attributes, but allowlisted caller metadata can overwrite authoritative team attribution.
litellm/integrations/otel/model/config.py Adds and normalizes the Langfuse metadata allowlist and threads it into mapper construction.
litellm/integrations/otel/model/payloads.py Propagates RequestAnnotations from RequestContext into LLMCallSpanData.
litellm/integrations/otel/mappers/init.py Makes mapper factories config-aware while preserving default construction behavior.
tests/test_litellm/integrations/otel/test_otel_v2_vendor_mappers.py Covers ordinary trace controls and allowlisting but misses SDK metadata sourcing and reserved-key collisions.
litellm/integrations/otel/README.md Documents the new configuration in-tree despite the repository requirement to keep customer documentation in litellm-docs.

Comments Outside Diff (1)

  1. litellm/integrations/otel/README.md, line 165-169 (link)

    P2 Product documentation is misplaced

    The new customer-facing environment variable and YAML configuration guidance belongs in the litellm-docs repository; keeping it here splits product documentation across repositories and bypasses the established publishing workflow.

    Rule Used: Prevent documentation from being added - needs to ... (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "chore(otel): drop added inline comments" | Re-trigger Greptile

@codecov

codecov Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_otel_v2_langfuse_trace_controls (49112b2) with litellm_internal_staging (4d54324)

Open in CodSpeed

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.

[Bug]: OTel v2 Langfuse mapper omits trace user, session, name, and tags

3 participants