diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py index 9ba66c17aaa..51bc22afb0b 100644 --- a/litellm/integrations/langfuse/langfuse_sdk.py +++ b/litellm/integrations/langfuse/langfuse_sdk.py @@ -395,6 +395,12 @@ def _parse_sample_rate(raw: str) -> float | None: return rate if 0.0 <= rate <= 1.0 else None +def _usable_sample_rate() -> float: + raw: Final = os.environ.get("LANGFUSE_SAMPLE_RATE") + parsed: Final = _parse_sample_rate(raw) if raw is not None else 1.0 + return 1.0 if parsed is None else parsed + + def configured_sample_rate() -> float: """``LANGFUSE_SAMPLE_RATE`` as a fraction, exporting everything when it is unset or unusable.""" raw: Final = os.environ.get("LANGFUSE_SAMPLE_RATE") @@ -633,6 +639,9 @@ def build_langfuse_client( That same cache keeps the first secret and host it saw for a public key, so the REST client behind ``get_prompt`` and ``auth_check`` is rebuilt from the credentials actually supplied. Without both keys the SDK disables the client, which has no REST client to rebuild. + + The SDK reads ``LANGFUSE_SAMPLE_RATE`` itself and raises on anything it cannot parse, so it + gets the rate litellm already validated; the sampler that matters is on litellm's provider. """ public_key: Final = parameters.get("public_key") secret_key: Final = parameters.get("secret_key") @@ -641,6 +650,7 @@ def build_langfuse_client( credentialed: Final = isinstance(public_key, str) and isinstance(secret_key, str) client: Final = Langfuse( **parameters, # pyright: ignore[reportArgumentType] # kwargs-ok: dict mirrors the typed ctor, values resolved by the callers + sample_rate=_usable_sample_rate(), tracer_provider=TracerProvider( resource=_resource(environment=environment, release=release), shutdown_on_exit=False ), diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py index 94f957285d0..1e591f42cf0 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py @@ -555,6 +555,33 @@ def test_sdk_client_without_keys_is_built_disabled_and_fails_auth_check(monkeypa assert client.auth_check() is False +@pytest.mark.parametrize("raw", ["1.5", "-0.5", "abc"]) +def test_sdk_client_is_built_despite_an_unusable_sample_rate(monkeypatch: pytest.MonkeyPatch, raw: str): + """The SDK parses ``LANGFUSE_SAMPLE_RATE`` itself and would raise, which took the whole callback down.""" + monkeypatch.setenv("LANGFUSE_SAMPLE_RATE", raw) + requests = [] + + def record(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(401, json={"message": "unauthorized"}) + + client = build_langfuse_client( + parameters={ + "public_key": "pk-sr-test-" + raw, + "secret_key": "sk", + "base_url": "http://127.0.0.1:1", + "httpx_client": httpx.Client(transport=httpx.MockTransport(record)), + }, + environment=None, + release=None, + mock_mode=True, + ) + + with pytest.raises(UnauthorizedError): + client.auth_check() + assert requests[-1].headers["authorization"] == "Basic " + b64encode(f"pk-sr-test-{raw}:sk".encode()).decode() + + def test_sdk_client_does_not_take_over_the_process_tracer_provider(): provider_before = otel_trace.get_tracer_provider() build_langfuse_client(