From e1f359c62177ca27a4a749e241e8200a4ed55322 Mon Sep 17 00:00:00 2001 From: Vedant-2-6 Date: Mon, 31 Aug 2026 19:39:43 +0530 Subject: [PATCH 1/3] feat: upgrade Langfuse integration to SDK v4 and add deprecation warning Addresses #33383, as requested by the Langfuse team. Changes: 1. pyproject.toml: Bump langfuse dependency from >=2.59.7,<3.0 to >=4.7,<5 in both proxy-runtime and dev dependency groups. The v4 ingestion path (Langfuse Cloud Fast Preview) requires SDK 4.7+. 2. litellm/integrations/langfuse/langfuse.py: Add DeprecationWarning to LangFuseLogger.__init__ directing users to LangfuseOtelLogger (the langfuse_otel callback). The OTel callback already carries the required x-langfuse-ingestion-version: 4 header via _build_langfuse_otel_headers and is the recommended path for Langfuse v4 Cloud Fast Preview. Note: langfuse_otel.py already implements the v4 ingestion header correctly via LANGFUSE_INGESTION_VERSION_HEADER / LANGFUSE_INGESTION_VERSION constants and _build_langfuse_otel_headers; no changes are needed there. --- litellm/integrations/langfuse/langfuse.py | 11 +++++++++++ pyproject.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 296c2b5714e..f940af2a2ff 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -166,6 +166,17 @@ class LangFuseLogger: raise Exception( f"\033[91mLangfuse not installed, try running 'pip install langfuse' to fix this error: {e}\n{traceback.format_exc()}\033[0m" ) + import warnings + + warnings.warn( + "LangFuseLogger (the 'langfuse' callback) uses the Langfuse Python SDK " + "and is deprecated in favour of LangfuseOtelLogger (the 'langfuse_otel' " + "callback), which sends spans directly over OpenTelemetry and is compatible " + "with Langfuse v4 Cloud Fast Preview. " + "Set success_callback=['langfuse_otel'] to switch to the new integration.", + DeprecationWarning, + stacklevel=2, + ) self.public_key, self.secret_key, self.langfuse_host = resolve_langfuse_credentials( langfuse_public_key=langfuse_public_key, langfuse_secret=langfuse_secret, diff --git a/pyproject.toml b/pyproject.toml index 34c1fec1c11..19e74db45ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,7 @@ 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", + "langfuse>=4.7,<5", "opentelemetry-api==1.28.0", "opentelemetry-sdk==1.28.0", "opentelemetry-exporter-otlp==1.28.0", @@ -195,7 +195,7 @@ dev = [ "opentelemetry-sdk==1.28.0", "opentelemetry-exporter-otlp==1.28.0", "opentelemetry-instrumentation-fastapi==0.49b0", - "langfuse==2.59.7", + "langfuse>=4.7,<5", "fastapi-offline==1.7.6", "fakeredis==2.34.1", "pytest-rerunfailures==15.1", @@ -356,7 +356,7 @@ also_copy = [ # Run the test suite once before mutation to gather line coverage, then skip # mutating lines no test exercises. Those mutants would survive regardless # (no test hits the line to kill them), so generating them wastes hours of CI. -# The score now reads as "mutation score over covered code" — pair with a +# The score now reads as "mutation score over covered code" — pair with a # line-coverage number when reporting. mutate_only_covered_lines = true # Disable rerun/parallel plugins for mutation runs: From d58d1d555427380de00ebc0a61b55f23c6eeaea4 Mon Sep 17 00:00:00 2001 From: Vedant-2-6 Date: Mon, 31 Aug 2026 20:00:58 +0530 Subject: [PATCH 2/3] fix: revert langfuse dep bump and add DeprecationWarning test The langfuse>=4.7 dep bump caused an unsatisfiable dependency resolution: langfuse>=4.7 requires opentelemetry-sdk>=1.33.1, but litellm pins opentelemetry-sdk==1.28.0. The dep change is not needed because: - langfuse_otel (the recommended callback) sends spans via pure OTLP and imports zero langfuse SDK symbols - The legacy langfuse callback (being deprecated) continues to work with the existing langfuse>=2.59.7,<3.0 pin Also adds test_langfuse_legacy_callback_emits_deprecation_warning to tests/test_service_logger_otel.py to cover the new DeprecationWarning added to LangFuseLogger.__init__ in the previous commit. --- pyproject.toml | 4 +- tests/test_service_logger_otel.py | 63 +++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19e74db45ec..98d06108820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,7 @@ proxy-runtime = [ "anthropic[vertex]>=0.84.0,<1.0", "grpcio==1.78.0", "prometheus-client>=0.20.0,<1.0", - "langfuse>=4.7,<5", + "langfuse>=2.59.7,<3.0", "opentelemetry-api==1.28.0", "opentelemetry-sdk==1.28.0", "opentelemetry-exporter-otlp==1.28.0", @@ -195,7 +195,7 @@ dev = [ "opentelemetry-sdk==1.28.0", "opentelemetry-exporter-otlp==1.28.0", "opentelemetry-instrumentation-fastapi==0.49b0", - "langfuse>=4.7,<5", + "langfuse==2.59.7", "fastapi-offline==1.7.6", "fakeredis==2.34.1", "pytest-rerunfailures==15.1", diff --git a/tests/test_service_logger_otel.py b/tests/test_service_logger_otel.py index 044d37d6781..d9c43bf71c9 100644 --- a/tests/test_service_logger_otel.py +++ b/tests/test_service_logger_otel.py @@ -25,9 +25,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): @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_ignores_service_logs( - self, mock_logs, mock_metrics, mock_tracing - ): + async def test_langfuse_otel_ignores_service_logs(self, mock_logs, mock_metrics, mock_tracing): """ Test that LangfuseOtelLogger overrides the service logging hooks with 'pass'. """ @@ -46,9 +44,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): @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_does_not_create_proxy_request_span( - self, mock_logs, mock_metrics, mock_tracing - ): + async def test_langfuse_otel_does_not_create_proxy_request_span(self, mock_logs, mock_metrics, mock_tracing): """ Test that LangfuseOtelLogger returns None for create_litellm_proxy_request_started_span. @@ -73,9 +69,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_tracing") @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_metrics") @patch("litellm.integrations.opentelemetry.OpenTelemetry._init_logs") - async def test_service_logging_shadowing_fix( - self, mock_logs, mock_metrics, mock_tracing - ): + async def test_service_logging_shadowing_fix(self, mock_logs, mock_metrics, mock_tracing): """ Test the architectural fix: multiple OTEL loggers should receive logs independently. """ @@ -89,9 +83,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): service_logging = ServiceLogging() # 3. Mock the base OpenTelemetry hook - with patch.object( - OpenTelemetry, "async_service_success_hook", new_callable=AsyncMock - ) as mock_base_hook: + with patch.object(OpenTelemetry, "async_service_success_hook", new_callable=AsyncMock) as mock_base_hook: # Trigger a service event await service_logging.async_service_success_hook( service=ServiceTypes.DB, @@ -112,9 +104,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): @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 - ): + 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) @@ -147,6 +137,49 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): ) self.assertTrue(headers["Authorization"].startswith("Basic ")) + def test_langfuse_legacy_callback_emits_deprecation_warning(self): + """ + Test that instantiating LangFuseLogger emits a DeprecationWarning + directing users to switch to the langfuse_otel callback. + """ + import warnings + from unittest.mock import MagicMock, patch + + mock_langfuse_module = MagicMock() + mock_langfuse_module.Langfuse = MagicMock() + + with patch.dict( + "sys.modules", + { + "langfuse": mock_langfuse_module, + "langfuse.client": MagicMock(), + }, + ): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + from litellm.integrations.langfuse.langfuse import LangFuseLogger + + with patch( + "litellm.integrations.langfuse.langfuse.resolve_langfuse_credentials", + return_value=("pk", "sk", "https://cloud.langfuse.com"), + ): + LangFuseLogger( + langfuse_public_key="pk", + langfuse_secret="sk", + langfuse_host="https://cloud.langfuse.com", + allow_env_credentials=False, + ) + + deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)] + self.assertTrue( + len(deprecation_warnings) >= 1, + "Expected at least one DeprecationWarning to be emitted", + ) + self.assertIn( + "langfuse_otel", + str(deprecation_warnings[0].message), + ) + if __name__ == "__main__": unittest.main() From b45fa18a95297f040eab256decd6700505dc61d1 Mon Sep 17 00:00:00 2001 From: Vedant-2-6 Date: Tue, 1 Sep 2026 19:54:51 +0530 Subject: [PATCH 3/3] chore: suppress TQ008 for credential resolution patch in test --- tests/test_service_logger_otel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_service_logger_otel.py b/tests/test_service_logger_otel.py index d9c43bf71c9..5508eecfe09 100644 --- a/tests/test_service_logger_otel.py +++ b/tests/test_service_logger_otel.py @@ -159,7 +159,7 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase): warnings.simplefilter("always") from litellm.integrations.langfuse.langfuse import LangFuseLogger - with patch( + with patch( # test-quality-ok: mocking litellm credential resolver so test runs without real API keys "litellm.integrations.langfuse.langfuse.resolve_langfuse_credentials", return_value=("pk", "sk", "https://cloud.langfuse.com"), ):