fix(langfuse): add deterministic otel trace-id normalization helper

Add normalize_trace_id_for_otel: a valid 32-hex id (case-insensitive) or a
dashed UUID is reused directly; any other string is hashed via
sha256(id).digest()[:16].hex(), identical to langfuse.create_trace_id(seed),
so callers can reproduce the trace id from an external id. Not wired into span
creation yet; covered by unit tests.
This commit is contained in:
Dhanush Reddy Chilakala 2026-06-26 00:39:09 -05:00
parent 868c0bbfac
commit 246dfd1872
No known key found for this signature in database
GPG key ID: 51C52D74B4BAB2DC
2 changed files with 76 additions and 0 deletions

View file

@ -1,7 +1,9 @@
import base64
import json # <--- NEW
import os
import re
from datetime import datetime
from hashlib import sha256
from typing import TYPE_CHECKING, Any, Optional, Union
from litellm._logging import verbose_logger
@ -26,6 +28,23 @@ else:
LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
LANGFUSE_CLOUD_US_ENDPOINT = "https://us.cloud.langfuse.com/api/public/otel"
_OTEL_TRACE_ID_HEX = re.compile(r"[0-9a-f]{32}")
def normalize_trace_id_for_otel(trace_id: str) -> str:
"""Map an arbitrary trace id onto a valid 32-hex-char OTEL/Langfuse trace id.
Langfuse derives a trace's identity from the OTEL span-context trace id, which
must be 32 lowercase hex chars. An id that is already valid (case-insensitively,
or a dashed UUID) is reused directly so an explicit valid id is preserved; any
other string is hashed exactly as ``langfuse.create_trace_id(seed=...)`` does,
so a caller can reproduce the trace id from their external id.
"""
canonical = trace_id.replace("-", "").lower()
if _OTEL_TRACE_ID_HEX.fullmatch(canonical):
return canonical
return sha256(trace_id.encode("utf-8")).digest()[:16].hex()
class LangfuseOtelLogger(OpenTelemetry):
def __init__(self, config=None, *args, **kwargs):

View file

@ -550,6 +550,63 @@ class TestLangfuseOtelIntegration:
# Endpoint assertion removed as side effect is gone
class TestNormalizeTraceIdForOtel:
"""Unit tests for the trace-id normalization helper."""
def test_valid_lowercase_hex_passthrough(self):
from litellm.integrations.langfuse.langfuse_otel import (
normalize_trace_id_for_otel,
)
tid = "abcdef0123456789abcdef0123456789"
assert normalize_trace_id_for_otel(tid) == tid
def test_uppercase_hex_is_lowercased(self):
from litellm.integrations.langfuse.langfuse_otel import (
normalize_trace_id_for_otel,
)
assert (
normalize_trace_id_for_otel("ABCDEF0123456789ABCDEF0123456789")
== "abcdef0123456789abcdef0123456789"
)
def test_dashed_uuid_is_stripped_not_hashed(self):
from litellm.integrations.langfuse.langfuse_otel import (
normalize_trace_id_for_otel,
)
assert (
normalize_trace_id_for_otel("123e4567-e89b-12d3-a456-426614174000")
== "123e4567e89b12d3a456426614174000"
)
def test_external_id_hashed_like_langfuse_create_trace_id(self):
"""Non-hex ids hash the ORIGINAL string (dashes and case preserved) so
the result is byte-for-byte ``langfuse.create_trace_id(seed=...)``."""
import hashlib
from litellm.integrations.langfuse.langfuse_otel import (
normalize_trace_id_for_otel,
)
for seed in ("global_20260225143025_ord2468", "Order-42", "ab-cd"):
assert (
normalize_trace_id_for_otel(seed)
== hashlib.sha256(seed.encode("utf-8")).digest()[:16].hex()
)
def test_output_is_always_32_lowercase_hex(self):
import re
from litellm.integrations.langfuse.langfuse_otel import (
normalize_trace_id_for_otel,
)
for seed in ("x", "global_seed", "ABCDEF0123456789ABCDEF0123456789", "1-2-3"):
assert re.fullmatch(r"[0-9a-f]{32}", normalize_trace_id_for_otel(seed))
class TestLangfuseOtelResponsesAPI:
"""Test suite for Langfuse OTEL integration with ResponsesAPI"""