diff --git a/litellm/integrations/otel/mappers/openinference.py b/litellm/integrations/otel/mappers/openinference.py index 1fa19b8d4a5..a7e0f1af3ac 100644 --- a/litellm/integrations/otel/mappers/openinference.py +++ b/litellm/integrations/otel/mappers/openinference.py @@ -104,22 +104,13 @@ class OpenInferenceMapper: @staticmethod def _indexed_split(inputs: int, outputs: int) -> tuple[int, int]: - """How many prompt and response messages get per-index attributes. - - Both directions share one span-wide allowance. The response is reserved at - least half of it, so a long prompt can never push the completion off the - span, and the prompt takes whatever the response leaves unused. - """ + """Prompt and response share one allowance; the response is reserved at least half of it.""" indexed_out: Final = min(outputs, max(_MAX_INDEXED_MESSAGES // 2, _MAX_INDEXED_MESSAGES - inputs)) return _MAX_INDEXED_MESSAGES - indexed_out, indexed_out @staticmethod def _prompt_positions(total: int, indexed: int) -> tuple[int, ...]: - """Which prompt messages get per-index attributes: message 0 and the most recent turns. - - A value length limit clips the ``input.value`` blob, so the system prompt and the - live turn each keep a short key of their own. The middle of a long prompt does not. - """ + """Prompt messages that get per-index attributes: message 0 and the most recent turns.""" if total <= indexed: return tuple(range(total)) return (0, *range(total - indexed + 1, total)) diff --git a/litellm/integrations/otel/mappers/utils.py b/litellm/integrations/otel/mappers/utils.py index d8918491720..c023621d2ef 100644 --- a/litellm/integrations/otel/mappers/utils.py +++ b/litellm/integrations/otel/mappers/utils.py @@ -33,17 +33,10 @@ core telemetry no matter how many vocabularies are configured. MAX_MESSAGE_ATTRS_PER_SPAN: Final = DEFAULT_SPAN_ATTRIBUTE_LIMIT // 8 -"""Span-wide ceiling on attributes spent spelling out chat messages per index. +"""Span-wide ceiling on per-index chat message attributes, prompt and response together. -A conversation is the other unbounded family: two attributes per message, for -the prompt and the response alike, on the same span. Past a few dozen turns the -family alone exceeds the span attribute limit and evicts the core telemetry -written before it. The ceiling covers both directions together, since a budget -handed to each direction separately doubles. An eighth is the largest share -that still fits beside the tool ceiling and the core of every vocabulary at -once, request parameters, cost breakdown and identity included. The complete -conversation still rides the JSON blob attributes; only the per-index -convenience keys are capped. +An eighth is the largest share that still fits beside the tool ceiling and the core +of every vocabulary at once. The complete conversation still rides the JSON blobs. """ diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_emitter.py b/tests/test_litellm/integrations/otel/test_otel_v2_emitter.py index 6491ab0f79f..16fbb242ebd 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_emitter.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_emitter.py @@ -485,13 +485,7 @@ def _indexed_message_count(attributes, prefix): @pytest.mark.parametrize("turns", [60, 200]) def test_long_conversation_does_not_evict_core_attributes(turns): - """Per-message OpenInference attributes must never crowd core telemetry off the span. - - With content capture on, the OpenInference vocabulary spells every prompt and - response message out as two per-index attributes. A few dozen turns overruns - the OTel SDK's 128-attribute span limit, which evicts oldest-first, so the - ``gen_ai.*`` set written before it is what disappears. - """ + """Per-message OpenInference attributes must never crowd core telemetry off the span.""" span = _conversation_span(["genai", "openinference"], _conversation_payload(turns)) a = span.attributes @@ -522,12 +516,7 @@ def test_short_conversation_keeps_every_message_indexed(): def test_indexed_prompt_keeps_opener_and_latest_turns_under_a_value_length_limit(monkeypatch): - """The per-index keys are the only untruncated copy once the SDK clips string values. - - Operators bound attribute sizes with ``OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT``, - which cuts the ``input.value`` blob short. The system prompt and the live turn - then have to survive as their own short keys, whatever the conversation length. - """ + """The system prompt and the live turn keep their own keys once the SDK clips ``input.value``.""" monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "256") payload = _conversation_payload(60) payload["messages"][0] = {"role": "system", "content": "be terse"} @@ -547,11 +536,7 @@ def test_indexed_prompt_keeps_opener_and_latest_turns_under_a_value_length_limit def test_message_cap_is_shared_across_input_and_output(): - """One span-wide allowance covers both directions, and the response always keeps a share. - - A long prompt takes what a single reply leaves over, and a many-choice reply - cannot take the whole allowance away from the prompt either. - """ + """One span-wide allowance covers both directions, and the response always keeps a share.""" long_prompt = _conversation_span(["genai", "openinference"], _conversation_payload(60, choices=1)).attributes many_choices = _conversation_span(["genai", "openinference"], _conversation_payload(60, choices=20)).attributes @@ -569,13 +554,7 @@ def test_message_cap_is_shared_across_input_and_output(): def test_fully_populated_span_with_every_vocabulary_stays_within_the_attribute_limit(): - """Every capped family maxed at once still leaves the whole core intact. - - Every vocabulary in the registry plus ``legacy``, every request parameter, - every cost component, a hundred-plus tools, a two-hundred-turn prompt and - twenty choices is the worst case the two span-wide ceilings have to absorb - together. - """ + """Every capped family maxed at once still leaves the whole core intact.""" payload = _conversation_payload( 200, choices=20,