From 2ba1b5bf5037a58f2997fa01e13f8d91170d91d1 Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 12:28:48 +0530 Subject: [PATCH 1/8] fix(hosted_vllm): forward provider-specific params in Responses API (closes #48362) --- .../hosted_vllm/responses/transformation.py | 58 +++++++++++++++ litellm/responses/utils.py | 10 ++- .../responses/test_hosted_vllm_responses.py | 71 +++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/litellm/llms/hosted_vllm/responses/transformation.py b/litellm/llms/hosted_vllm/responses/transformation.py index 8489bc0da84..9e9254712e8 100644 --- a/litellm/llms/hosted_vllm/responses/transformation.py +++ b/litellm/llms/hosted_vllm/responses/transformation.py @@ -11,9 +11,32 @@ from typing import Final from litellm.litellm_core_utils.url_utils import join_api_base from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ResponseInputParam from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders +HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS: Final = ( + "chat_template_kwargs", + "top_k", + "repetition_penalty", + "presence_penalty", + "frequency_penalty", + "min_p", + "best_of", + "stop_token_ids", + "spaces_between_special_tokens", + "prompt_logprobs", + "skip_special_tokens", + "include_stop_str_in_output", + "guided_json", + "guided_regex", + "guided_choice", + "guided_grammar", + "guided_decoding_backend", + "guided_whitespace_pattern", + "priority", +) + class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): """ @@ -29,6 +52,41 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): def custom_llm_provider(self) -> LlmProviders: return LlmProviders.HOSTED_VLLM + def get_supported_openai_params(self, model: str) -> list[str]: + params = super().get_supported_openai_params(model) + params.extend(HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS) + return params + + def transform_responses_api_request( + self, + model: str, + input: str | ResponseInputParam, + response_api_optional_request_params: dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> dict: + request: Final[dict] = super().transform_responses_api_request( + model=model, + input=input, + response_api_optional_request_params=response_api_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + for param in HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS: + if param in response_api_optional_request_params and response_api_optional_request_params[param] is not None: + request[param] = response_api_optional_request_params[param] + elif litellm_params is not None: + val = ( + litellm_params.get(param) + if hasattr(litellm_params, "get") + else getattr(litellm_params, param, None) + ) + if val is not None: + request[param] = val + + return request + def validate_environment( self, headers: dict, diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index dc75e77d685..1492ee51963 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -338,6 +338,14 @@ class ResponsesAPIRequestUtils: custom_llm_provider: Final = params.pop("custom_llm_provider", None) special_params: Final[dict[str, object]] = params.pop("kwargs", {}) + allowed_openai_params: Final = ( + params.get("allowed_openai_params") + or special_params.get("allowed_openai_params") + or [] + ) + allowed_set: Final = {p for p in allowed_openai_params if isinstance(p, str)} + default_keys: Final = set(valid_keys) | allowed_set + additional_drop_params: Final[list[str] | None] = params.pop("additional_drop_params", None) non_default_params: Final = _apply_nested_drop_params( PreProcessNonDefaultParams.base_pre_process_non_default_params( @@ -345,7 +353,7 @@ class ResponsesAPIRequestUtils: special_params=special_params, custom_llm_provider=custom_llm_provider, additional_drop_params=additional_drop_params, - default_param_values={k: None for k in valid_keys}, + default_param_values={k: None for k in default_keys}, additional_endpoint_specific_params=["input"], ), additional_drop_params, diff --git a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py index e32f876a6b9..2fec0e44b44 100644 --- a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py +++ b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py @@ -183,3 +183,74 @@ def test_hosted_vllm_validate_environment_custom_api_key(): ) assert headers.get("Authorization") == "Bearer my-custom-key" + + + +def test_hosted_vllm_responses_supported_params(): + """Test get_supported_openai_params() includes vLLM provider-specific params.""" + config = HostedVLLMResponsesAPIConfig() + supported = config.get_supported_openai_params(model="Qwen/Qwen3-8B") + + assert "chat_template_kwargs" in supported + assert "top_k" in supported + assert "repetition_penalty" in supported + assert "presence_penalty" in supported + assert "frequency_penalty" in supported + assert "min_p" in supported + + +def test_hosted_vllm_responses_forwards_provider_specific_params(): + """ + Regression test for Issue #48362: + hosted_vllm Responses API must not drop provider-specific params + (chat_template_kwargs, top_k, repetition_penalty, presence_penalty) + that chat completions forwards. + """ + mock_client = _make_mock_http_client(_make_mock_responses_api_response("Test output")) + + with patch( + "litellm.llms.custom_httpx.llm_http_handler.get_httpx_client", + return_value=mock_client, + ): + response = litellm.responses( + model="hosted_vllm/Qwen/Qwen3-8B", + input="hi", + api_base="https://test-vllm.example.com/v1", + api_key="test-key", + chat_template_kwargs={"enable_thinking": False}, + top_k=20, + repetition_penalty=1.0, + presence_penalty=1.5, + ) + + assert response is not None + assert mock_client.post.call_count == 1 + call_json = mock_client.post.call_args[1]["json"] + assert call_json["model"] == "Qwen/Qwen3-8B" + assert call_json["input"] == "hi" + assert call_json["chat_template_kwargs"] == {"enable_thinking": False} + assert call_json["top_k"] == 20 + assert call_json["repetition_penalty"] == 1.0 + assert call_json["presence_penalty"] == 1.5 + + +def test_hosted_vllm_responses_transform_request_from_litellm_params(): + """Test transform_responses_api_request forwards provider params from litellm_params.""" + config = HostedVLLMResponsesAPIConfig() + litellm_params = GenericLiteLLMParams( + chat_template_kwargs={"enable_thinking": False}, + top_k=50, + min_p=0.05, + ) + + request = config.transform_responses_api_request( + model="Qwen/Qwen3-8B", + input="test input", + response_api_optional_request_params={}, + litellm_params=litellm_params, + headers={}, + ) + + assert request["chat_template_kwargs"] == {"enable_thinking": False} + assert request["top_k"] == 50 + assert request["min_p"] == 0.05 From 33f1c589016439dfdaf8e8a7556175423ffa45fc Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 12:59:55 +0530 Subject: [PATCH 2/8] style: apply ruff format and add test coverage for optional request params --- .../hosted_vllm/responses/transformation.py | 5 ++++- litellm/responses/utils.py | 4 +--- .../responses/test_hosted_vllm_responses.py | 20 ++++++++++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/litellm/llms/hosted_vllm/responses/transformation.py b/litellm/llms/hosted_vllm/responses/transformation.py index 9e9254712e8..e35d5a76207 100644 --- a/litellm/llms/hosted_vllm/responses/transformation.py +++ b/litellm/llms/hosted_vllm/responses/transformation.py @@ -74,7 +74,10 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): ) for param in HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS: - if param in response_api_optional_request_params and response_api_optional_request_params[param] is not None: + if ( + param in response_api_optional_request_params + and response_api_optional_request_params[param] is not None + ): request[param] = response_api_optional_request_params[param] elif litellm_params is not None: val = ( diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 1492ee51963..0f7be714c1b 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -339,9 +339,7 @@ class ResponsesAPIRequestUtils: special_params: Final[dict[str, object]] = params.pop("kwargs", {}) allowed_openai_params: Final = ( - params.get("allowed_openai_params") - or special_params.get("allowed_openai_params") - or [] + params.get("allowed_openai_params") or special_params.get("allowed_openai_params") or [] ) allowed_set: Final = {p for p in allowed_openai_params if isinstance(p, str)} default_keys: Final = set(valid_keys) | allowed_set diff --git a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py index 2fec0e44b44..2f0302004de 100644 --- a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py +++ b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py @@ -185,7 +185,6 @@ def test_hosted_vllm_validate_environment_custom_api_key(): assert headers.get("Authorization") == "Bearer my-custom-key" - def test_hosted_vllm_responses_supported_params(): """Test get_supported_openai_params() includes vLLM provider-specific params.""" config = HostedVLLMResponsesAPIConfig() @@ -254,3 +253,22 @@ def test_hosted_vllm_responses_transform_request_from_litellm_params(): assert request["chat_template_kwargs"] == {"enable_thinking": False} assert request["top_k"] == 50 assert request["min_p"] == 0.05 + + +def test_hosted_vllm_responses_transform_request_from_optional_request_params(): + """Test transform_responses_api_request forwards provider params from response_api_optional_request_params.""" + config = HostedVLLMResponsesAPIConfig() + + request = config.transform_responses_api_request( + model="Qwen/Qwen3-8B", + input="test input", + response_api_optional_request_params={ + "chat_template_kwargs": {"enable_thinking": False}, + "top_k": 30, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert request["chat_template_kwargs"] == {"enable_thinking": False} + assert request["top_k"] == 30 From e21e594f2758f9b7bc12f1b8954d78505c88637d Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 13:07:17 +0530 Subject: [PATCH 3/8] fix(types): annotate params with Final to satisfy LIT010 discipline --- litellm/llms/hosted_vllm/responses/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/hosted_vllm/responses/transformation.py b/litellm/llms/hosted_vllm/responses/transformation.py index e35d5a76207..386693e9897 100644 --- a/litellm/llms/hosted_vllm/responses/transformation.py +++ b/litellm/llms/hosted_vllm/responses/transformation.py @@ -53,7 +53,7 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): return LlmProviders.HOSTED_VLLM def get_supported_openai_params(self, model: str) -> list[str]: - params = super().get_supported_openai_params(model) + params: Final = super().get_supported_openai_params(model) params.extend(HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS) return params From 849b42f83098ca867907e34a275a2baa6fb45f8e Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 13:14:10 +0530 Subject: [PATCH 4/8] fix(types): suppress LIT001 mutable collection warnings matching superclass signatures --- litellm/llms/hosted_vllm/responses/transformation.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/litellm/llms/hosted_vllm/responses/transformation.py b/litellm/llms/hosted_vllm/responses/transformation.py index 386693e9897..1f5039a5b26 100644 --- a/litellm/llms/hosted_vllm/responses/transformation.py +++ b/litellm/llms/hosted_vllm/responses/transformation.py @@ -52,7 +52,9 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): def custom_llm_provider(self) -> LlmProviders: return LlmProviders.HOSTED_VLLM - def get_supported_openai_params(self, model: str) -> list[str]: + def get_supported_openai_params( + self, model: str + ) -> list[str]: # mutable-ok: mirrors OpenAIResponsesAPIConfig signature params: Final = super().get_supported_openai_params(model) params.extend(HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS) return params @@ -61,11 +63,11 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): self, model: str, input: str | ResponseInputParam, - response_api_optional_request_params: dict, + response_api_optional_request_params: dict, # mutable-ok: mirrors OpenAIResponsesAPIConfig signature litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> dict: - request: Final[dict] = super().transform_responses_api_request( + headers: dict, # mutable-ok: mirrors OpenAIResponsesAPIConfig signature + ) -> dict: # mutable-ok: mirrors OpenAIResponsesAPIConfig signature + request: Final = super().transform_responses_api_request( model=model, input=input, response_api_optional_request_params=response_api_optional_request_params, From ba571943dc83b16eba944e77d6253bcebb0a5067 Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 13:34:44 +0530 Subject: [PATCH 5/8] fix(types): annotate types and suppress basedpyright diagnostics on untyped base overrides --- .../hosted_vllm/responses/transformation.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/litellm/llms/hosted_vllm/responses/transformation.py b/litellm/llms/hosted_vllm/responses/transformation.py index 1f5039a5b26..59138b23627 100644 --- a/litellm/llms/hosted_vllm/responses/transformation.py +++ b/litellm/llms/hosted_vllm/responses/transformation.py @@ -11,7 +11,9 @@ from typing import Final from litellm.litellm_core_utils.url_utils import join_api_base from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str -from litellm.types.llms.openai import ResponseInputParam +from litellm.types.llms.openai import ( + ResponseInputParam, # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType] # upstream openai export +) from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders @@ -52,27 +54,29 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): def custom_llm_provider(self) -> LlmProviders: return LlmProviders.HOSTED_VLLM - def get_supported_openai_params( - self, model: str - ) -> list[str]: # mutable-ok: mirrors OpenAIResponsesAPIConfig signature - params: Final = super().get_supported_openai_params(model) + def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: mirrors base signature + params: Final[list[str]] = list( # mutable-ok: accumulator list + super().get_supported_openai_params(model) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] # base returns untyped list + ) params.extend(HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS) return params def transform_responses_api_request( self, model: str, - input: str | ResponseInputParam, - response_api_optional_request_params: dict, # mutable-ok: mirrors OpenAIResponsesAPIConfig signature + input: str | ResponseInputParam, # pyright: ignore[reportUnknownParameterType] # mirrors OpenAIResponsesAPIConfig signature + response_api_optional_request_params: dict[str, object], # mutable-ok: mirrors base signature litellm_params: GenericLiteLLMParams, - headers: dict, # mutable-ok: mirrors OpenAIResponsesAPIConfig signature - ) -> dict: # mutable-ok: mirrors OpenAIResponsesAPIConfig signature - request: Final = super().transform_responses_api_request( - model=model, - input=input, - response_api_optional_request_params=response_api_optional_request_params, - litellm_params=litellm_params, - headers=headers, + headers: dict[str, object], # mutable-ok: mirrors base signature + ) -> dict[str, object]: # mutable-ok: mirrors base signature + request: Final[dict[str, object]] = dict( # mutable-ok: payload dict + super().transform_responses_api_request( # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] # base returns untyped dict + model=model, + input=input, + response_api_optional_request_params=response_api_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) ) for param in HOSTED_VLLM_RESPONSES_SUPPORTED_PARAMS: @@ -81,12 +85,8 @@ class HostedVLLMResponsesAPIConfig(OpenAIResponsesAPIConfig): and response_api_optional_request_params[param] is not None ): request[param] = response_api_optional_request_params[param] - elif litellm_params is not None: - val = ( - litellm_params.get(param) - if hasattr(litellm_params, "get") - else getattr(litellm_params, param, None) - ) + else: + val: object = getattr(litellm_params, param, None) if val is not None: request[param] = val From d73e18f3305ac60bb3907a2246c9941a102a3d5f Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 13:58:39 +0530 Subject: [PATCH 6/8] fix(types): type annotate allowed_set and suppress dynamic collection diagnostic --- litellm/responses/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 0f7be714c1b..bfe944b70af 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -334,15 +334,19 @@ class ResponsesAPIRequestUtils: """ from litellm.utils import PreProcessNonDefaultParams - valid_keys: Final = get_type_hints(ResponsesAPIOptionalRequestParams).keys() + valid_keys: Final[set[str]] = set(get_type_hints(ResponsesAPIOptionalRequestParams).keys()) custom_llm_provider: Final = params.pop("custom_llm_provider", None) special_params: Final[dict[str, object]] = params.pop("kwargs", {}) - allowed_openai_params: Final = ( - params.get("allowed_openai_params") or special_params.get("allowed_openai_params") or [] + allowed_openai_params: Final[object] = params.get("allowed_openai_params") or special_params.get( + "allowed_openai_params" ) - allowed_set: Final = {p for p in allowed_openai_params if isinstance(p, str)} - default_keys: Final = set(valid_keys) | allowed_set + allowed_set: Final[set[str]] = ( # mutable-ok: set accumulator + {str(p) for p in allowed_openai_params if isinstance(p, str)} + if isinstance(allowed_openai_params, (list, tuple, set)) + else set() + ) + default_keys: Final[set[str]] = valid_keys | allowed_set additional_drop_params: Final[list[str] | None] = params.pop("additional_drop_params", None) non_default_params: Final = _apply_nested_drop_params( From e978c82ecb1628db70028dde1b9a20b9d7c1aadc Mon Sep 17 00:00:00 2001 From: HarshDubey23 Date: Mon, 26 Oct 2026 14:04:14 +0530 Subject: [PATCH 7/8] fix(types): use frozenset for valid_keys and allowed_set to avoid mutable type discipline breach --- litellm/responses/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index bfe944b70af..3fab03cc3b0 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -334,19 +334,19 @@ class ResponsesAPIRequestUtils: """ from litellm.utils import PreProcessNonDefaultParams - valid_keys: Final[set[str]] = set(get_type_hints(ResponsesAPIOptionalRequestParams).keys()) + valid_keys: Final[frozenset[str]] = frozenset(get_type_hints(ResponsesAPIOptionalRequestParams).keys()) custom_llm_provider: Final = params.pop("custom_llm_provider", None) special_params: Final[dict[str, object]] = params.pop("kwargs", {}) allowed_openai_params: Final[object] = params.get("allowed_openai_params") or special_params.get( "allowed_openai_params" ) - allowed_set: Final[set[str]] = ( # mutable-ok: set accumulator - {str(p) for p in allowed_openai_params if isinstance(p, str)} - if isinstance(allowed_openai_params, (list, tuple, set)) - else set() + allowed_set: Final[frozenset[str]] = ( + frozenset(p for p in allowed_openai_params if isinstance(p, str)) # pyright: ignore[reportUnknownVariableType] # element type is dynamic + if isinstance(allowed_openai_params, (list, tuple, set, frozenset)) + else frozenset() ) - default_keys: Final[set[str]] = valid_keys | allowed_set + default_keys: Final[frozenset[str]] = valid_keys | allowed_set additional_drop_params: Final[list[str] | None] = params.pop("additional_drop_params", None) non_default_params: Final = _apply_nested_drop_params( From 88befe027b5c4efc834da5be8dff4de2eea25d72 Mon Sep 17 00:00:00 2001 From: HarshDubey23 <191850210+HarshDubey23@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:48:03 +0530 Subject: [PATCH 8/8] fix(hosted_vllm): drop unneeded allowed_openai_params change, test extra_body merge The provider params reach the request through HostedVLLMResponsesAPIConfig alone, so the change to get_requested_response_api_optional_param is not needed for this fix. Also cover a request that sends its own extra_body: both reach vLLM and extra_body wins on a shared key. --- litellm/responses/utils.py | 14 ++-------- .../responses/test_hosted_vllm_responses.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 3fab03cc3b0..dc75e77d685 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -334,20 +334,10 @@ class ResponsesAPIRequestUtils: """ from litellm.utils import PreProcessNonDefaultParams - valid_keys: Final[frozenset[str]] = frozenset(get_type_hints(ResponsesAPIOptionalRequestParams).keys()) + valid_keys: Final = get_type_hints(ResponsesAPIOptionalRequestParams).keys() custom_llm_provider: Final = params.pop("custom_llm_provider", None) special_params: Final[dict[str, object]] = params.pop("kwargs", {}) - allowed_openai_params: Final[object] = params.get("allowed_openai_params") or special_params.get( - "allowed_openai_params" - ) - allowed_set: Final[frozenset[str]] = ( - frozenset(p for p in allowed_openai_params if isinstance(p, str)) # pyright: ignore[reportUnknownVariableType] # element type is dynamic - if isinstance(allowed_openai_params, (list, tuple, set, frozenset)) - else frozenset() - ) - default_keys: Final[frozenset[str]] = valid_keys | allowed_set - additional_drop_params: Final[list[str] | None] = params.pop("additional_drop_params", None) non_default_params: Final = _apply_nested_drop_params( PreProcessNonDefaultParams.base_pre_process_non_default_params( @@ -355,7 +345,7 @@ class ResponsesAPIRequestUtils: special_params=special_params, custom_llm_provider=custom_llm_provider, additional_drop_params=additional_drop_params, - default_param_values={k: None for k in default_keys}, + default_param_values={k: None for k in valid_keys}, additional_endpoint_specific_params=["input"], ), additional_drop_params, diff --git a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py index 2f0302004de..a83ad492bdd 100644 --- a/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py +++ b/tests/unit/llms/hosted_vllm/responses/test_hosted_vllm_responses.py @@ -272,3 +272,30 @@ def test_hosted_vllm_responses_transform_request_from_optional_request_params(): assert request["chat_template_kwargs"] == {"enable_thinking": False} assert request["top_k"] == 30 + + +def test_hosted_vllm_responses_provider_params_merge_with_request_extra_body(): + """ + A request-level extra_body must not drop the forwarded provider params: + unrelated keys from both reach vLLM, and extra_body wins on a shared key. + """ + mock_client = _make_mock_http_client(_make_mock_responses_api_response("Test output")) + + with patch( + "litellm.llms.custom_httpx.llm_http_handler.get_httpx_client", + return_value=mock_client, + ): + litellm.responses( + model="hosted_vllm/Qwen/Qwen3-8B", + input="hi", + api_base="https://test-vllm.example.com/v1", + api_key="test-key", + chat_template_kwargs={"enable_thinking": False}, + top_k=20, + extra_body={"min_p": 0.1, "top_k": 5}, + ) + + call_json = mock_client.post.call_args[1]["json"] + assert call_json["chat_template_kwargs"] == {"enable_thinking": False} + assert call_json["min_p"] == 0.1 + assert call_json["top_k"] == 5