diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index 7de42c00ede..a93c45ef840 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -90,7 +90,6 @@ class LangfuseOtelLogger(OpenTelemetry): "generation_name": LangfuseSpanAttributes.GENERATION_NAME, "generation_id": LangfuseSpanAttributes.GENERATION_ID, "parent_observation_id": LangfuseSpanAttributes.PARENT_OBSERVATION_ID, - "version": LangfuseSpanAttributes.GENERATION_VERSION, "mask_input": LangfuseSpanAttributes.MASK_INPUT, "mask_output": LangfuseSpanAttributes.MASK_OUTPUT, "trace_user_id": LangfuseSpanAttributes.TRACE_USER_ID, @@ -99,13 +98,18 @@ class LangfuseOtelLogger(OpenTelemetry): "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, + "trace_release": LangfuseSpanAttributes.RELEASE, "existing_trace_id": LangfuseSpanAttributes.EXISTING_TRACE_ID, "update_trace_keys": LangfuseSpanAttributes.UPDATE_TRACE_KEYS, "debug_langfuse": LangfuseSpanAttributes.DEBUG_LANGFUSE, } + version: Final = ( + metadata.get("trace_version") if metadata.get("trace_version") is not None else metadata.get("version") + ) + if version is not None: + safe_set_attribute(span, LangfuseSpanAttributes.VERSION.value, version) + for key, enum_attr in mapping.items(): if key in metadata and metadata[key] is not None: value = metadata[key] diff --git a/litellm/integrations/otel/presets/langfuse.py b/litellm/integrations/otel/presets/langfuse.py index 5104ee2ff55..c2f64422eff 100644 --- a/litellm/integrations/otel/presets/langfuse.py +++ b/litellm/integrations/otel/presets/langfuse.py @@ -42,9 +42,7 @@ def langfuse_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, public_key: Final = params.get("langfuse_public_key") secret_key: Final = 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 _V1Langfuse._build_langfuse_otel_headers( + _V1Langfuse._get_langfuse_authorization_header(public_key=public_key, secret_key=secret_key) + ) return {} diff --git a/litellm/types/integrations/langfuse_otel.py b/litellm/types/integrations/langfuse_otel.py index 9ef48bdcdd0..c58dc567cda 100644 --- a/litellm/types/integrations/langfuse_otel.py +++ b/litellm/types/integrations/langfuse_otel.py @@ -16,12 +16,13 @@ class LangfuseOtelConfig(BaseModel): class LangfuseSpanAttributes(str, Enum): LANGFUSE_ENVIRONMENT = "langfuse.environment" + VERSION = "langfuse.version" + RELEASE = "langfuse.release" # ---- Generation-level metadata ---- GENERATION_NAME = "langfuse.generation.name" GENERATION_ID = "langfuse.generation.id" PARENT_OBSERVATION_ID = "langfuse.generation.parent_observation_id" - GENERATION_VERSION = "langfuse.generation.version" MASK_INPUT = "langfuse.generation.mask_input" MASK_OUTPUT = "langfuse.generation.mask_output" @@ -36,8 +37,6 @@ 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" EXISTING_TRACE_ID = "langfuse.trace.existing_id" UPDATE_TRACE_KEYS = "langfuse.trace.update_keys" diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py index f7c0b5452fe..e44c56e1fdf 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py @@ -1,5 +1,6 @@ """Per-request multi-tenant credential routing (V1 parity).""" +import base64 import os import sys @@ -42,6 +43,17 @@ def test_langfuse_dynamic_headers_need_both_keys(): assert headers is not None and "Authorization" in headers +def test_langfuse_dynamic_headers_carry_v4_ingestion_version(): + headers = dynamic_otlp_headers( + "langfuse_otel", {"langfuse_public_key": "pk", "langfuse_secret_key": "sk"} + ) + expected_auth = "Basic " + base64.b64encode(b"pk:sk").decode() + assert headers == { + "Authorization": expected_auth, + "x-langfuse-ingestion-version": "4", + } + + def test_weave_dynamic_headers(): headers = dynamic_otlp_headers( "weave_otel", {"wandb_api_key": "w", "weave_project_id": "p"} diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index 28f138c7acd..9392f974570 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -211,7 +211,7 @@ class TestLangfuseOtelIntegration: LangfuseSpanAttributes.GENERATION_NAME.value: "gen-name", LangfuseSpanAttributes.GENERATION_ID.value: "gen-id", LangfuseSpanAttributes.PARENT_OBSERVATION_ID.value: "parent-id", - LangfuseSpanAttributes.GENERATION_VERSION.value: "v1", + LangfuseSpanAttributes.VERSION.value: "t-ver", LangfuseSpanAttributes.MASK_INPUT.value: True, LangfuseSpanAttributes.MASK_OUTPUT.value: False, LangfuseSpanAttributes.TRACE_USER_ID.value: "user-123", @@ -221,8 +221,7 @@ class TestLangfuseOtelIntegration: LangfuseSpanAttributes.TRACE_NAME.value: "trace-name", LangfuseSpanAttributes.TRACE_ID.value: "traceid", # stripped dashes LangfuseSpanAttributes.TRACE_METADATA.value: json.dumps({"k": "v"}), - LangfuseSpanAttributes.TRACE_VERSION.value: "t-ver", - LangfuseSpanAttributes.TRACE_RELEASE.value: "rel-1", + LangfuseSpanAttributes.RELEASE.value: "rel-1", LangfuseSpanAttributes.EXISTING_TRACE_ID.value: "existing-id", LangfuseSpanAttributes.UPDATE_TRACE_KEYS.value: json.dumps( ["key1", "key2"] @@ -240,6 +239,52 @@ class TestLangfuseOtelIntegration: actual == expected ), "Mismatch between expected and actual OTEL attribute mapping." + @pytest.mark.parametrize( + "metadata, expected_version", + [ + ( + {"version": "v-observation", "trace_version": "v-trace"}, + "v-trace", + ), + ({"trace_version": "v-trace"}, "v-trace"), + ({"version": "v-observation"}, "v-observation"), + ({"version": "v-observation", "trace_version": ""}, ""), + ({}, None), + ], + ids=[ + "trace-version-wins-as-documented", + "trace-only", + "observation-version-is-the-fallback", + "empty-trace-version-is-not-absent", + "neither-key-emits-nothing", + ], + ) + def test_version_emitted_on_langfuse_v4_key(self, metadata, expected_version): + kwargs = {"litellm_params": {"metadata": {"trace_release": "rel-9", **metadata}}} + + with patch( + "litellm.integrations.arize._utils.safe_set_attribute" + ) as mock_safe_set_attribute: + LangfuseOtelLogger._set_langfuse_specific_attributes( + MagicMock(), kwargs, None + ) + + emitted = { + call.args[1]: call.args[2] for call in mock_safe_set_attribute.call_args_list + } + + if expected_version is None: + assert "langfuse.version" not in emitted + else: + assert emitted["langfuse.version"] == expected_version + assert emitted["langfuse.release"] == "rel-9" + for retired_key in ( + "langfuse.generation.version", + "langfuse.trace.version", + "langfuse.trace.release", + ): + assert retired_key not in emitted + def test_set_langfuse_specific_attributes_with_content(self): """Test that _set_langfuse_specific_attributes correctly sets observation.output with regular content response.""" from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes