diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py index 0fcc7fdc573..b0d0048fde1 100644 --- a/litellm/integrations/langfuse/langfuse_sdk.py +++ b/litellm/integrations/langfuse/langfuse_sdk.py @@ -76,10 +76,11 @@ _LANGFUSE_INGESTION_VERSION_HEADER: Final = "x-langfuse-ingestion-version" _LANGFUSE_INGESTION_VERSION: Final = "4" _NO_REST_RETRIES: Final = RequestOptions(max_retries=0) _TRUNCATION_MARKER: Final = "" +_METADATA_PREFIXES: Final = (LangfuseOtelSpanAttributes.OBSERVATION_METADATA, LangfuseOtelSpanAttributes.TRACE_METADATA) _TRUNCATION_GROUPS: Final = ( (LangfuseOtelSpanAttributes.OBSERVATION_INPUT, LangfuseOtelSpanAttributes.TRACE_INPUT), (LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, LangfuseOtelSpanAttributes.TRACE_OUTPUT), - (LangfuseOtelSpanAttributes.OBSERVATION_METADATA, LangfuseOtelSpanAttributes.TRACE_METADATA), + _METADATA_PREFIXES, ) _SERVER_FLOOR_HINT: Final = ( "; the OTLP traces route needs a self-hosted Langfuse server on 3.63.0 or newer " @@ -605,6 +606,11 @@ def _group_size(attributes: Mapping[str, AttributeValue], group: tuple[str, ...] ) +def _marker_key(prefix: str) -> str: + """Langfuse reads input and output as one string but metadata only as flattened keys, so the marker gets one.""" + return f"{prefix}.truncated" if prefix in _METADATA_PREFIXES else prefix + + def _truncated(span: ReadableSpan) -> ReadableSpan | None: """The span with its largest remaining input, output or metadata replaced by the marker the v2 consumer wrote when an event went over ``LANGFUSE_MAX_EVENT_SIZE_BYTES``, or ``None`` once all three are gone.""" @@ -614,7 +620,9 @@ def _truncated(span: ReadableSpan) -> ReadableSpan | None: return None kept: Final = {key: value for key, value in attributes.items() if not _in_group(key, largest)} marked: Final = { - prefix: _TRUNCATION_MARKER for prefix in largest if any(_in_group(key, (prefix,)) for key in attributes) + _marker_key(prefix): _TRUNCATION_MARKER + for prefix in largest + if any(_in_group(key, (prefix,)) for key in attributes) } return ReadableSpan( name=span.name, diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py index 1e5f7c07a26..765b844820e 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py @@ -1313,13 +1313,15 @@ def test_exporter_truncates_a_single_oversized_span_the_way_v2_did_instead_of_dr def test_exporter_truncates_largest_first_and_drops_only_when_nothing_is_left(monkeypatch, caplog): + """Langfuse stores a bare ``langfuse.observation.metadata`` string as nothing, so the metadata marker travels + under a flattened key the way every other metadata value does.""" monkeypatch.setattr("litellm.integrations.langfuse.langfuse_sdk.sleep", lambda _: None) span = _generation_span( **{ "langfuse.observation.input": "i" * 3000, "langfuse.observation.output": "o" * 2000, "langfuse.observation.metadata.a": "m" * 500, - "langfuse.observation.metadata.b": "m" * 500, + "langfuse.trace.metadata.b": "m" * 500, } ) posted: list[dict[str, str]] = [] @@ -1343,9 +1345,14 @@ def test_exporter_truncates_largest_first_and_drops_only_when_nothing_is_left(mo [], ["langfuse.observation.input"], ["langfuse.observation.input", "langfuse.observation.output"], - ["langfuse.observation.input", "langfuse.observation.metadata", "langfuse.observation.output"], + [ + "langfuse.observation.input", + "langfuse.observation.metadata.truncated", + "langfuse.observation.output", + "langfuse.trace.metadata.truncated", + ], ] - assert "langfuse.observation.metadata.a" not in posted[-1] + assert "langfuse.observation.metadata.a" not in posted[-1] and "langfuse.trace.metadata.b" not in posted[-1] assert "dropping it" in caplog.text