From dddc1e0ad8e67c7c78ce383ca91b8c0f5381f916 Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 15 Sep 2026 00:15:48 +0000 Subject: [PATCH] fix(langfuse): honor ssl_verify=False and SSL_VERIFY on the v4 OTLP exporter Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/integrations/langfuse/langfuse_sdk.py | 17 +++++---- .../langfuse/test_langfuse_sdk.py | 35 ++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py index 5dc079d2f63..984b41bc14d 100644 --- a/litellm/integrations/langfuse/langfuse_sdk.py +++ b/litellm/integrations/langfuse/langfuse_sdk.py @@ -431,22 +431,24 @@ def _build_verified_span_exporter(*, public_key: object, secret_key: object, bas bundle and client certificate; v4 ships every observation through its own OTLP exporter, so a private-CA deployment would fail TLS on every export in a background thread while ``auth_check`` (still on the httpx client) stays - green. Only built when custom TLS material is configured; endpoint and - headers mirror ``langfuse._client.span_processor``. + green. Only built when TLS is configured away from the default (a CA bundle, + a client certificate, or verification switched off); endpoint and headers + mirror ``langfuse._client.span_processor``. """ import litellm + from litellm.llms.custom_httpx.http_handler import get_ssl_verify - ca_bundle: Final = litellm.ssl_verify if isinstance(litellm.ssl_verify, str) else None + ssl_verify: Final = get_ssl_verify() configured_certificate: Final = os.getenv("SSL_CERTIFICATE") or litellm.ssl_certificate client_certificate: Final = configured_certificate if isinstance(configured_certificate, str) else None - if ca_bundle is None and client_certificate is None: + if ssl_verify is True and client_certificate is None: return None from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter export_path: Final = os.getenv("LANGFUSE_OTEL_TRACES_EXPORT_PATH") endpoint: Final = f"{base_url}/{export_path}" if export_path else f"{base_url}/api/public/otel/v1/traces" encoded_auth: Final = b64encode(f"{public_key}:{secret_key}".encode()).decode("ascii") - return OTLPSpanExporter( + exporter: Final = OTLPSpanExporter( endpoint=endpoint, headers={ # mutable-ok: the exporter copies these into its session headers "Authorization": "Basic " + encoded_auth, @@ -454,9 +456,12 @@ def _build_verified_span_exporter(*, public_key: object, secret_key: object, bas "x-langfuse-sdk-version": version("langfuse"), "x-langfuse-public-key": str(public_key), }, - certificate_file=ca_bundle, + certificate_file=ssl_verify if isinstance(ssl_verify, str) else None, client_certificate_file=client_certificate, ) + if ssl_verify is False: + exporter._certificate_file = False # pyright: ignore[reportPrivateUsage] # the ctor coerces a False certificate_file back to True + return exporter def acquire_langfuse_client( diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py index 9ea6f34c115..f4bc4b796b8 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py @@ -955,7 +955,8 @@ def test_ssl_exporter_is_only_built_with_custom_tls_material(monkeypatch, tmp_pa import litellm from litellm.integrations.langfuse.langfuse_sdk import _build_verified_span_exporter - monkeypatch.delenv("SSL_CERTIFICATE", raising=False) + for name in ("SSL_CERTIFICATE", "SSL_VERIFY", "SSL_CERT_FILE"): + monkeypatch.delenv(name, raising=False) monkeypatch.setattr(litellm, "ssl_verify", True) monkeypatch.setattr(litellm, "ssl_certificate", None) assert ( @@ -973,6 +974,38 @@ def test_ssl_exporter_is_only_built_with_custom_tls_material(monkeypatch, tmp_pa assert exporter._headers["x-langfuse-sdk-version"] == installed_langfuse_version() +@pytest.mark.parametrize("switch", ["attribute", "env"]) +def test_ssl_exporter_disables_verification_when_litellm_does(monkeypatch, switch): + """v2 exported through the httpx client, so ``ssl_verify=False`` reached ingestion; v4's exporter must match.""" + import litellm + from litellm.integrations.langfuse.langfuse_sdk import _build_verified_span_exporter + + for name in ("SSL_CERTIFICATE", "SSL_VERIFY", "SSL_CERT_FILE"): + monkeypatch.delenv(name, raising=False) + monkeypatch.setattr(litellm, "ssl_certificate", None) + if switch == "attribute": + monkeypatch.setattr(litellm, "ssl_verify", False) + else: + monkeypatch.setattr(litellm, "ssl_verify", True) + monkeypatch.setenv("SSL_VERIFY", "False") + + exporter = _build_verified_span_exporter(public_key="pk", secret_key="sk", base_url="https://lf.internal.example") + assert exporter is not None + assert exporter._certificate_file is False + assert exporter._client_cert is None + + posted = [] + + def post(self, url, **kwargs): + posted.append((url, kwargs["verify"])) + raise ConnectionError("stop before the network") + + monkeypatch.setattr("requests.Session.post", post) + with pytest.raises(ConnectionError): + exporter._export(b"payload") + assert posted[0] == ("https://lf.internal.example/api/public/otel/v1/traces", False) + + def test_second_client_on_the_same_key_does_not_build_another_provider(): """A discarded TracerProvider is pinned forever by its atexit hook.""" import gc