From 0413b42347d4af06aa5665293b56fdb348bd9dbc Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 1 Nov 2026 10:18:35 +0530 Subject: [PATCH] fix(otel): restore OpenInference cache and reasoning token details in v2 mapper --- .../otel/mappers/openinference.py | 5 +++ .../otel/test_otel_v2_vendor_mappers.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/litellm/integrations/otel/mappers/openinference.py b/litellm/integrations/otel/mappers/openinference.py index 0c465bbe5cb..ad127be8ac9 100644 --- a/litellm/integrations/otel/mappers/openinference.py +++ b/litellm/integrations/otel/mappers/openinference.py @@ -190,6 +190,8 @@ class OpenInferenceMapper: - ``llm.output_messages.{i}.message.role`` / ``...content`` - ``llm.output_messages.{i}.message.tool_calls.{j}.tool_call.*`` - ``llm.token_count.prompt`` / ``...completion`` / ``...total`` + - ``llm.token_count.prompt_details.cache_read`` / ``...cache_write`` + - ``llm.token_count.completion_details.reasoning`` - ``metadata`` — JSON object of allowlisted promoted request metadata - ``input.value`` / ``output.value`` — JSON-serialized request / response """ @@ -201,6 +203,9 @@ class OpenInferenceMapper: "llm.token_count.prompt": lambda d: d.usage.input_tokens, "llm.token_count.completion": lambda d: d.usage.output_tokens, "llm.token_count.total": lambda d: d.usage.total_tokens, + "llm.token_count.prompt_details.cache_read": lambda d: d.usage.cache_read_input_tokens or None, + "llm.token_count.prompt_details.cache_write": lambda d: d.usage.cache_creation_input_tokens or None, + "llm.token_count.completion_details.reasoning": lambda d: d.usage.reasoning_tokens or None, } # Folded into the ``llm.invocation_parameters`` JSON blob. diff --git a/tests/unit/integrations/otel/test_otel_v2_vendor_mappers.py b/tests/unit/integrations/otel/test_otel_v2_vendor_mappers.py index 57764bf902b..4b393e54883 100644 --- a/tests/unit/integrations/otel/test_otel_v2_vendor_mappers.py +++ b/tests/unit/integrations/otel/test_otel_v2_vendor_mappers.py @@ -100,6 +100,48 @@ def test_openinference_mapper_input_output_messages(): assert params["max_tokens"] == 128 +_OPENINFERENCE_TOKEN_DETAIL_KEYS: Final = ( + "llm.token_count.prompt_details.cache_read", + "llm.token_count.prompt_details.cache_write", + "llm.token_count.completion_details.reasoning", +) + + +def test_openinference_mapper_emits_cache_and_reasoning_token_details(): + usage: Final = LLMUsage( + input_tokens=120, + output_tokens=40, + total_tokens=160, + cache_read_input_tokens=80, + cache_creation_input_tokens=30, + reasoning_tokens=25, + ) + attrs: Final = OpenInferenceMapper().map(_llm_call(usage=usage)) + assert attrs["llm.token_count.prompt_details.cache_read"] == 80 + assert attrs["llm.token_count.prompt_details.cache_write"] == 30 + assert attrs["llm.token_count.completion_details.reasoning"] == 25 + + +@pytest.mark.parametrize( + "usage", + [ + LLMUsage(input_tokens=12, output_tokens=8, total_tokens=20), + LLMUsage( + input_tokens=12, + output_tokens=8, + total_tokens=20, + cache_read_input_tokens=0, + cache_creation_input_tokens=0, + reasoning_tokens=0, + ), + ], + ids=["absent", "zero"], +) +def test_openinference_mapper_omits_missing_token_details(usage: LLMUsage): + attrs: Final = OpenInferenceMapper().map(_llm_call(usage=usage)) + assert not set(_OPENINFERENCE_TOKEN_DETAIL_KEYS) & set(attrs) + + def test_openinference_mapper_skips_non_llm_roles(): from litellm.integrations.otel.model.payloads import GuardrailSpanData