Skip to content

fix(otel v2): name Langfuse traces from the langfuse_trace_name header or metadata.trace_name - #40793

Merged
yucheng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_otel_v2_langfuse_trace_name
Sep 12, 2026
Merged

yucheng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_otel_v2_langfuse_trace_name

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • OTel v2 Langfuse traces ignore the langfuse_trace_name header
  • They also ignore body metadata.trace_name
  • Traces show up in Langfuse named POST /v1/chat/completions
  • Legacy v1 honored both, header first, so v2 regressed

How it solves it:

  • Read the header, then metadata.trace_name (litellm_metadata on /v1/messages)
  • Emit langfuse.trace.name on the generation span
  • Stamp the same name on the request root span while it is recording
  • Langfuse gets a named root observation, so the trace name sticks

User Flow

Before: a developer tags requests with langfuse_trace_name so their Langfuse project groups traces by workflow, but every trace is named after the URL

  1. They send POST https://litellm-domain/v1/chat/completions with header langfuse_trace_name: nightly-eval and a normal chat body
  2. They get a 200 and an x-litellm-call-id header back
  3. They open Langfuse, Tracing, Traces, and the new trace is named POST /v1/chat/completions
  4. They retry with "metadata": {"trace_name": "nightly-eval"} in the body instead of the header, and the trace is still named POST /v1/chat/completions

