From ef27e618adef480104e08c83ce75593f4e10b6f3 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:38:46 +0200 Subject: [PATCH] fix(langfuse): send v4 ingestion header for otel callback --- .../integrations/langfuse/langfuse_otel.py | 25 +++++++++-- tests/test_service_logger_otel.py | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index b96ec72b04e..276fd24cc32 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -25,6 +25,8 @@ else: 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" class LangfuseOtelLogger(OpenTelemetry): @@ -295,7 +297,9 @@ class LangfuseOtelLogger(OpenTelemetry): 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", @@ -344,7 +348,9 @@ class LangfuseOtelLogger(OpenTelemetry): 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 @@ -365,6 +371,17 @@ class LangfuseOtelLogger(OpenTelemetry): auth_header = base64.b64encode(auth_string.encode()).decode() return f"Basic {auth_header}" + @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 ) -> Optional[dict]: @@ -389,7 +406,9 @@ class LangfuseOtelLogger(OpenTelemetry): 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 diff --git a/tests/test_service_logger_otel.py b/tests/test_service_logger_otel.py index 35070d55546..01e918d3f42 100644 --- a/tests/test_service_logger_otel.py +++ b/tests/test_service_logger_otel.py @@ -8,10 +8,15 @@ from unittest.mock import patch, AsyncMock, MagicMock sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) import litellm -from litellm.integrations.langfuse.langfuse_otel import LangfuseOtelLogger +from litellm.integrations.langfuse.langfuse_otel import ( + LANGFUSE_OTEL_INGESTION_VERSION, + LANGFUSE_OTEL_INGESTION_VERSION_HEADER, + LangfuseOtelLogger, +) from litellm.integrations.opentelemetry import OpenTelemetry from litellm.types.services import ServiceTypes from litellm._service_logger import ServiceLogging +from litellm.types.utils import StandardCallbackDynamicParams class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): @@ -108,6 +113,44 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): "Generic OTEL logger should have received the log exactly once.", ) + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_tracing") + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_metrics") + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_logs") + async def test_langfuse_otel_env_config_includes_v4_ingestion_header( + self, mock_logs, mock_metrics, mock_tracing + ): + logger = LangfuseOtelLogger() + + headers = OpenTelemetry._get_headers_dictionary(logger.config.headers) + + self.assertEqual( + headers[LANGFUSE_OTEL_INGESTION_VERSION_HEADER], + LANGFUSE_OTEL_INGESTION_VERSION, + ) + self.assertTrue(headers["Authorization"].startswith("Basic ")) + + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_tracing") + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_metrics") + @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_logs") + async def test_langfuse_otel_dynamic_headers_include_v4_ingestion_header( + self, mock_logs, mock_metrics, mock_tracing + ): + logger = LangfuseOtelLogger() + + headers = logger.construct_dynamic_otel_headers( + StandardCallbackDynamicParams( + langfuse_public_key="pk-lf-dynamic", + langfuse_secret_key="sk-lf-dynamic", + ) + ) + + self.assertIsNotNone(headers) + self.assertEqual( + headers[LANGFUSE_OTEL_INGESTION_VERSION_HEADER], + LANGFUSE_OTEL_INGESTION_VERSION, + ) + self.assertTrue(headers["Authorization"].startswith("Basic ")) + if __name__ == "__main__": unittest.main()