diff --git a/litellm/integrations/otel/presets/destinations.py b/litellm/integrations/otel/presets/destinations.py index 673337854ce..984b385a647 100644 --- a/litellm/integrations/otel/presets/destinations.py +++ b/litellm/integrations/otel/presets/destinations.py @@ -15,7 +15,7 @@ import litellm from litellm._logging import verbose_logger from litellm.integrations.otel.model.destination import OtelDestination from litellm.litellm_core_utils.url_utils import is_url_destination_allowed_by_host -from litellm.types.utils import CaptureMessageContent, OtelSpanScope, StandardCallbackDynamicParams +from litellm.types.utils import OtelSpanScope, StandardCallbackDynamicParams #: An endpoint plus the OTLP transport to reach it with, or ``None`` when the backend #: names no destination. The transport is ``None`` where the backend has only one. @@ -171,14 +171,6 @@ def _resource_defaults(callback_name: str) -> Mapping[str, str]: return backend() if backend is not None else _NO_ATTRS -def _capture_message_content(callback_name: str, configured: str | None) -> str | None: - """The key's or team's own setting, else the prompt and response where the older Langfuse logger sent them - whatever the global setting.""" - if configured is not None or callback_name != "langfuse_otel" or not _stands_in_for_the_legacy_langfuse_logger(): - return configured - return CaptureMessageContent.SPAN_ONLY - - def _span_scope(callback_name: str, params: StandardCallbackDynamicParams) -> OtelSpanScope: if callback_name != "langfuse_otel": return "full" @@ -246,7 +238,7 @@ def destination_for( callback_name=callback_name, protocol=protocol, span_scope=_span_scope(callback_name, params), - capture_message_content=_capture_message_content(callback_name, capture_message_content), + capture_message_content=capture_message_content, excluded_db_systems=excluded_db_systems, success_sampling_rate=success_sampling_rate, error_sampling_rate=error_sampling_rate, diff --git a/tests/integration/observability/test_langfuse_otel_tenant_only.py b/tests/integration/observability/test_langfuse_otel_tenant_only.py index 4204d286d82..32b8efdac9e 100644 --- a/tests/integration/observability/test_langfuse_otel_tenant_only.py +++ b/tests/integration/observability/test_langfuse_otel_tenant_only.py @@ -87,15 +87,19 @@ def _one_model_span(spans: tuple[Span, ...]) -> bool: return len(_model_spans(spans)) == 1 and not LEGACY_SPANS & {str(span["name"]) for span in spans} +@pytest.mark.parametrize("capture", [None, "span_only"]) @pytest.mark.parametrize("level", ["key", "team"]) def test_a_tenant_langfuse_project_gets_user_session_and_tags_without_operator_keys( - level: Literal["key", "team"], proxy: Gateway, provider: Wire, audit_sinks: SpanSinks + level: Literal["key", "team"], capture: str | None, proxy: Gateway, provider: Wire, audit_sinks: SpanSinks ) -> None: logging: Final[list[JsonValue]] = [ { "callback_name": "langfuse_otel", "callback_type": "success", - "callback_vars": langfuse_callback_variables(audit_sinks), + "callback_vars": { + **langfuse_callback_variables(audit_sinks), + **({"capture_message_content": capture} if capture else {}), + }, } ] with proxy.scenario() as scenario: @@ -135,7 +139,7 @@ def test_a_tenant_langfuse_project_gets_user_session_and_tags_without_operator_k for span in tenant ] == [{"user.id": "user-42", "session.id": session, "langfuse.trace.tags": TAGS}], [s["name"] for s in tenant] assert "langfuse.observation.metadata" in tenant[0]["attributes"], sorted(tenant[0]["attributes"]) - assert f"lf-tenant-only-{trace_id}" in str(tenant[0]["attributes"].get("langfuse.observation.input")), sorted( + assert (f"lf-tenant-only-{trace_id}" in json.dumps(tenant[0]["attributes"])) == (capture == "span_only"), sorted( tenant[0]["attributes"] ) assert len(operator) == 1, [span["name"] for span in operator] diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py index eb422508789..2739239e3fa 100644 --- a/tests/unit/integrations/otel/test_otel_v2_destinations.py +++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py @@ -3590,23 +3590,15 @@ class TestLangfuseDestinationFields: assert destination is not None assert dict(destination.resource_attributes) == {} - @pytest.mark.parametrize( - ("configured", "operator_keys", "expected"), - [ - (None, False, "span_only"), - ("no_content", False, "no_content"), - (None, True, None), - ("span_only", True, "span_only"), - ], - ids=["unset", "key-no-content", "operator-keys", "operator-keys-key-span-only"], - ) - def test_a_langfuse_destination_captures_the_prompt_where_the_legacy_logger_sent_it( + @pytest.mark.parametrize("operator_keys", [False, True], ids=["no-operator-keys", "operator-keys"]) + @pytest.mark.parametrize("configured", [None, "no_content", "span_only"]) + def test_a_langfuse_destination_keeps_its_own_capture_setting_whatever_the_operator_keys( self, monkeypatch: pytest.MonkeyPatch, configured: str | None, operator_keys: bool, - expected: str | None, ) -> None: + """An unset setting stays unset, so the destination follows the global capture mode either way.""" credential_less_proxy(monkeypatch) if operator_keys: monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-operator") @@ -3619,14 +3611,17 @@ class TestLangfuseDestinationFields: ) assert destination is not None - assert destination.capture_message_content == expected + assert destination.capture_message_content == configured - def test_only_the_langfuse_destination_gets_the_prompt_it_captures_by_default( - self, monkeypatch: pytest.MonkeyPatch + @pytest.mark.parametrize(("configured", "captured"), [(None, False), ("span_only", True)]) + def test_a_langfuse_destination_gets_the_prompt_only_when_its_setting_asks_without_operator_keys( + self, monkeypatch: pytest.MonkeyPatch, configured: str | None, captured: bool ) -> None: credential_less_proxy(monkeypatch) destination: Final = destination_for( - "langfuse_otel", StandardCallbackDynamicParams(langfuse_public_key="pk", langfuse_secret_key="sk") + "langfuse_otel", + StandardCallbackDynamicParams(langfuse_public_key="pk", langfuse_secret_key="sk"), + capture_message_content=configured, ) arize: Final = destination_for("arize", StandardCallbackDynamicParams(arize_space_id="s", arize_api_key="k")) assert destination is not None @@ -3637,7 +3632,7 @@ class TestLangfuseDestinationFields: ) assert {headers: "langfuse.observation.input" in attributes for headers, attributes in received.items()} == { - destination.header_string(): True, + destination.header_string(): captured, arize.header_string(): False, None: False, } @@ -5377,10 +5372,7 @@ class TestCaptureMessageContent: def test_the_setting_rides_the_teams_destination_and_omission_stays_omitted( self, monkeypatch: pytest.MonkeyPatch, setting: str | None ) -> None: - """On a proxy with Langfuse keys of its own; without them see ``TestLangfuseDestinationFields``.""" monkeypatch.setenv("LITELLM_OTEL_V2", "true") - monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-operator") - monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-operator") is_otel_v2_enabled.cache_clear() callback_vars: Final = { "langfuse_public_key": "pk-team",