diff --git a/litellm/integrations/otel/mappers/genai.py b/litellm/integrations/otel/mappers/genai.py index d4f14e97a7a..d9be68a06c2 100644 --- a/litellm/integrations/otel/mappers/genai.py +++ b/litellm/integrations/otel/mappers/genai.py @@ -10,7 +10,12 @@ table: one lambda per mapping operation, applied against the typed span data. from typing import Callable from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue, SpanData -from litellm.integrations.otel.mappers.utils import collect, drop_none +from litellm.integrations.otel.mappers.utils import ( + collect, + drop_none, + output_messages, + serialize_messages, +) from litellm.integrations.otel.model.payloads import ( GuardrailSpanData, LLMCallSpanData, @@ -47,6 +52,8 @@ class GenAIMapper: else None ), GenAI.REQUEST_SEED: lambda d: d.request_params.seed, + GenAI.INPUT_MESSAGES: lambda d: serialize_messages(d.messages_in), + GenAI.OUTPUT_MESSAGES: lambda d: serialize_messages(output_messages(d)), GenAI.RESPONSE_MODEL: lambda d: d.response_model, GenAI.RESPONSE_ID: lambda d: d.response_id, GenAI.RESPONSE_FINISH_REASONS: lambda d: ( diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_components.py b/tests/test_litellm/integrations/otel/test_otel_v2_components.py index 19f4b0ff457..19eef284b91 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_components.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_components.py @@ -2,6 +2,8 @@ baggage helpers, metrics, the typed coercion helpers, mapper branches, span-name builders, and the registry validator's failure paths. Needs the OTel SDK.""" +import json + import pytest pytest.importorskip("opentelemetry") @@ -225,6 +227,47 @@ def test_genai_mapper_all_request_params(): assert attrs["server.port"] == 443 +def test_genai_mapper_stamps_input_output_messages(): + data = LLMCallSpanData( + operation=GenAIOperation.CHAT, + provider="openai", + request_model="gpt-4o", + response_model="gpt-4o-2024", + response_id="resp_1", + request_params=LLMRequestParams(), + usage=LLMUsage(), + finish_reasons=("stop",), + error=None, + response_cost=None, + server=None, + identity=RequestIdentity(call_id="c1"), + messages_in=( + {"role": "system", "content": "Be concise."}, + {"role": "user", "content": "What's the weather?"}, + ), + choices_out=( + { + "finish_reason": "stop", + "message": {"role": "assistant", "content": "Sunny."}, + }, + ), + ) + attrs = GenAIMapper().map(data) + assert json.loads(attrs[GenAI.INPUT_MESSAGES]) == [ + {"role": "system", "content": "Be concise."}, + {"role": "user", "content": "What's the weather?"}, + ] + assert json.loads(attrs[GenAI.OUTPUT_MESSAGES]) == [ + {"role": "assistant", "content": "Sunny."} + ] + + +def test_genai_mapper_omits_messages_when_content_not_captured(): + attrs = GenAIMapper().map(_full_llm_call()) + assert GenAI.INPUT_MESSAGES not in attrs + assert GenAI.OUTPUT_MESSAGES not in attrs + + def test_genai_mapper_cost_breakdown(): from litellm.integrations.otel.model.semconv import LiteLLM