fix(responses): honor allowed_openai_params for non-spec fields on native /v1/responses

This commit is contained in:
Devin AI 2026-07-28 13:18:15 +00:00
parent daf22ec871
commit 0f8d8a9c29
3 changed files with 92 additions and 3 deletions

View file

@ -1028,7 +1028,10 @@ def responses(
local_vars["reasoning"] = _mapped
# Get ResponsesAPIOptionalRequestParams with only valid parameters
response_api_optional_params: ResponsesAPIOptionalRequestParams = (
ResponsesAPIRequestUtils.get_requested_response_api_optional_param(local_vars)
ResponsesAPIRequestUtils.get_requested_response_api_optional_param(
local_vars,
allowed_openai_params=allowed_openai_params,
)
)
_file_search_dispatch = _responses_try_dispatch_emulated_file_search(

View file

@ -7,6 +7,7 @@ from typing import (
List,
Mapping,
Optional,
Sequence,
Type,
Union,
cast,
@ -185,19 +186,26 @@ class ResponsesAPIRequestUtils:
@staticmethod
def get_requested_response_api_optional_param(
params: Dict[str, Any],
allowed_openai_params: Sequence[str] | None = None,
) -> ResponsesAPIOptionalRequestParams:
"""
Filter parameters to only include those defined in ResponsesAPIOptionalRequestParams.
Filter parameters to only include those defined in ResponsesAPIOptionalRequestParams,
plus any request/deployment level `allowed_openai_params` the caller opted into.
Args:
params: Dictionary of parameters to filter
allowed_openai_params: Extra request fields to keep even though they are not
part of the Responses API spec
Returns:
ResponsesAPIOptionalRequestParams instance with only the valid parameters
"""
from litellm.types.utils import all_litellm_params
from litellm.utils import PreProcessNonDefaultParams
valid_keys = get_type_hints(ResponsesAPIOptionalRequestParams).keys()
valid_keys = frozenset(get_type_hints(ResponsesAPIOptionalRequestParams)) | frozenset(
param for param in allowed_openai_params or () if param not in all_litellm_params
)
custom_llm_provider = params.pop("custom_llm_provider", None)
special_params = params.pop("kwargs", {})

View file

@ -246,6 +246,84 @@ async def test_aresponses_keeps_include_obfuscation_in_stream_options():
assert request_body["stream_options"] == {"include_obfuscation": False}
@pytest.mark.asyncio
async def test_aresponses_allowed_openai_params_forwards_extension_param():
"""
allowed_openai_params must preserve request fields that are not part of the
Responses API spec, so OpenAI-compatible upstreams that require them still
receive them. Regression test for https://github.com/BerriAI/litellm/issues/34926
"""
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_allowed_params_test", "gpt-5.5"), 200
)
await litellm.aresponses(
model="openai/gpt-5.5",
api_key="fake-api-key",
input="ping",
allowed_openai_params=["client_metadata"],
client_metadata={"probe": "keep-me"},
)
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["client_metadata"] == {"probe": "keep-me"}
@pytest.mark.asyncio
async def test_aresponses_drops_extension_param_without_allowlist():
"""Without an allowlist, unknown extension params stay off the wire."""
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_no_allowlist_test", "gpt-5.5"), 200
)
await litellm.aresponses(
model="openai/gpt-5.5",
api_key="fake-api-key",
input="ping",
client_metadata={"probe": "keep-me"},
)
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 "client_metadata" not in request_body
@pytest.mark.asyncio
async def test_aresponses_allowed_openai_params_cannot_leak_litellm_params():
"""allowed_openai_params must never promote internal litellm params (e.g. credentials) into the request body."""
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_no_credential_leak_test", "gpt-5.5"), 200
)
await litellm.aresponses(
model="openai/gpt-5.5",
api_key="fake-api-key",
input="ping",
allowed_openai_params=["api_key", "api_base"],
)
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 "api_key" not in request_body
assert "api_base" not in request_body
@pytest.mark.asyncio
async def test_aresponses_request_level_drop_params_drops_bedrock_mantle_service_tier(
monkeypatch,