feat: add force_reasoning_effort litellm_params config

Add force_reasoning_effort as a per-deployment litellm_params field
that overrides reasoning_effort regardless of what the request sends.
Follows the same pattern as merge_reasoning_content_in_choices:
declared in GenericLiteLLMParams, captured in get_litellm_params(),
consumed downstream.

Override points for each entry path:
- /v1/chat/completions: main.py reads from litellm_params after
  get_litellm_params(), overrides optional_params["reasoning_effort"]
- /v1/messages → responses adapter: _build_responses_kwargs() pops
  from extra_kwargs, overrides reasoning.effort in responses format
- /v1/responses direct: responses/main.py pops from local_vars,
  overrides reasoning dict

Config example:
  litellm_params:
    model: github_copilot/gpt-5.4
    force_reasoning_effort: xhigh

Tested: all three endpoints with explicit low effort in request,
all overridden to xhigh with high reasoning_tokens in response.
This commit is contained in:
Vigilans 2026-04-16 10:51:09 +08:00
parent 2f22a1293e
commit c0ee4ec695
6 changed files with 43 additions and 1 deletions

View file

@ -90,6 +90,7 @@ def get_litellm_params(
async_call: Optional[bool] = None,
ssl_verify: Optional[bool] = None,
merge_reasoning_content_in_choices: Optional[bool] = None,
force_reasoning_effort: Optional[str] = None,
use_litellm_proxy: Optional[bool] = None,
api_version: Optional[str] = None,
max_retries: Optional[int] = None,
@ -149,6 +150,7 @@ def get_litellm_params(
"async_call": async_call,
"ssl_verify": ssl_verify,
"merge_reasoning_content_in_choices": merge_reasoning_content_in_choices,
"force_reasoning_effort": force_reasoning_effort,
"api_version": api_version,
"max_retries": max_retries,
"use_litellm_proxy": use_litellm_proxy,

View file

@ -441,6 +441,18 @@ def anthropic_messages_handler(
params=local_vars
)
)
force_reasoning_effort = litellm_params.force_reasoning_effort
if force_reasoning_effort:
output_config = anthropic_messages_optional_request_params.get("output_config") or {}
anthropic_messages_optional_request_params["output_config"] = {
**output_config,
"effort": force_reasoning_effort,
}
# Effort alone does not trigger reasoning — force-enable thinking too
thinking = anthropic_messages_optional_request_params.get("thinking")
if not isinstance(thinking, dict) or thinking.get("type") == "disabled":
anthropic_messages_optional_request_params["thinking"] = {"type": "adaptive"}
return base_llm_http_handler.anthropic_messages_handler(
model=model,
messages=messages,

View file

@ -15,6 +15,7 @@ from litellm.types.llms.openai import ResponsesAPIResponse
from .streaming_iterator import AnthropicResponsesStreamWrapper
from .transformation import LiteLLMAnthropicToResponsesAPIAdapter
from ..utils import is_reasoning_auto_summary_enabled
_ADAPTER = LiteLLMAnthropicToResponsesAPIAdapter()
@ -76,7 +77,8 @@ def _build_responses_kwargs(
responses_kwargs["stream"] = True
# Forward litellm-specific kwargs (api_key, api_base, logging obj, etc.)
excluded = {"anthropic_messages"}
excluded = {"anthropic_messages", "force_reasoning_effort"}
force_reasoning_effort = (extra_kwargs or {}).get("force_reasoning_effort")
for key, value in (extra_kwargs or {}).items():
if key == "litellm_logging_obj" and value is not None:
from litellm.litellm_core_utils.litellm_logging import (
@ -93,6 +95,17 @@ def _build_responses_kwargs(
elif key not in excluded and key not in responses_kwargs and value is not None:
responses_kwargs[key] = value
# Override reasoning effort if force_reasoning_effort is configured
if force_reasoning_effort:
reasoning = responses_kwargs.get("reasoning")
if isinstance(reasoning, dict):
responses_kwargs["reasoning"] = {**reasoning, "effort": force_reasoning_effort}
else:
responses_kwargs["reasoning"] = {
**({"summary": "detailed"} if is_reasoning_auto_summary_enabled() else {}),
"effort": force_reasoning_effort,
}
return responses_kwargs

View file

@ -1577,6 +1577,7 @@ def completion( # type: ignore # noqa: PLR0915
merge_reasoning_content_in_choices=kwargs.get(
"merge_reasoning_content_in_choices", None
),
force_reasoning_effort=kwargs.get("force_reasoning_effort"),
use_litellm_proxy=kwargs.get("use_litellm_proxy", False),
api_version=api_version,
azure_ad_token=kwargs.get("azure_ad_token"),
@ -1592,6 +1593,11 @@ def completion( # type: ignore # noqa: PLR0915
tpm=kwargs.get("tpm"),
rpm=kwargs.get("rpm"),
)
# Override reasoning_effort if force_reasoning_effort is configured
force_reasoning_effort = litellm_params.get("force_reasoning_effort")
if force_reasoning_effort:
optional_params["reasoning_effort"] = force_reasoning_effort
cast(LiteLLMLoggingObj, logging).update_environment_variables(
model=model,
user=user,

View file

@ -862,6 +862,14 @@ def responses(
if _mapped is not None:
reasoning = _mapped
local_vars["reasoning"] = _mapped
# Override reasoning effort if force_reasoning_effort is configured
force_reasoning_effort = local_vars.pop("force_reasoning_effort", None)
if force_reasoning_effort:
if isinstance(reasoning, dict):
reasoning = {**reasoning, "effort": force_reasoning_effort}
else:
reasoning = {"effort": force_reasoning_effort}
local_vars["reasoning"] = reasoning
# Get ResponsesAPIOptionalRequestParams with only valid parameters
response_api_optional_params: ResponsesAPIOptionalRequestParams = (
ResponsesAPIRequestUtils.get_requested_response_api_optional_param(

View file

@ -203,6 +203,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
use_litellm_proxy: Optional[bool] = False
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
merge_reasoning_content_in_choices: Optional[bool] = False
force_reasoning_effort: Optional[str] = None
model_info: Optional[Dict] = None
mock_response: Optional[Union[str, ModelResponse, Exception, Any]] = None