After: the same requests produce traces named the way the developer asked

  1. They send POST https://litellm-domain/v1/chat/completions with header langfuse_trace_name: nightly-eval and a normal chat body
  2. They get a 200 and an x-litellm-call-id header back
  3. They open Langfuse, Tracing, Traces, and the new trace is named nightly-eval
  4. They retry with "metadata": {"trace_name": "nightly-eval"} in the body instead of the header, and the trace is named nightly-eval too
  5. If both are sent with different values, the header wins, same as v1
  6. A request with neither keeps the existing POST /v1/chat/completions name

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • 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 (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup for both legs: real proxy on :17700 with a real Postgres database, real Anthropic calls, LITELLM_OTEL_V2=true, LANGFUSE_HOST pointing at a local Langfuse v3 on :3100 with real project keys, --num_workers 4 on both legs (4 Started server process lines in each proxy log). Config:

model_list:
  - model_name: haiku
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY

litellm_settings:
  callbacks: ["langfuse_otel"]

general_settings:
  master_key: sk-i2tn-master
  database_url: os.environ/I2TN_DATABASE_URL

Requests ($T is a per-run tag, r4base for Before and r4fixed for After):

P=http://localhost:17700; K="Authorization: Bearer sk-i2tn-master"; CT="Content-Type: application/json"
BODY='{"model":"haiku","messages":[{"role":"user","content":"reply with one word: ok"}],"max_tokens":5}'
curl -s -D - -o /dev/null $P/v1/chat/completions -H "$K" -H "$CT" -H "langfuse_trace_name: $T-hdr-only" -d "$BODY"
curl -s -D - -o /dev/null $P/v1/chat/completions -H "$K" -H "$CT" -d "${BODY%\}},\"metadata\":{\"trace_name\":\"$T-body-only\"}}"
curl -s -D - -o /dev/null $P/v1/chat/completions -H "$K" -H "$CT" -H "langfuse_trace_name: $T-hdr-wins" -d "${BODY%\}},\"metadata\":{\"trace_name\":\"$T-body-loses\"}}"
curl -s -D - -o /dev/null $P/v1/chat/completions -H "$K" -H "$CT" -d "$BODY"
curl -s -D - -o /dev/null $P/v1/chat/completions -H "$K" -H "$CT" -H "langfuse_trace_name: $T-stream-hdr" -d "${BODY%\}},\"stream\":true}"
curl -s -D - -o /dev/null $P/v1/messages -H "$K" -H "$CT" -H "langfuse_trace_name: $T-msgs-hdr" -d "$BODY"
curl -s -D - -o /dev/null $P/v1/messages -H "$K" -H "$CT" -d "${BODY%\}},\"litellm_metadata\":{\"trace_name\":\"$T-msgs-body\"}}"
RESP='{"model":"haiku","input":"reply with one word: ok","max_output_tokens":16}'
curl -s -D - -o /dev/null $P/v1/responses -H "$K" -H "$CT" -H "langfuse_trace_name: $T-resp-hdr" -d "$RESP"
curl -s -D - -o /dev/null $P/v1/responses -H "$K" -H "$CT" -d "${RESP%\}},\"metadata\":{\"trace_name\":\"$T-resp-body\"}}"

Readback, per x-litellm-call-id printed by the requests above, straight from the Langfuse public API:

LF=http://localhost:3100/api/public
tid=$(curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" "$LF/observations?type=GENERATION&limit=100" \
  | jq -r --arg c "$cid" '.data[] | select(.metadata.attributes["langfuse.observation.id"]==$c) | .traceId' | head -1)
curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" "$LF/traces/$tid" | jq -r --arg c "$cid" '"call_id=\($c) trace=\(.id) name=\(.name)"'

Before (09b6948)

/v1/chat/completions, header only

  1. curl ... -H "langfuse_trace_name: r4base-hdr-only" -> HTTP/1.1 200 OK, x-litellm-call-id: fbb61a69-84bd-44d6-b6de-d051f5a517e2
  2. Readback -> call_id=fbb61a69-84bd-44d6-b6de-d051f5a517e2 trace=59a57d3d0e7ee520d7ce9fd8af6a135c name=POST /v1/chat/completions

/v1/chat/completions, body metadata only

  1. curl ... -d '{..., "metadata":{"trace_name":"r4base-body-only"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: 86f55c72-e7d6-4938-a269-3f442fbe0f95
  2. Readback -> call_id=86f55c72-e7d6-4938-a269-3f442fbe0f95 trace=996080ce82da050cbaa3457234b0587c name=POST /v1/chat/completions

/v1/chat/completions, header and body disagree

  1. curl ... -H "langfuse_trace_name: r4base-hdr-wins" -d '{..., "metadata":{"trace_name":"r4base-body-loses"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: a72fcb18-025b-444d-aadb-79eb4f77d4f0
  2. Readback -> call_id=a72fcb18-025b-444d-aadb-79eb4f77d4f0 trace=445b928ab83e7084e37ad93050de99bf name=POST /v1/chat/completions

/v1/chat/completions, neither

  1. curl ... -> HTTP/1.1 200 OK, x-litellm-call-id: 330b1150-6351-4cf3-bc51-7643af3aba7e
  2. Readback -> call_id=330b1150-6351-4cf3-bc51-7643af3aba7e trace=9f91f85018bdb81fabc9169345a38546 name=POST /v1/chat/completions

/v1/chat/completions streaming, header

  1. curl ... -H "langfuse_trace_name: r4base-stream-hdr" -d '{..., "stream":true}' -> HTTP/1.1 200 OK, x-litellm-call-id: ed9b634c-fb76-4406-9de9-c2ef8e87891f
  2. Readback -> call_id=ed9b634c-fb76-4406-9de9-c2ef8e87891f trace=f15b0014cf59aa543726b2e5b31c48ae name=POST /v1/chat/completions

/v1/messages, header

  1. curl ... /v1/messages -H "langfuse_trace_name: r4base-msgs-hdr" -> HTTP/1.1 200 OK, x-litellm-call-id: 62235fec-6fce-49fe-915b-2115a4925209
  2. Readback -> call_id=62235fec-6fce-49fe-915b-2115a4925209 trace=1deb9f023550099e258e05c870c5e2e8 name=POST /v1/messages

/v1/messages, litellm_metadata body

  1. curl ... /v1/messages -d '{..., "litellm_metadata":{"trace_name":"r4base-msgs-body"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: e1e2ae63-6e0e-4694-878d-138dc3209fe4
  2. Readback -> call_id=e1e2ae63-6e0e-4694-878d-138dc3209fe4 trace=a2ffb12437cfc730ba472801dd8cf8f5 name=POST /v1/messages

/v1/responses, header

  1. curl ... /v1/responses -H "langfuse_trace_name: r4base-resp-hdr" -> HTTP/1.1 200 OK, x-litellm-call-id: 9f5cc8a1-6e6d-400a-aaa1-49245f45b36c
  2. Readback -> call_id=9f5cc8a1-6e6d-400a-aaa1-49245f45b36c trace=3bd3075ea63c1bc72947bdbf88066f4a name=POST /v1/responses

/v1/responses, body metadata

  1. curl ... /v1/responses -d '{..., "metadata":{"trace_name":"r4base-resp-body"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: 66ac02a1-d8ea-4811-a50e-fc9baa0b651f
  2. Readback -> call_id=66ac02a1-d8ea-4811-a50e-fc9baa0b651f trace=6c9be0ca54888a5e6d7ddefde0e59f97 name=POST /v1/responses

Langfuse UI, trace list and the header-and-body-disagree trace

  1. Open http://localhost:3100, org lpr-org, project team-a, Tracing, type r4base in the IDs / Names search -> No results., none of the 9 requests produced a trace named after the caller
  2. Clear the search and type POST /v1/chat/completions -> the 22:35:04 to 22:35:07 rows from this run are all named POST /v1/chat/completions

Before: Langfuse trace list shows only endpoint-derived names

  1. Open the trace for the header-and-body-disagree request, http://localhost:3100/project/cmtt6ri2y0006qh07dhgqspom/traces/445b928ab83e7084e37ad93050de99bf -> header reads POST /v1/chat/completions: 445b928ab83e7084e37ad93050de99bf, neither r4base-hdr-wins nor r4base-body-loses appears anywhere on the page

Before: Langfuse trace detail named POST /v1/chat/completions

After (0b85ead)

The same 9 requests against the PR tip merged into the current litellm_internal_staging head 22c60ef9e7 (merge commit 4f883a7a65, 4 workers, real Anthropic and Langfuse) stored the same names, tagged m4-*

/v1/chat/completions, header only

  1. curl ... -H "langfuse_trace_name: r4fixed-hdr-only" -> HTTP/1.1 200 OK, x-litellm-call-id: 0d56b55b-1c93-4b30-add9-8339fc2e9eaf
  2. Readback -> call_id=0d56b55b-1c93-4b30-add9-8339fc2e9eaf trace=4366d979b22020e523d23e7522be32b0 name=r4fixed-hdr-only

/v1/chat/completions, body metadata only

  1. curl ... -d '{..., "metadata":{"trace_name":"r4fixed-body-only"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: 446d53b8-0cbb-4049-9b9e-2ea749c5be77
  2. Readback -> call_id=446d53b8-0cbb-4049-9b9e-2ea749c5be77 trace=372eaa18f06befae2d28591f0c4697b6 name=r4fixed-body-only

/v1/chat/completions, header and body disagree

  1. curl ... -H "langfuse_trace_name: r4fixed-hdr-wins" -d '{..., "metadata":{"trace_name":"r4fixed-body-loses"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: 24170f99-064e-434a-a31c-6c7f237aa84a
  2. Readback -> call_id=24170f99-064e-434a-a31c-6c7f237aa84a trace=38ae44246cc8cda528fb5f0e25ca348f name=r4fixed-hdr-wins

/v1/chat/completions, neither

  1. curl ... -> HTTP/1.1 200 OK, x-litellm-call-id: 6507ae87-11ce-4e6f-944b-32c0f9d99c93
  2. Readback -> call_id=6507ae87-11ce-4e6f-944b-32c0f9d99c93 trace=c02c6a732e58b08d44c38f3533cd24d7 name=POST /v1/chat/completions (unchanged fallback)

/v1/chat/completions streaming, header

  1. curl ... -H "langfuse_trace_name: r4fixed-stream-hdr" -d '{..., "stream":true}' -> HTTP/1.1 200 OK, x-litellm-call-id: f739dce4-3633-4f99-9247-c5c616ffa2f8
  2. Readback -> call_id=f739dce4-3633-4f99-9247-c5c616ffa2f8 trace=f3ebd5276e68cae3aaf5f7b0142387d7 name=r4fixed-stream-hdr

/v1/messages, header

  1. curl ... /v1/messages -H "langfuse_trace_name: r4fixed-msgs-hdr" -> HTTP/1.1 200 OK, x-litellm-call-id: 9910a21d-5ace-4e95-aef0-c8424618a248
  2. Readback -> call_id=9910a21d-5ace-4e95-aef0-c8424618a248 trace=6b7e3af42e249d3557116e0ee3f7a308 name=r4fixed-msgs-hdr

/v1/messages, litellm_metadata body

  1. curl ... /v1/messages -d '{..., "litellm_metadata":{"trace_name":"r4fixed-msgs-body"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: 6b8f7b5b-1210-41db-b1c8-cc16de032e1e
  2. Readback -> call_id=6b8f7b5b-1210-41db-b1c8-cc16de032e1e trace=bc4924115dd66bd935f866b503c9ed89 name=r4fixed-msgs-body

/v1/responses, header

  1. curl ... /v1/responses -H "langfuse_trace_name: r4fixed-resp-hdr" -> HTTP/1.1 200 OK, x-litellm-call-id: cf60eae9-953e-4dbd-97ab-2cedc185edfa
  2. Readback -> call_id=cf60eae9-953e-4dbd-97ab-2cedc185edfa trace=2b4de76249e939f966985cd4d380da9a name=r4fixed-resp-hdr

/v1/responses, body metadata

  1. curl ... /v1/responses -d '{..., "metadata":{"trace_name":"r4fixed-resp-body"}}' -> HTTP/1.1 200 OK, x-litellm-call-id: fc9d0031-5f24-4fc7-b377-ece3bdd6e18b
  2. Readback -> call_id=fc9d0031-5f24-4fc7-b377-ece3bdd6e18b trace=3a5e5477c165fe8e4da41ae5c0942a85 name=r4fixed-resp-body

Langfuse UI, trace list and the header-and-body-disagree trace

  1. Open http://localhost:3100, org lpr-org, project team-a, Tracing, type r4fixed in the IDs / Names search -> 8 rows, one per named request: r4fixed-hdr-only, r4fixed-body-only, r4fixed-hdr-wins, r4fixed-stream-hdr, r4fixed-msgs-hdr, r4fixed-msgs-body, r4fixed-resp-hdr, r4fixed-resp-body (the unnamed request stays POST /v1/chat/completions, 22:33:58 in the previous search)

After: Langfuse trace list shows the caller-supplied names

  1. Open the trace for the header-and-body-disagree request, http://localhost:3100/project/cmtt6ri2y0006qh07dhgqspom/traces/38ae44246cc8cda528fb5f0e25ca348f -> header reads r4fixed-hdr-wins: 38ae44246cc8cda528fb5f0e25ca348f, the header won over metadata.trace_name, and the child spans (POST /v1/chat/completions, auth, chat haiku, batch_write_to_db) are unchanged

After: Langfuse trace detail named r4fixed-hdr-wins

Type

🐛 Bug Fix

Caveats (if any)

Low

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/1d3bdadbd958454796014f11b7033854
Open in Devin Desktop: https://app.devin.ai/desktop/session/1d3bdadbd958454796014f11b7033854?variant=devin
Requested by: @yucheng-berri


Note

Low Risk
Telemetry-only labeling for Langfuse OTel v2; no auth, billing, or request-path logic changes beyond reading optional header/metadata.

Overview
OTel v2 Langfuse traces now use a caller-provided name instead of defaulting to the HTTP route (e.g. POST /v1/chat/completions), restoring behavior that v1 already had.

Resolution order is langfuse_trace_name header first, then metadata.trace_name or litellm_metadata.trace_name (for /v1/messages). The name is emitted as langfuse.trace.name on the LLM generation span and stamped on the proxy root span during log_pre_api_call while it is still recording.

The Langfuse logger is split: LangfuseOpenTelemetryV2 handles trace naming for any Langfuse mapper config; LangfuseContentOpenTelemetryV2 keeps root input/output stamping behind capture_span_content. The factory now selects the Langfuse base logger whenever the Langfuse mapper is enabled, not only when content capture is on.

Reviewed by Cursor Bugbot for commit 0b85ead. Bugbot is set up for automated code reviews on this repo. Configure here.

ran /live-pr-risk and found no regressions/backward incompatible risks

…r or metadata.trace_name

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@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

@codspeed

codspeed Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_otel_v2_langfuse_trace_name (0b85ead) with litellm_internal_staging (f90b5ca)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (22c60ef) during the generation of this report, so f90b5ca was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@greptile-apps

greptile-apps Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR restores caller-defined Langfuse trace names in OTel v2 while preserving existing fallback behavior.

  • Resolves trace names from langfuse_trace_name, then body metadata.
  • Applies the selected name to the request root span and generation span.
  • Enables trace naming when span-content capture is disabled.
  • Adds coverage for precedence, missing names, /v1/messages metadata, and both content-capture modes.
  • The latest revision fully types the test helper in accordance with repository conventions.

Confidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness or repository-rule violations.

No new actionable issues were found. The root-context concern was resolved after the implementation clarified that context is propagated into synchronous provider execution, and the untyped test-helper thread was manually resolved after the latest revision added explicit parameter, return, and local-variable types.

Important Files Changed

Filename Overview
litellm/integrations/otel/langfuse_logger.py Adds Langfuse root-span trace naming and separates trace-only behavior from content stamping.
litellm/integrations/otel/logger.py Propagates the resolved trace name into span data and selects the appropriate Langfuse logger regardless of content capture.
litellm/integrations/otel/mappers/langfuse.py Maps the caller-selected name to the Langfuse trace-name attribute.
litellm/integrations/otel/model/metadata.py Resolves trace names with header-first precedence and carries them through the call event.
litellm/integrations/otel/model/payloads.py Adds the optional trace name to generation-span mapping data.
tests/test_litellm/integrations/otel/test_langfuse_logger.py Tests root and generation naming across precedence and content-capture configurations; the helper is now fully typed.
tests/test_litellm/integrations/otel/test_otel_v2_sources_of_truth.py Tests trace-name extraction, precedence, fallback, and payload propagation.
tests/test_litellm/integrations/otel/test_otel_v2_vendor_mappers.py Verifies Langfuse emits the trace-name attribute only when a name is present.

Reviews (2): Last reviewed commit: "test(otel v2): type the named-request he..." | Re-trigger Greptile

Comment thread litellm/integrations/otel/langfuse_logger.py
Comment thread tests/test_litellm/integrations/otel/test_langfuse_logger.py Outdated
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0b85ead. Configure here.

@yucheng-berri
yucheng-berri merged commit bf146e2 into litellm_internal_staging Sep 12, 2026
132 of 133 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_otel_v2_langfuse_trace_name branch September 12, 2026 00:37
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