Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docker/build_from_pip/Dockerfile.build_from_pip
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ RUN uv venv --python python && \
"anthropic[vertex]==0.84.0" \
"grpcio==1.78.0" \
"prometheus-client==0.20.0" \
"langfuse==2.59.7" \
"opentelemetry-api==1.28.0" \
"opentelemetry-sdk==1.28.0" \
"opentelemetry-exporter-otlp==1.28.0" \
"langfuse==4.7.0" \
"opentelemetry-api==1.33.1" \
"opentelemetry-sdk==1.33.1" \
"opentelemetry-exporter-otlp==1.33.1" \
"ddtrace==2.19.0" \
"sentry-sdk==2.21.0" \
"mangum==0.17.0" \
Expand Down
267 changes: 127 additions & 140 deletions litellm/integrations/langfuse/langfuse.py

Large diffs are not rendered by default.

60 changes: 53 additions & 7 deletions litellm/integrations/langfuse/langfuse_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@

LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
LANGFUSE_CLOUD_US_ENDPOINT = "https://us.cloud.langfuse.com/api/public/otel"
LANGFUSE_OTEL_INGESTION_VERSION_HEADER = "x-langfuse-ingestion-version"
LANGFUSE_OTEL_INGESTION_VERSION = "4"


def build_langfuse_otel_headers(
public_key: str,
secret_key: str,
) -> dict[str, str]:
auth_string = f"{public_key}:{secret_key}"
auth_header = base64.b64encode(auth_string.encode()).decode()
return {
"Authorization": f"Basic {auth_header}",
LANGFUSE_OTEL_INGESTION_VERSION_HEADER: LANGFUSE_OTEL_INGESTION_VERSION,
}


class LangfuseOtelLogger(OpenTelemetry):
Expand Down Expand Up @@ -92,19 +106,35 @@ def _set_metadata_attributes(span: Span, metadata: dict):
"version": LangfuseSpanAttributes.GENERATION_VERSION,
"mask_input": LangfuseSpanAttributes.MASK_INPUT,
"mask_output": LangfuseSpanAttributes.MASK_OUTPUT,
"user_id": LangfuseSpanAttributes.TRACE_USER_ID,
"trace_user_id": LangfuseSpanAttributes.TRACE_USER_ID,
"session_id": LangfuseSpanAttributes.SESSION_ID,
"tags": LangfuseSpanAttributes.TAGS,
"trace_name": LangfuseSpanAttributes.TRACE_NAME,
"trace_id": LangfuseSpanAttributes.TRACE_ID,
"trace_metadata": LangfuseSpanAttributes.TRACE_METADATA,
"trace_version": LangfuseSpanAttributes.TRACE_VERSION,
"trace_release": LangfuseSpanAttributes.TRACE_RELEASE,
"existing_trace_id": LangfuseSpanAttributes.EXISTING_TRACE_ID,
"update_trace_keys": LangfuseSpanAttributes.UPDATE_TRACE_KEYS,
"debug_langfuse": LangfuseSpanAttributes.DEBUG_LANGFUSE,
}

trace_metadata = metadata.get("trace_metadata")
if isinstance(trace_metadata, dict):
for key, value in trace_metadata.items():
serialized_value = json.dumps(value) if isinstance(value, (list, dict)) else value
safe_set_attribute(
span,
f"{LangfuseSpanAttributes.TRACE_METADATA.value}.{key}",
serialized_value,
)
elif trace_metadata is not None:
safe_set_attribute(
span,
LangfuseSpanAttributes.TRACE_METADATA.value,
trace_metadata,
)

for key, enum_attr in mapping.items():
if key in metadata and metadata[key] is not None:
value = metadata[key]
Expand Down Expand Up @@ -284,7 +314,9 @@ def _create_open_telemetry_config_from_langfuse_env(self) -> OpenTelemetryConfig
auth_header = LangfuseOtelLogger._get_langfuse_authorization_header(
public_key=public_key, secret_key=secret_key
)
otlp_auth_headers = f"Authorization={auth_header}"
otlp_auth_headers = LangfuseOtelLogger._format_otel_headers(
LangfuseOtelLogger._build_langfuse_otel_headers(auth_header)
)

