From 871c049f49bd0676f735a7dc0753e3e9c8027392 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 20 Feb 2026 16:22:19 +0530 Subject: [PATCH 1/2] Add support for reasoning and tools viaconfig --- docs/my-website/docs/reasoning_content.md | 19 +++++++++++++ litellm/responses/main.py | 11 ++++++++ litellm/router.py | 34 ++++++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/docs/my-website/docs/reasoning_content.md b/docs/my-website/docs/reasoning_content.md index 04c6d7ee6c..b5a5809bd4 100644 --- a/docs/my-website/docs/reasoning_content.md +++ b/docs/my-website/docs/reasoning_content.md @@ -642,6 +642,25 @@ model_list: model: openai/responses/gpt-5-mini ``` +**Per-model configuration** (recommended when using Open WebUI or clients that cannot set `extra_body`): + +```yaml +model_list: + - model_name: gpt-5.1 + litellm_params: + model: openai/gpt-5.1 + # String format - uses reasoning_auto_summary for summary when set + reasoning_effort: "high" + model_info: + mode: responses # if using Responses API bridge + + - model_name: gpt-5.1-with-summary + litellm_params: + model: openai/gpt-5.1 + # Dict format - explicit control over effort and summary + reasoning_effort: {"effort": "high", "summary": "detailed"} +``` + diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 2a1749b562..276e0cb9a0 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -20,6 +20,9 @@ from pydantic import BaseModel import litellm from litellm._logging import verbose_logger +from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, +) from litellm.constants import request_timeout from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.prompt_templates.common_utils import ( @@ -670,6 +673,14 @@ def responses( ) local_vars.update(kwargs) + # Map reasoning_effort (from litellm_params/proxy config) to reasoning when not set + if reasoning is None and "reasoning_effort" in local_vars: + _mapped = LiteLLMResponsesTransformationHandler()._map_reasoning_effort( + local_vars.pop("reasoning_effort") + ) + if _mapped is not None: + reasoning = _mapped + local_vars["reasoning"] = _mapped # Get ResponsesAPIOptionalRequestParams with only valid parameters response_api_optional_params: ResponsesAPIOptionalRequestParams = ( ResponsesAPIRequestUtils.get_requested_response_api_optional_param( diff --git a/litellm/router.py b/litellm/router.py index 4e268a94a2..da81196767 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -110,12 +110,12 @@ from litellm.router_utils.handle_error import ( async_raise_no_deployment_exception, send_llm_exception_alert, ) -from litellm.router_utils.pre_call_checks.model_rate_limit_check import ( - ModelRateLimitingCheck, -) from litellm.router_utils.pre_call_checks.deployment_affinity_check import ( DeploymentAffinityCheck, ) +from litellm.router_utils.pre_call_checks.model_rate_limit_check import ( + ModelRateLimitingCheck, +) from litellm.router_utils.pre_call_checks.prompt_caching_deployment_check import ( PromptCachingDeploymentCheck, ) @@ -1947,6 +1947,29 @@ class Router: ) # add new deployment to router return deployment_pydantic_obj + @staticmethod + def _merge_tools_from_deployment( + deployment: dict, kwargs: dict + ) -> None: + """ + Merge tools from deployment litellm_params with request kwargs. + When both have tools, concatenate them (deployment tools first, then request tools). + tool_choice: use request value if provided, else deployment's. + """ + dep_params = deployment.get("litellm_params", {}) or {} + dep_params = ( + dep_params.model_dump(exclude_none=True) + if hasattr(dep_params, "model_dump") + else dep_params + ) + dep_tools = dep_params.get("tools") or [] + req_tools = kwargs.get("tools") or [] + if dep_tools or req_tools: + merged = list(dep_tools) + list(req_tools) + kwargs["tools"] = merged + if "tool_choice" not in kwargs and dep_params.get("tool_choice") is not None: + kwargs["tool_choice"] = dep_params["tool_choice"] + def _update_kwargs_with_deployment( self, deployment: dict, @@ -1954,10 +1977,13 @@ class Router: function_name: Optional[str] = None, ) -> None: """ - 2 jobs: + 3 jobs: - Adds selected deployment, model_info and api_base to kwargs["metadata"] (used for logging) - Adds default litellm params to kwargs, if set. + - Merges tools from deployment with request (proxy-configured tools + request tools). """ + self._merge_tools_from_deployment(deployment=deployment, kwargs=kwargs) + model_info = deployment.get("model_info", {}).copy() deployment_litellm_model_name = deployment["litellm_params"]["model"] deployment_api_base = deployment["litellm_params"].get("api_base") From 5d8f2196d10fa75206302c65fc5503ae3a27b3fb Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 20 Feb 2026 16:22:44 +0530 Subject: [PATCH 2/2] Add test case --- .../responses/test_responses_utils.py | 32 +++++ tests/test_litellm/test_router.py | 117 +++++++++++++++++- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index 7feab9c603..c6f32b6d75 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -384,3 +384,35 @@ def test_responses_extra_body_forwarded_to_completion_transformation_handler(): assert call_kwargs.kwargs.get("extra_body") == { "custom_key": "custom_value" } + + +def test_responses_maps_reasoning_effort_from_litellm_params_to_reasoning(): + """ + Test that when reasoning_effort is passed in kwargs (e.g. from proxy litellm_params) + and reasoning is None, it is mapped to reasoning before the request. + + Supports per-model reasoning_effort/summary config in proxy for clients like Open WebUI + that cannot set extra_body. + """ + with patch( + "litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config", + return_value=None, + ), patch( + "litellm.responses.main.litellm_completion_transformation_handler.response_api_handler", + ) as mock_handler: + mock_handler.return_value = MagicMock() + + litellm.responses( + model="openai/gpt-4o", + input="Hello", + reasoning_effort={"effort": "high", "summary": "detailed"}, + ) + + mock_handler.assert_called_once() + call_kwargs = mock_handler.call_args + responses_api_request = call_kwargs.kwargs.get("responses_api_request", {}) + assert "reasoning" in responses_api_request + assert responses_api_request["reasoning"] == { + "effort": "high", + "summary": "detailed", + } diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 9dcb16b545..c55b26ca39 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -1877,19 +1877,20 @@ async def test_anthropic_messages_call_type_is_cached(): in PromptCachingDeploymentCheck.async_log_success_event. """ import asyncio + + from litellm.caching.dual_cache import DualCache from litellm.router_utils.pre_call_checks.prompt_caching_deployment_check import ( PromptCachingDeploymentCheck, ) from litellm.router_utils.prompt_caching_cache import PromptCachingCache - from litellm.caching.dual_cache import DualCache - from litellm.types.utils import CallTypes from litellm.types.utils import ( - StandardLoggingPayload, - StandardLoggingModelInformation, - StandardLoggingMetadata, + CallTypes, StandardLoggingHiddenParams, + StandardLoggingMetadata, + StandardLoggingModelInformation, + StandardLoggingPayload, ) - + # Create mock standard logging payload inline def create_standard_logging_payload() -> StandardLoggingPayload: return StandardLoggingPayload( @@ -2081,3 +2082,107 @@ def test_update_kwargs_with_deployment_no_tags(): # No tags key should be added if deployment has no tags assert "tags" not in kwargs["metadata"] + + +def test_update_kwargs_with_deployment_merges_tools(): + """ + Test that when both deployment litellm_params and request have tools, + they are merged (deployment tools first, then request tools). + + Supports proxy-configured tools (e.g. for o3 deep research) merged with + client-provided tools. + """ + router = litellm.Router( + model_list=[ + { + "model_name": "o3-deep-research", + "litellm_params": { + "model": "openai/o3-deep-research", + "api_key": "fake-key", + "tools": [{"type": "web_search"}], + "tool_choice": "auto", + }, + }, + ], + ) + + kwargs: dict = { + "metadata": {}, + "tools": [ + { + "type": "function", + "function": {"name": "get_weather", "description": "Get weather"}, + }, + ], + } + deployment = router.get_deployment_by_model_group_name( + model_group_name="o3-deep-research" + ) + router._update_kwargs_with_deployment(deployment=deployment, kwargs=kwargs) + + # Tools should be merged: deployment first, then request + assert "tools" in kwargs + assert len(kwargs["tools"]) == 2 + assert kwargs["tools"][0] == {"type": "web_search"} + assert kwargs["tools"][1]["function"]["name"] == "get_weather" + # tool_choice from request (none) - deployment's should be used + assert kwargs["tool_choice"] == "auto" + + +def test_update_kwargs_with_deployment_merge_tools_deployment_only(): + """ + Test that when only deployment has tools, they are applied to kwargs. + """ + router = litellm.Router( + model_list=[ + { + "model_name": "o3-deep-research", + "litellm_params": { + "model": "openai/o3-deep-research", + "api_key": "fake-key", + "tools": [{"type": "web_search"}], + "tool_choice": "required", + }, + }, + ], + ) + + kwargs: dict = {"metadata": {}} + deployment = router.get_deployment_by_model_group_name( + model_group_name="o3-deep-research" + ) + router._update_kwargs_with_deployment(deployment=deployment, kwargs=kwargs) + + assert kwargs["tools"] == [{"type": "web_search"}] + assert kwargs["tool_choice"] == "required" + + +def test_update_kwargs_with_deployment_merge_tools_request_overrides_tool_choice(): + """ + Test that when request has tool_choice, it overrides deployment's. + """ + router = litellm.Router( + model_list=[ + { + "model_name": "o3-deep-research", + "litellm_params": { + "model": "openai/o3-deep-research", + "api_key": "fake-key", + "tools": [{"type": "web_search"}], + "tool_choice": "auto", + }, + }, + ], + ) + + kwargs: dict = { + "metadata": {}, + "tool_choice": "none", + } + deployment = router.get_deployment_by_model_group_name( + model_group_name="o3-deep-research" + ) + router._update_kwargs_with_deployment(deployment=deployment, kwargs=kwargs) + + # Request tool_choice should be preserved (merged tools still applied) + assert kwargs["tool_choice"] == "none"