fix(otel): keep a key's Langfuse destination on the global capture setting when it sets none, with or without global Langfuse keys

This commit is contained in:
yucheng 2026-10-28 23:45:46 -07:00
parent 4737d491d6
commit 7537c4e26a
3 changed files with 21 additions and 33 deletions

View file

@ -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,

View file

@ -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]

View file

@ -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",