diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 464dc799c16..a641b0a8109 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -484,8 +484,8 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) if sanitized_tools is not None: response_api_optional_request_params["tools"] = sanitized_tools - final_request_params: Final = dict( - ResponsesAPIRequestParams(model=model, input=replay_safe_input, **response_api_optional_request_params) + final_request_params: Final = dict[str, object]( + model=model, input=replay_safe_input, **response_api_optional_request_params ) return final_request_params @@ -1197,9 +1197,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) if sanitized_tools is not None: response_api_optional_request_params["tools"] = sanitized_tools - data: Final = dict( - ResponsesAPIRequestParams(model=model, input=replay_safe_input, **response_api_optional_request_params) - ) + data: Final = dict[str, object](model=model, input=replay_safe_input, **response_api_optional_request_params) return url, data diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 8cc9e912e80..eab662e3402 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1323,6 +1323,26 @@ class ResponsesAPIStreamOptions(TypedDict, total=False): include_obfuscation: bool +class ResponsesAPIModerationPolicyEntry(TypedDict, total=False): + """Moderation policy for either the input or the output of a response.""" + + mode: Required[ReadOnly[Literal["score", "block"]]] + + +class ResponsesAPIModerationPolicy(TypedDict, total=False): + """Policy applied to moderated response input and output.""" + + input: ReadOnly[ResponsesAPIModerationPolicyEntry | None] + output: ReadOnly[ResponsesAPIModerationPolicyEntry | None] + + +class ResponsesAPIModeration(TypedDict, total=False): + """Configuration for running moderation on the input and output of a response.""" + + model: Required[ReadOnly[str]] + policy: ReadOnly[ResponsesAPIModerationPolicy | None] + + class ResponsesAPIOptionalRequestParams(TypedDict, total=False): """TypedDict for Optional parameters supported by the responses API.""" @@ -1355,6 +1375,8 @@ class ResponsesAPIOptionalRequestParams(TypedDict, total=False): partial_images: int | None # Number of partial images to generate (1-3) for streaming image generation context_management: list[ContextManagementEntry] | None """Context management configuration. E.g. [{\"type\": \"compaction\", \"compact_threshold\": 200000}] for server-side compaction (minimum 1000).""" + moderation: ReadOnly[ResponsesAPIModeration | None] + """Moderation configuration for the response input and output. E.g. {\"model\": \"omni-moderation-latest\"}.""" class ResponsesAPIRequestParams(ResponsesAPIOptionalRequestParams, total=False): diff --git a/tests/unit/responses/test_responses_api_request_body.py b/tests/unit/responses/test_responses_api_request_body.py index 4a2fc0627ab..3c1c21c9e90 100644 --- a/tests/unit/responses/test_responses_api_request_body.py +++ b/tests/unit/responses/test_responses_api_request_body.py @@ -228,6 +228,56 @@ async def test_aresponses_drops_stream_options(): assert "stream_options" not in request_body +@pytest.mark.asyncio +async def test_aresponses_forwards_moderation(): + """OpenAI's top-level `moderation` param must reach the wire instead of being silently dropped.""" + moderation: Final = { + "model": "omni-moderation-latest", + "policy": {"input": {"mode": "score"}, "output": {"mode": "block"}}, + } + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + mock_post.return_value = MockResponse(_minimal_responses_api_payload("resp_moderation_test", "gpt-5.4"), 200) + + await litellm.aresponses( + model="openai/gpt-5.4", + api_key="fake-api-key", + input="hi", + moderation=moderation, + ) + + mock_post.assert_called_once() + post_kwargs = mock_post.call_args.kwargs + request_body = post_kwargs["json"] if "json" in post_kwargs else json.loads(post_kwargs["data"]) + assert request_body["moderation"] == moderation + + +@pytest.mark.asyncio +async def test_aresponses_omits_moderation_when_none(): + """An explicit moderation=None is a default value and must not be sent to the provider.""" + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + mock_post.return_value = MockResponse( + _minimal_responses_api_payload("resp_moderation_none_test", "gpt-5.4"), 200 + ) + + await litellm.aresponses( + model="openai/gpt-5.4", + api_key="fake-api-key", + input="hi", + moderation=None, + ) + + mock_post.assert_called_once() + post_kwargs = mock_post.call_args.kwargs + request_body = post_kwargs["json"] if "json" in post_kwargs else json.loads(post_kwargs["data"]) + assert "moderation" not in request_body + + @pytest.mark.asyncio async def test_aresponses_forwards_non_enum_reasoning_effort( monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index daffc3c0c41..b565dd90d4c 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -43893,6 +43893,34 @@ export interface components { */ type: "tool_search_output"; }; + /** + * ResponsesAPIModeration + * @description Configuration for running moderation on the input and output of a response. + */ + ResponsesAPIModeration: { + /** Model */ + model: string; + policy?: components["schemas"]["ResponsesAPIModerationPolicy"] | null; + }; + /** + * ResponsesAPIModerationPolicy + * @description Policy applied to moderated response input and output. + */ + ResponsesAPIModerationPolicy: { + input?: components["schemas"]["ResponsesAPIModerationPolicyEntry"] | null; + output?: components["schemas"]["ResponsesAPIModerationPolicyEntry"] | null; + }; + /** + * ResponsesAPIModerationPolicyEntry + * @description Moderation policy for either the input or the output of a response. + */ + ResponsesAPIModerationPolicyEntry: { + /** + * Mode + * @enum {string} + */ + mode: "score" | "block"; + }; /** * ResponsesAPIRequestParams * @description TypedDict for request parameters supported by the responses API. @@ -43918,6 +43946,7 @@ export interface components { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -66337,6 +66366,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -69801,6 +69831,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -80458,6 +80489,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */