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()