return OpenTelemetryConfig(
exporter="otlp_http",
Expand Down Expand Up @@ -333,7 +365,9 @@ def get_langfuse_otel_config() -> "OpenTelemetryConfig":
auth_header = LangfuseOtelLogger._get_langfuse_authorization_header(
public_key=public_key, secret_key=secret_key
)
otlp_auth_headers = f"Authorization={auth_header}"
otlp_auth_headers = LangfuseOtelLogger._format_otel_headers(
LangfuseOtelLogger._build_langfuse_otel_headers(auth_header)
)

# Prevent modification of global env vars which causes leakage
# os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
Expand All @@ -350,9 +384,21 @@ def _get_langfuse_authorization_header(public_key: str, secret_key: str) -> str:
"""
Get the authorization header for Langfuse OpenTelemetry.
"""
auth_string = f"{public_key}:{secret_key}"
auth_header = base64.b64encode(auth_string.encode()).decode()
return f"Basic {auth_header}"
return build_langfuse_otel_headers(
public_key=public_key,
secret_key=secret_key,
)["Authorization"]

@staticmethod
def _build_langfuse_otel_headers(auth_header: str) -> dict[str, str]:
return {
"Authorization": auth_header,
LANGFUSE_OTEL_INGESTION_VERSION_HEADER: LANGFUSE_OTEL_INGESTION_VERSION,
}

@staticmethod
def _format_otel_headers(headers: dict[str, str]) -> str:
return ",".join(f"{key}={value}" for key, value in headers.items())

def construct_dynamic_otel_headers(
self, standard_callback_dynamic_params: StandardCallbackDynamicParams
Expand All @@ -374,7 +420,7 @@ def construct_dynamic_otel_headers(
public_key=dynamic_langfuse_public_key,
secret_key=dynamic_langfuse_secret_key,
)
dynamic_headers["Authorization"] = auth_header
dynamic_headers = LangfuseOtelLogger._build_langfuse_otel_headers(auth_header)

return dynamic_headers

Expand Down
27 changes: 10 additions & 17 deletions litellm/integrations/langfuse/langfuse_prompt_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import os
from functools import lru_cache
from importlib.metadata import version as package_version
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast

from packaging.version import Version
from typing_extensions import TypeAlias

from litellm.integrations.custom_logger import CustomLogger
Expand All @@ -25,7 +25,7 @@

if TYPE_CHECKING:
from langfuse import Langfuse
from langfuse.client import ChatPromptClient, TextPromptClient
from langfuse.model import ChatPromptClient, TextPromptClient

from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj

Expand Down Expand Up @@ -64,7 +64,6 @@ def langfuse_client_init(
Exception: If langfuse package is not installed
"""
try:
import langfuse
from langfuse import Langfuse
except Exception as e:
raise Exception(
Expand Down Expand Up @@ -95,20 +94,16 @@ def langfuse_client_init(
"flush_interval": LangFuseLogger._get_langfuse_flush_interval(flush_interval), # flush interval in seconds
}

if Version(langfuse.version.__version__) >= Version("2.6.0"):
parameters["sdk_integration"] = "litellm"
import httpx

if Version(langfuse.version.__version__) >= Version("2.7.3"):
import httpx
import litellm

import litellm
from ...llms.custom_httpx.http_handler import get_ssl_configuration

from ...llms.custom_httpx.http_handler import get_ssl_configuration

parameters["httpx_client"] = httpx.Client(
verify=get_ssl_configuration(),
cert=os.getenv("SSL_CERTIFICATE", litellm.ssl_certificate),
)
parameters["httpx_client"] = httpx.Client(
verify=get_ssl_configuration(),
cert=os.getenv("SSL_CERTIFICATE", litellm.ssl_certificate),
)

client = Langfuse(**parameters)

Expand All @@ -123,9 +118,7 @@ def __init__(
langfuse_host=None,
flush_interval=1,
):
import langfuse

self.langfuse_sdk_version = langfuse.version.__version__
self.langfuse_sdk_version = package_version("langfuse")
self.Langfuse = langfuse_client_init(
langfuse_public_key=langfuse_public_key,
langfuse_secret=langfuse_secret,
Expand Down
12 changes: 7 additions & 5 deletions litellm/integrations/otel/presets/langfuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from litellm.integrations.langfuse.langfuse_otel import (
LangfuseOtelLogger as _V1Langfuse,
)
from litellm.integrations.langfuse.langfuse_otel import (
build_langfuse_otel_headers,
)
from litellm.integrations.otel.model.config import (
ExporterOwner,
ExporterSpec,
Expand Down Expand Up @@ -40,9 +43,8 @@ def langfuse_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str,
public_key = params.get("langfuse_public_key")
secret_key = params.get("langfuse_secret_key")
if public_key and secret_key:
return {
"Authorization": _V1Langfuse._get_langfuse_authorization_header(
public_key=public_key, secret_key=secret_key
)
}
return build_langfuse_otel_headers(
public_key=public_key,
secret_key=secret_key,
)
return {}
6 changes: 3 additions & 3 deletions litellm/types/integrations/langfuse_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class LangfuseSpanAttributes(str, Enum):
GENERATION_NAME = "langfuse.generation.name"
GENERATION_ID = "langfuse.generation.id"
PARENT_OBSERVATION_ID = "langfuse.generation.parent_observation_id"
GENERATION_VERSION = "langfuse.generation.version"
GENERATION_VERSION = "langfuse.version"
MASK_INPUT = "langfuse.generation.mask_input"
MASK_OUTPUT = "langfuse.generation.mask_output"

Expand All @@ -36,8 +36,8 @@ class LangfuseSpanAttributes(str, Enum):
TRACE_NAME = "langfuse.trace.name"
TRACE_ID = "langfuse.trace.id"
TRACE_METADATA = "langfuse.trace.metadata"
TRACE_VERSION = "langfuse.trace.version"
TRACE_RELEASE = "langfuse.trace.release"
TRACE_VERSION = "langfuse.version"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Version attributes collide on OTel

Medium Severity

The GENERATION_VERSION and TRACE_VERSION attributes now map to the same langfuse.version key. This causes _set_metadata_attributes to overwrite one value when both are present, preventing distinct generation and trace versions from being recorded for Langfuse OTel spans. The SDK callback path still treats these fields as distinct.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aaeb263. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2c1dfdb4f0e. Langfuse v4 uses the single langfuse.version semantic attribute; observation version now takes precedence over propagated trace_version, matching SDK behavior, with trace version used as the fallback

TRACE_RELEASE = "langfuse.release"
EXISTING_TRACE_ID = "langfuse.trace.existing_id"
UPDATE_TRACE_KEYS = "langfuse.trace.update_keys"

Expand Down
30 changes: 15 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ proxy-runtime = [
"anthropic[vertex]>=0.84.0,<1.0",
"grpcio==1.78.0",
"prometheus-client>=0.20.0,<1.0",
"langfuse>=2.59.7,<3.0",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"langfuse>=4.7,<5.0",

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.

P2 Hard breaking change for users on langfuse < 4.7

The minimum is raised from >=2.59.7,<3.0 to >=4.7,<5.0, skipping v3 entirely (which was never supported) and jumping two major versions. Any deployment that has langfuse pinned to v2.x or v3.x will encounter an import-time AttributeError or resolution conflict when they next update LiteLLM. Per the team's policy on backwards-incompatible changes, a feature flag or explicit opt-in mechanism should gate the v4 behaviour, giving users a migration window.

Rule Used: What: avoid backwards-incompatible changes without... (source)

"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"ddtrace>=2.19.0,<3.0",
"sentry-sdk>=2.21.0,<3.0",
"mangum>=0.17.0,<1.0",
Expand Down Expand Up @@ -169,11 +169,11 @@ dev = [
"types-PyYAML==6.0.12.20250915",
"botocore-stubs==1.43.14",
"types-boto3[bedrock,bedrock-agent,bedrock-runtime,kms,s3,sagemaker-runtime,sts]==1.43.30",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"langfuse==2.59.7",
"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"langfuse==4.7.0",
"fastapi-offline==1.7.6",
"fakeredis==2.34.1",
"pytest-rerunfailures==15.1",
Expand All @@ -192,10 +192,10 @@ proxy-dev = [
"prisma==0.11.0",
"hypercorn==0.17.3",
"prometheus-client==0.20.0",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"azure-identity==1.25.2",
"a2a-sdk==1.1.0",
]
Expand All @@ -215,7 +215,7 @@ ci = [
"lunary==1.4.36; python_version == '3.10'",
"lunary==1.4.37; python_version >= '3.11'",
"logfire==4.6.0",
"traceloop-sdk==0.33.12",
"traceloop-sdk==0.34.0",
"detect-secrets==1.5.0",
"PyGithub==2.8.1",
"aiodynamo==24.7",
Expand Down
Loading
Loading