Merge pull request #21663 from BerriAI/litellm_add_reasoning_support_config

[Feat] Add reasoning support via config
This commit is contained in:
Sameer Kankute
2026-02-20 18:17:09 +05:30
committed by GitHub
5 changed files with 203 additions and 10 deletions
+19
View File
@@ -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"}
```
</TabItem>
</Tabs>
+11
View File
@@ -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(
+30 -4
View File
@@ -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")
@@ -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",
}
+111 -6
View File
@@ -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"