mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
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:
parent
4737d491d6
commit
7537c4e26a
3 changed files with 21 additions and 33 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue