From ea0898fae2c7ede9e82e73a2d9c8741a9654210f Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sun, 18 Oct 2026 18:45:35 -0700 Subject: [PATCH] fix(rust-responses): filter body params with Python's Responses param rules Native Responses now builds its body through the same steps as litellm.responses, so the drop_params and model_param_gating features no longer need to keep these requests on Python. Co-Authored-By: Claude Opus 5.5 --- litellm/responses/dispatch.py | 2 - litellm/responses/main.py | 32 ++++++++------ litellm/responses/utils.py | 2 +- litellm/rust_bridge/catalog.py | 2 - litellm/rust_bridge/detectors.py | 17 +------ litellm/rust_bridge/host/routes/responses.py | 44 ++++++++++++++++--- .../responses/test_dispatch.py | 2 - .../responses/test_requests.py | 30 +++++++++++++ tests/unit/rust_bridge/test_detectors.py | 4 +- 9 files changed, 89 insertions(+), 46 deletions(-) diff --git a/litellm/responses/dispatch.py b/litellm/responses/dispatch.py index 7145e2a1b83..7b2d0e3fc29 100644 --- a/litellm/responses/dispatch.py +++ b/litellm/responses/dispatch.py @@ -65,7 +65,6 @@ def _public_request( _FEATURES: Final[detectors.Detectors] = MappingProxyType( { **detectors.COMMON, - Feature.DROP_PARAMS: detectors.drop_params, Feature.MODIFY_PARAMS: detectors.modify_params, Feature.STREAMING: detectors.streaming, Feature.PROMPT_MANAGEMENT: detectors.prompt_management, @@ -81,7 +80,6 @@ _FEATURES: Final[detectors.Detectors] = MappingProxyType( Feature.CACHE_CONTROL_MARKS: detectors.cache_control_marks, Feature.ANTHROPIC_TOOL_SEARCH: detectors.anthropic_tool_search, Feature.TOOL_SCHEMA_REPAIR: detectors.tool_schema_repair, - Feature.MODEL_PARAM_GATING: detectors.model_param_gating, Feature.WORKLOAD_IDENTITY: detectors.openai_workload_identity, Feature.CLIENT_HEADERS: detectors.client_headers, } diff --git a/litellm/responses/main.py b/litellm/responses/main.py index e775e155dea..e3f9553a46b 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -1623,12 +1623,14 @@ def responses( # Get optional parameters for the responses API request_drop_params: Final = kwargs.get("drop_params") - responses_api_request_params: Final[dict] = ResponsesAPIRequestUtils.get_optional_params_responses_api( - model=model, - responses_api_provider_config=responses_api_provider_config, - response_api_optional_params=response_api_optional_params, - allowed_openai_params=allowed_openai_params, - drop_params=normalize_drop_params(request_drop_params), + responses_api_request_params: Final[Mapping[str, object]] = ( + ResponsesAPIRequestUtils.get_optional_params_responses_api( + model=model, + responses_api_provider_config=responses_api_provider_config, + response_api_optional_params=response_api_optional_params, + allowed_openai_params=allowed_openai_params, + drop_params=normalize_drop_params(request_drop_params), + ) ) litellm_logging_obj.update_from_kwargs( @@ -1670,7 +1672,7 @@ def responses( model=model, input=input, responses_api_provider_config=responses_api_provider_config, - response_api_optional_request_params=responses_api_request_params, + response_api_optional_request_params=dict(responses_api_request_params), custom_llm_provider=custom_llm_provider, litellm_params=litellm_params, logging_obj=litellm_logging_obj, @@ -2473,12 +2475,14 @@ def compact_responses( # Get optional parameters for the responses API request_drop_params: Final = kwargs.get("drop_params") - responses_api_request_params: Final[dict] = ResponsesAPIRequestUtils.get_optional_params_responses_api( - model=model, - responses_api_provider_config=responses_api_provider_config, - response_api_optional_params=response_api_optional_params, - allowed_openai_params=None, - drop_params=normalize_drop_params(request_drop_params), + responses_api_request_params: Final[Mapping[str, object]] = ( + ResponsesAPIRequestUtils.get_optional_params_responses_api( + model=model, + responses_api_provider_config=responses_api_provider_config, + response_api_optional_params=response_api_optional_params, + allowed_openai_params=None, + drop_params=normalize_drop_params(request_drop_params), + ) ) # Pre Call logging @@ -2503,7 +2507,7 @@ def compact_responses( model=model, input=input, responses_api_provider_config=responses_api_provider_config, - response_api_optional_request_params=responses_api_request_params, + response_api_optional_request_params=dict(responses_api_request_params), litellm_params=litellm_params, logging_obj=litellm_logging_obj, custom_llm_provider=custom_llm_provider, diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 8e5e1c1cfee..ffdb54145f5 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -202,7 +202,7 @@ class ResponsesAPIRequestUtils: response_api_optional_params: ResponsesAPIOptionalRequestParams, allowed_openai_params: list[str] | None = None, drop_params: bool | None = None, - ) -> dict: + ) -> Mapping[str, object]: """ Get optional parameters for the responses API. diff --git a/litellm/rust_bridge/catalog.py b/litellm/rust_bridge/catalog.py index 93d344aa3ea..2fb5629d127 100644 --- a/litellm/rust_bridge/catalog.py +++ b/litellm/rust_bridge/catalog.py @@ -178,7 +178,6 @@ MESSAGES_PYTHON_FEATURES: Final = COMMON_PYTHON_FEATURES | { } RESPONSES_PYTHON_FEATURES: Final = COMMON_PYTHON_FEATURES | { - Feature.DROP_PARAMS, Feature.MODIFY_PARAMS, Feature.STREAMING, Feature.PROMPT_MANAGEMENT, @@ -194,7 +193,6 @@ RESPONSES_PYTHON_FEATURES: Final = COMMON_PYTHON_FEATURES | { Feature.CACHE_CONTROL_MARKS, Feature.ANTHROPIC_TOOL_SEARCH, Feature.TOOL_SCHEMA_REPAIR, - Feature.MODEL_PARAM_GATING, Feature.WORKLOAD_IDENTITY, Feature.CLIENT_HEADERS, Feature.DEPLOYMENT_HOOK, diff --git a/litellm/rust_bridge/detectors.py b/litellm/rust_bridge/detectors.py index 4404bc39230..d550eebcf42 100644 --- a/litellm/rust_bridge/detectors.py +++ b/litellm/rust_bridge/detectors.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from openai.types.responses import ResponseInputParam from litellm.responses.mcp.litellm_proxy_mcp_handler import ToolParam - from litellm.types.llms.openai import ALL_RESPONSES_API_TOOL_PARAMS, ResponsesAPIOptionalRequestParams + from litellm.types.llms.openai import ALL_RESPONSES_API_TOOL_PARAMS Detector: TypeAlias = Callable[[NativeCall], bool] Detectors: TypeAlias = Mapping[Feature, Detector] @@ -240,21 +240,6 @@ def tool_schema_repair(request: NativeCall) -> bool: _PARAMS: Final = TypeAdapter(dict[str, object]) -def model_param_gating(request: NativeCall) -> bool: - from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig - from litellm.rust_bridge.host.routes.responses import PARAMETERS - - params: Final = {name: request.resolved[name] for name in PARAMETERS if request.resolved.get(name) is not None} - mapped: Final = _PARAMS.validate_python( - OpenAIResponsesAPIConfig().map_openai_params( # pyright: ignore[reportUnknownMemberType] # returns a bare dict - cast("ResponsesAPIOptionalRequestParams", params), # cast-ok: the handler passes these kwargs - _openai_model(request), - drop_params=True, - ) - ) - return mapped != params - - _CHAT_UNGATED: Final = frozenset( { "model", diff --git a/litellm/rust_bridge/host/routes/responses.py b/litellm/rust_bridge/host/routes/responses.py index 22e8cbbf3b0..98129515c42 100644 --- a/litellm/rust_bridge/host/routes/responses.py +++ b/litellm/rust_bridge/host/routes/responses.py @@ -1,18 +1,50 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from typing import Final import litellm +from litellm import get_llm_provider +from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, +) +from litellm.litellm_core_utils.core_helpers import normalize_drop_params +from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.rust_bridge import failures -from litellm.rust_bridge.public_call import known_params, optional_str -from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams, ResponsesAPIResponse - -PARAMETERS: Final = tuple(ResponsesAPIOptionalRequestParams.__annotations__) +from litellm.rust_bridge.public_call import optional_str +from litellm.types.llms.openai import ResponsesAPIResponse +from litellm.utils import ProviderConfigManager def body_params(request: Mapping[str, object]) -> Mapping[str, object]: - return known_params(PARAMETERS, request) + model, provider, _, _ = get_llm_provider( + model=str(request["model"]), custom_llm_provider=optional_str(request.get("custom_llm_provider")) + ) + reasoning: Final = request.get("reasoning") + reasoning_effort: Final = request.get("reasoning_effort") + requested: Final = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + **{name: value for name, value in request.items() if name != "reasoning_effort"}, + "custom_llm_provider": provider, + "reasoning": ( + LiteLLMResponsesTransformationHandler().map_reasoning_effort(reasoning_effort) + if reasoning is None and reasoning_effort is not None + else reasoning + ), + } + ) + config: Final = ProviderConfigManager.get_provider_responses_api_config(provider=provider, model=model) + if config is None: + return dict(requested) + allowed: Final = request.get("allowed_openai_params") + allowed_values: Final[Sequence[object] | None] = allowed if isinstance(allowed, list) else None + return ResponsesAPIRequestUtils.get_optional_params_responses_api( + model=model, + responses_api_provider_config=config, + response_api_optional_params=requested, + allowed_openai_params=[str(name) for name in allowed_values] if allowed_values is not None else None, + drop_params=normalize_drop_params(request.get("drop_params")), + ) def connection_defaults(provider: str) -> tuple[str | None, str | None]: diff --git a/tests/test_litellm_rust/responses/test_dispatch.py b/tests/test_litellm_rust/responses/test_dispatch.py index 1b60d3a09c7..9cdf7ec7beb 100644 --- a/tests/test_litellm_rust/responses/test_dispatch.py +++ b/tests/test_litellm_rust/responses/test_dispatch.py @@ -276,7 +276,6 @@ ENCODED_ID: Final = ResponsesAPIRequestUtils._build_encrypted_item_id("deploymen (Feature.TIMEOUT_VARIANTS, {"request_timeout": 5}, {}), (Feature.AGENTIC_LOOP_HOOK, {}, {"callbacks": [_Agentic()]}), (Feature.TLS_POLICY, {}, {"ssl_security_level": "DEFAULT@SECLEVEL=1"}), - (Feature.DROP_PARAMS, {}, {"drop_params": True}), (Feature.STREAMING, {"stream": True}, {}), (Feature.PROMPT_MANAGEMENT, {"prompt_id": "welcome"}, {}), (Feature.TEXT_FORMAT, {"text_format": dict}, {}), @@ -302,7 +301,6 @@ ENCODED_ID: Final = ResponsesAPIRequestUtils._build_encrypted_item_id("deploymen {}, ), (Feature.TOOL_SCHEMA_REPAIR, {"tools": [{"type": "function", "name": "f", "parameters": "{}"}]}, {}), - (Feature.MODEL_PARAM_GATING, {"max_output_tokens": 1}, {}), (Feature.CLIENT_HEADERS, {"headers": {"x-tenant": "a"}}, {}), (Feature.DEPLOYMENT_HOOK, {}, {"callbacks": [_Deployment()]}), ( diff --git a/tests/test_litellm_rust/responses/test_requests.py b/tests/test_litellm_rust/responses/test_requests.py index 8a227408af6..84a86c307fd 100644 --- a/tests/test_litellm_rust/responses/test_requests.py +++ b/tests/test_litellm_rust/responses/test_requests.py @@ -5,6 +5,7 @@ import pytest from pydantic import TypeAdapter import litellm +from litellm.llms.openai.responses.transformation import OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.types.llms.openai import ResponsesAPIResponse from tests.test_litellm_rust.responses.data import RESPONSES_MODEL, RESPONSES_TEXT, call @@ -52,3 +53,32 @@ async def test_continuation_ids_are_decoded_before_they_reach_the_wire( await call(server, asynchronous=asynchronous, previous_response_id=previous) assert _OBJECT.validate_python(server.requests[0].body)["previous_response_id"] == original + + +@pytest.mark.asyncio +async def test_additional_drop_params_are_dropped_natively(server: RecordingServer) -> None: + await call(server, user="end-user", max_output_tokens=16, additional_drop_params=["user"]) + + assert_served_natively(server) + body: Final = _OBJECT.validate_python(server.requests[0].body) + assert "user" not in body + assert body["max_output_tokens"] == 16 + + +@pytest.mark.asyncio +async def test_reasoning_effort_reaches_the_wire_as_reasoning(server: RecordingServer) -> None: + await call(server, reasoning_effort="low") + + assert_served_natively(server) + body: Final = _OBJECT.validate_python(server.requests[0].body) + assert body["reasoning"] == {"effort": "low"} + assert "reasoning_effort" not in body + + +@pytest.mark.asyncio +async def test_sub_minimum_max_output_tokens_is_raised_like_python(server: RecordingServer) -> None: + await call(server, max_output_tokens=OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS - 1) + + assert_served_natively(server) + body: Final = _OBJECT.validate_python(server.requests[0].body) + assert body["max_output_tokens"] == OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS diff --git a/tests/unit/rust_bridge/test_detectors.py b/tests/unit/rust_bridge/test_detectors.py index 593a7f4f5f9..4d0a1a67c3c 100644 --- a/tests/unit/rust_bridge/test_detectors.py +++ b/tests/unit/rust_bridge/test_detectors.py @@ -122,8 +122,6 @@ def _wrap_nested_level(value: object, depth: int) -> object: (detectors.tool_schema_repair, {"tools": [function_tool({"type": "string", "pattern": r"\p{L}"})]}, True), (detectors.tool_schema_repair, {"tools": [function_tool({"type": "object"})]}, False), (detectors.tool_schema_repair, {}, False), - (detectors.model_param_gating, {"max_output_tokens": 1}, True), - (detectors.model_param_gating, {"max_output_tokens": 1024}, False), (detectors.cache_control_injection, {"enable_prompt_caching": True}, True), (detectors.cache_control_injection, {"cache_control_injection_points": [{"location": "message"}]}, True), (detectors.cache_control_injection, {"cache_control_injection_points": []}, False), @@ -212,8 +210,8 @@ def test_input_rewrite_preserves_caller_input(input_value: object, expected: boo @pytest.mark.parametrize( ("detector", "setting", "value", "kwargs"), ( - (detectors.drop_params, "drop_params", True, {}), (detectors.modify_params, "modify_params", True, {}), + (detectors.drop_params, "drop_params", True, {}), (detectors.retries_fallbacks, "num_retries", 3, {}), (detectors.cache_control_injection, "enable_anthropic_prompt_caching", True, {}), (detectors.stream_cost_injection, "include_cost_in_streaming_usage", True, {"stream": True}),