diff --git a/litellm/integrations/arize/_utils.py b/litellm/integrations/arize/_utils.py index 6c20cb678cf..aa0c80dad32 100644 --- a/litellm/integrations/arize/_utils.py +++ b/litellm/integrations/arize/_utils.py @@ -20,12 +20,14 @@ class ArizeOTELAttributes(BaseLLMObsOTELAttributes): @staticmethod @override - def set_messages(span: "Span", messages: List[Dict[str, Any]]): + def set_messages(span: "Span", kwargs: Dict[str, Any]): from litellm.integrations._types.open_inference import ( MessageAttributes, SpanAttributes, ) + messages = kwargs.get("messages") + # for /chat/completions # https://docs.arize.com/arize/large-language-models/tracing/semantic-conventions if messages: @@ -205,9 +207,7 @@ def set_attributes( SpanAttributes.OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKindValues.LLM.value, ) - messages = kwargs.get("messages") - - attributes.set_messages(span, messages) + attributes.set_messages(span, kwargs) # Capture tools (function definitions) used in the LLM call. tools = optional_params.get("tools") diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index 897e5890d5b..f14412ad53b 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -1,25 +1,104 @@ -import json -from typing import TYPE_CHECKING, Any, Dict, List +""" +If the LLM Obs has any specific attributes to log request or response, we can add them here. +Relevant Issue: https://github.com/BerriAI/litellm/issues/13764 +""" + +import json +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from numpy import isin from pydantic import BaseModel from typing_extensions import override +import litellm from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes import ( BaseLLMObsOTELAttributes, safe_set_attribute, ) +from litellm.types.llms.openai import HttpxBinaryResponseContent, ResponsesAPIResponse +from litellm.types.utils import ( + EmbeddingResponse, + ImageResponse, + ModelResponse, + RerankResponse, + TextCompletionResponse, + TranscriptionResponse, +) if TYPE_CHECKING: from opentelemetry.trace import Span +def get_output_content_by_type( + response_obj: Union[ + None, + dict, + EmbeddingResponse, + ModelResponse, + TextCompletionResponse, + ImageResponse, + TranscriptionResponse, + RerankResponse, + HttpxBinaryResponseContent, + ResponsesAPIResponse, + list, + ], + kwargs: Optional[Dict[str, Any]] = None, +) -> str: + """ + Extract output content from response objects based on their type. + + This utility function handles the type-specific logic for converting + various response objects into appropriate output formats for Langfuse logging. + + Args: + response_obj: The response object returned by the function + kwargs: Optional keyword arguments containing call_type and other metadata + + Returns: + The formatted output content suitable for Langfuse logging, or None + """ + if response_obj is None: + return "" + + kwargs = kwargs or {} + call_type = kwargs.get("call_type", None) + + # Embedding responses - no output content + if call_type == "embedding" or isinstance(response_obj, EmbeddingResponse): + return "embedding-output" + + # Binary/Speech responses + if isinstance(response_obj, HttpxBinaryResponseContent): + return "speech-output" + + if isinstance(response_obj, BaseModel): + return response_obj.model_dump_json() + + if response_obj and ( + isinstance(response_obj, dict) or isinstance(response_obj, list) + ): + return json.dumps(response_obj) + else: + return "" + + class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): @staticmethod @override - def set_messages(span: "Span", messages: List[Dict[str, Any]]): - safe_set_attribute( - span, "langfuse.observation.input", json.dumps({"messages": messages}) - ) + def set_messages(span: "Span", kwargs: Dict[str, Any]): + prompt = {"messages": kwargs.get("messages")} + optional_params = kwargs.get("optional_params", {}) + functions = optional_params.get("functions") + tools = optional_params.get("tools") + if functions is not None: + prompt["functions"] = functions + if tools is not None: + prompt["tools"] = tools + + input = prompt + safe_set_attribute(span, "langfuse.observation.input", json.dumps(input)) @staticmethod @override @@ -27,9 +106,5 @@ class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): safe_set_attribute( span, "langfuse.observation.output", - ( - response_obj.model_dump_json() - if isinstance(response_obj, BaseModel) - else response_obj - ), + get_output_content_by_type(response_obj), ) diff --git a/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py b/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py index 930d564d3da..8d7b2d681ab 100644 --- a/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py +++ b/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: class BaseLLMObsOTELAttributes(ABC): @staticmethod - def set_messages(span: "Span", messages: List[Dict[str, Any]]): + def set_messages(span: "Span", kwargs: Dict[str, Any]): pass @staticmethod