From 7a6b614ba9dcfcc457555c3c7523f2ef1b446535 Mon Sep 17 00:00:00 2001 From: Poppy Huang Date: Tue, 13 Oct 2026 11:08:35 +0800 Subject: [PATCH 1/3] fix(responses): forward the top-level moderation param to OpenAI --- litellm/types/llms/openai.py | 22 ++++++++ .../test_responses_api_request_body.py | 50 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 26f65fee4fe..f31e28fe99f 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1313,6 +1313,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[Literal["score", "block"]] + + +class ResponsesAPIModerationPolicy(TypedDict, total=False): + """Policy applied to moderated response input and output.""" + + input: ResponsesAPIModerationPolicyEntry | None + output: ResponsesAPIModerationPolicyEntry | None + + +class ResponsesAPIModeration(TypedDict, total=False): + """Configuration for running moderation on the input and output of a response.""" + + model: Required[str] + policy: ResponsesAPIModerationPolicy | None + + class ResponsesAPIOptionalRequestParams(TypedDict, total=False): """TypedDict for Optional parameters supported by the responses API.""" @@ -1345,6 +1365,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: 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 8c8b347be82..322731c74b4 100644 --- a/tests/unit/responses/test_responses_api_request_body.py +++ b/tests/unit/responses/test_responses_api_request_body.py @@ -223,6 +223,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 From a864aa68d87e4f67ec0c03e252b453e28697077a Mon Sep 17 00:00:00 2001 From: Poppy Huang Date: Tue, 13 Oct 2026 11:36:50 +0800 Subject: [PATCH 2/3] fix(responses): mark moderation TypedDict fields ReadOnly and regenerate dashboard API types --- litellm/types/llms/openai.py | 12 +++---- ui/litellm-dashboard/src/lib/http/schema.d.ts | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index f31e28fe99f..0fb9fc57cf1 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1316,21 +1316,21 @@ class ResponsesAPIStreamOptions(TypedDict, total=False): class ResponsesAPIModerationPolicyEntry(TypedDict, total=False): """Moderation policy for either the input or the output of a response.""" - mode: Required[Literal["score", "block"]] + mode: Required[ReadOnly[Literal["score", "block"]]] class ResponsesAPIModerationPolicy(TypedDict, total=False): """Policy applied to moderated response input and output.""" - input: ResponsesAPIModerationPolicyEntry | None - output: ResponsesAPIModerationPolicyEntry | None + 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[str] - policy: ResponsesAPIModerationPolicy | None + model: Required[ReadOnly[str]] + policy: ReadOnly[ResponsesAPIModerationPolicy | None] class ResponsesAPIOptionalRequestParams(TypedDict, total=False): @@ -1365,7 +1365,7 @@ 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: ResponsesAPIModeration | None + moderation: ReadOnly[ResponsesAPIModeration | None] """Moderation configuration for the response input and output. E.g. {\"model\": \"omni-moderation-latest\"}.""" diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index ada0bb2b8a2..b570f8514b4 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -43370,6 +43370,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. @@ -43395,6 +43423,7 @@ export interface components { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -65505,6 +65534,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -68989,6 +69019,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ @@ -79349,6 +79380,7 @@ export interface operations { } | null; /** Model */ model: string; + moderation?: components["schemas"]["ResponsesAPIModeration"] | null; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Partial Images */ From abc79dd4305dd321aab87648a271487688fa7031 Mon Sep 17 00:00:00 2001 From: Poppy Huang Date: Tue, 13 Oct 2026 13:06:28 +0800 Subject: [PATCH 3/3] fix(responses): build OpenAI request body as dict[str, object] to keep basedpyright gate green --- litellm/llms/openai/responses/transformation.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index ec4f0c04475..18574f5b8fd 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -309,8 +309,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 @@ -959,9 +959,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