mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
[Feat] Add logging callback support for /moderations API (#10390)
* add OpenAIModerationResponse as a type * Add typing for litellm.moderation response obj * Add typing for litellm.moderation response obj * test_moderations_api_logging * add assertions for logged slp object * test_moderations_api_logging * refactor moderations api usage * Revert "test_moderations_api_logging" This reverts commit5a9001f04c. * Revert "refactor moderations api usage" This reverts commitb076d3174c. * test_moderations_api_logging
This commit is contained in:
parent
25213f6087
commit
34cdefbb77
6 changed files with 147 additions and 12 deletions
|
|
@ -58,6 +58,7 @@ from litellm.responses.utils import ResponseAPILoggingUtils
|
|||
from litellm.types.llms.openai import (
|
||||
HttpxBinaryResponseContent,
|
||||
ImageGenerationRequestQuality,
|
||||
OpenAIModerationResponse,
|
||||
OpenAIRealtimeStreamList,
|
||||
OpenAIRealtimeStreamResponseBaseObject,
|
||||
OpenAIRealtimeStreamSessionEvents,
|
||||
|
|
@ -643,9 +644,9 @@ def completion_cost( # noqa: PLR0915
|
|||
or isinstance(completion_response, dict)
|
||||
): # tts returns a custom class
|
||||
if isinstance(completion_response, dict):
|
||||
usage_obj: Optional[
|
||||
Union[dict, Usage]
|
||||
] = completion_response.get("usage", {})
|
||||
usage_obj: Optional[Union[dict, Usage]] = (
|
||||
completion_response.get("usage", {})
|
||||
)
|
||||
else:
|
||||
usage_obj = getattr(completion_response, "usage", {})
|
||||
if isinstance(usage_obj, BaseModel) and not _is_known_usage_objects(
|
||||
|
|
@ -944,6 +945,7 @@ def response_cost_calculator(
|
|||
RerankResponse,
|
||||
ResponsesAPIResponse,
|
||||
LiteLLMRealtimeStreamLoggingObject,
|
||||
OpenAIModerationResponse,
|
||||
],
|
||||
model: str,
|
||||
custom_llm_provider: Optional[str],
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ from litellm.types.llms.openai import (
|
|||
FineTuningJob,
|
||||
HttpxBinaryResponseContent,
|
||||
OpenAIFileObject,
|
||||
OpenAIModerationResponse,
|
||||
ResponseCompletedEvent,
|
||||
ResponsesAPIResponse,
|
||||
)
|
||||
|
|
@ -943,6 +944,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
ResponseCompletedEvent,
|
||||
OpenAIFileObject,
|
||||
LiteLLMRealtimeStreamLoggingObject,
|
||||
OpenAIModerationResponse,
|
||||
],
|
||||
cache_hit: Optional[bool] = None,
|
||||
litellm_model_name: Optional[str] = None,
|
||||
|
|
@ -1137,6 +1139,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
or isinstance(logging_result, ResponsesAPIResponse)
|
||||
or isinstance(logging_result, OpenAIFileObject)
|
||||
or isinstance(logging_result, LiteLLMRealtimeStreamLoggingObject)
|
||||
or isinstance(logging_result, OpenAIModerationResponse)
|
||||
):
|
||||
## HIDDEN PARAMS ##
|
||||
hidden_params = getattr(logging_result, "_hidden_params", {})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import litellm
|
|||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
from litellm.types.llms.databricks import DatabricksTool
|
||||
from litellm.types.llms.openai import ChatCompletionThinkingBlock
|
||||
from litellm.types.llms.openai import (
|
||||
ChatCompletionThinkingBlock,
|
||||
OpenAIModerationResponse,
|
||||
)
|
||||
from litellm.types.utils import (
|
||||
ChatCompletionDeltaToolCall,
|
||||
ChatCompletionMessageToolCall,
|
||||
|
|
@ -297,6 +300,12 @@ class LiteLLMResponseObjectHandler:
|
|||
model_response_object = ImageResponse(**model_response_dict)
|
||||
return model_response_object
|
||||
|
||||
@staticmethod
|
||||
def convert_to_moderation_response(
|
||||
response_object: dict,
|
||||
) -> OpenAIModerationResponse:
|
||||
return OpenAIModerationResponse(**response_object)
|
||||
|
||||
@staticmethod
|
||||
def convert_chat_to_text_completion(
|
||||
response: ModelResponse,
|
||||
|
|
@ -500,9 +509,9 @@ def convert_to_model_response_object( # noqa: PLR0915
|
|||
provider_specific_fields["thinking_blocks"] = thinking_blocks
|
||||
|
||||
if reasoning_content:
|
||||
provider_specific_fields[
|
||||
"reasoning_content"
|
||||
] = reasoning_content
|
||||
provider_specific_fields["reasoning_content"] = (
|
||||
reasoning_content
|
||||
)
|
||||
|
||||
message = Message(
|
||||
content=content,
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ from .types.llms.openai import (
|
|||
ChatCompletionUserMessage,
|
||||
HttpxBinaryResponseContent,
|
||||
ImageGenerationRequestQuality,
|
||||
OpenAIModerationResponse,
|
||||
)
|
||||
from .types.utils import (
|
||||
LITELLM_IMAGE_VARIATION_PROVIDERS,
|
||||
|
|
@ -4436,7 +4437,7 @@ def adapter_completion(
|
|||
|
||||
def moderation(
|
||||
input: str, model: Optional[str] = None, api_key: Optional[str] = None, **kwargs
|
||||
):
|
||||
) -> OpenAIModerationResponse:
|
||||
# only supports open ai for now
|
||||
api_key = (
|
||||
api_key
|
||||
|
|
@ -4455,7 +4456,11 @@ def moderation(
|
|||
response = openai_client.moderations.create(input=input, model=model)
|
||||
else:
|
||||
response = openai_client.moderations.create(input=input)
|
||||
return response
|
||||
|
||||
response_dict: Dict = response.model_dump()
|
||||
return litellm.utils.LiteLLMResponseObjectHandler.convert_to_moderation_response(
|
||||
response_object=response_dict,
|
||||
)
|
||||
|
||||
|
||||
@client
|
||||
|
|
@ -4465,7 +4470,7 @@ async def amoderation(
|
|||
api_key: Optional[str] = None,
|
||||
custom_llm_provider: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
) -> OpenAIModerationResponse:
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
# only supports open ai for now
|
||||
|
|
@ -4487,10 +4492,13 @@ async def amoderation(
|
|||
_openai_client = openai_client
|
||||
|
||||
optional_params = GenericLiteLLMParams(**kwargs)
|
||||
litellm_logging_obj: Optional[LiteLLMLoggingObj] = kwargs.get(
|
||||
"litellm_logging_obj", None
|
||||
)
|
||||
try:
|
||||
(
|
||||
model,
|
||||
_custom_llm_provider,
|
||||
custom_llm_provider,
|
||||
_dynamic_api_key,
|
||||
_dynamic_api_base,
|
||||
) = litellm.get_llm_provider(
|
||||
|
|
@ -4502,11 +4510,28 @@ async def amoderation(
|
|||
except litellm.BadRequestError:
|
||||
# `model` is optional field for moderation - get_llm_provider will throw BadRequestError if model is not set / not recognized
|
||||
pass
|
||||
|
||||
# update litellm_logging_obj with environment variables
|
||||
custom_llm_provider = custom_llm_provider or litellm.LlmProviders.OPENAI.value
|
||||
if litellm_logging_obj is not None:
|
||||
litellm_logging_obj.update_environment_variables(
|
||||
model=model,
|
||||
user=kwargs.get("user", None),
|
||||
optional_params={},
|
||||
litellm_params={
|
||||
**kwargs,
|
||||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
if model is not None:
|
||||
response = await _openai_client.moderations.create(input=input, model=model)
|
||||
else:
|
||||
response = await _openai_client.moderations.create(input=input)
|
||||
return response
|
||||
response_dict: Dict = response.model_dump()
|
||||
return litellm.utils.LiteLLMResponseObjectHandler.convert_to_moderation_response(
|
||||
response_object=response_dict,
|
||||
)
|
||||
|
||||
|
||||
##### Image Generation #######################
|
||||
|
|
|
|||
|
|
@ -1268,3 +1268,21 @@ class ImageGenerationRequestQuality(str, Enum):
|
|||
AUTO = "auto"
|
||||
STANDARD = "standard"
|
||||
HD = "hd"
|
||||
|
||||
|
||||
class OpenAIModerationResponse(BaseLiteLLMOpenAIResponseObject):
|
||||
"""
|
||||
Response from the OpenAI Moderation API.
|
||||
"""
|
||||
|
||||
id: str
|
||||
"""The unique identifier for the moderation request."""
|
||||
|
||||
model: str
|
||||
"""The model used to generate the moderation results."""
|
||||
|
||||
results: List[Dict]
|
||||
"""A list of moderation objects."""
|
||||
|
||||
# Define private attributes using PrivateAttr
|
||||
_hidden_params: dict = PrivateAttr(default_factory=dict)
|
||||
|
|
|
|||
78
tests/logging_callback_tests/test_moderations_api_logging.py
Normal file
78
tests/logging_callback_tests/test_moderations_api_logging.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import Request
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
load_dotenv()
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from litellm.types.utils import StandardLoggingPayload, Usage, ModelInfoBase
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
|
||||
class TestCustomLogger(CustomLogger):
|
||||
def __init__(self):
|
||||
self.recorded_usage: Optional[Usage] = None
|
||||
self.standard_logging_payload: Optional[StandardLoggingPayload] = None
|
||||
|
||||
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
standard_logging_payload = kwargs.get("standard_logging_object")
|
||||
self.standard_logging_payload = standard_logging_payload
|
||||
print(
|
||||
"standard_logging_payload",
|
||||
json.dumps(standard_logging_payload, indent=4, default=str),
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model", [
|
||||
None,
|
||||
"omni-moderation-latest"
|
||||
])
|
||||
|
||||
async def test_moderations_api_logging(model):
|
||||
"""
|
||||
When moderations API is called, it should log the event on standard_logging_payload
|
||||
"""
|
||||
custom_logger = TestCustomLogger()
|
||||
litellm.logging_callback_manager.add_litellm_callback(custom_logger)
|
||||
|
||||
input_content = "Hello, how are you?"
|
||||
response = await litellm.amoderation(
|
||||
input=input_content,
|
||||
model=model,
|
||||
)
|
||||
|
||||
print("response", json.dumps(response, indent=4, default=str))
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
assert custom_logger.standard_logging_payload is not None
|
||||
|
||||
# validate the standard_logging_payload
|
||||
standard_logging_payload: StandardLoggingPayload = custom_logger.standard_logging_payload
|
||||
assert standard_logging_payload["call_type"] == litellm.utils.CallTypes.amoderation.value
|
||||
assert standard_logging_payload["status"] == "success"
|
||||
assert standard_logging_payload["custom_llm_provider"] == litellm.LlmProviders.OPENAI.value
|
||||
|
||||
|
||||
# assert the logged input == input
|
||||
assert standard_logging_payload["messages"][0]["content"] == input_content
|
||||
|
||||
# assert the logged response == response user received client side
|
||||
assert dict(standard_logging_payload["response"]) == response.model_dump()
|
||||
|
||||
Loading…
Add table
Reference in a new issue