fix(anthropic): send reasoning effort none wherever the Responses transformation accepts it

An openai/ deployment the cost map does not list now gets effort none for
disabled thinking on the chat route even when its model_info sets a mode,
since the off level now asks whether the loaded cost map lists the model
instead of reading the row the router registers. On the Responses bridge
the off switch is dropped only where the Responses transformation itself
would refuse reasoning.effort, so a slash-named model it accepts gets none
again for adaptive effort none and disabled thinking.
This commit is contained in:
mateo-berri 2026-11-03 10:00:15 -08:00
parent 927601ac0f
commit 476755238f
4 changed files with 50 additions and 16 deletions

View file

@ -27,6 +27,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
)
from litellm.litellm_core_utils.reasoning_effort_capability import deployment_rounds_reasoning_effort
from litellm.litellm_core_utils.tool_fields import drops_mcp_servers, tools_without_mcp_servers
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.responses.utils import auto_reasoning_summary
from litellm.types.integrations.custom_logger import CONVERTED_STREAM_KEYS
from litellm.types.llms.anthropic import (
@ -41,7 +42,7 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
AnthropicMessagesResponse,
)
from litellm.types.llms.openai import REASONING_EFFORT, ResponsesAPIResponse
from litellm.types.utils import CountContextManagement
from litellm.types.utils import CountContextManagement, LlmProviders
from litellm.utils import ProviderConfigManager
from ..adapters.handler import context_management_explicitly_dropped, request_safeguards_evaluator
@ -52,7 +53,6 @@ from ..convertible_blocks import reject_malformed_tool_result_text
from ..safeguards import with_safeguard_results
from ..utils import (
REASONING_EFFORT_OFF,
cost_map_row_lacks_reasoning,
litellm_logging_obj_from_kwargs,
local_model_name,
maps_bare_output_effort,
@ -277,7 +277,7 @@ def _reasoning_for_target(
return reasoning
if effort == REASONING_EFFORT_OFF:
off_level: Final = reasoning_off_level(model, custom_llm_provider, drop_params=drop_params)
if off_level is None or cost_map_row_lacks_reasoning(model, custom_llm_provider):
if off_level is None or _responses_api_refuses_reasoning_effort(model, custom_llm_provider):
return None
return {**reasoning_params, "effort": off_level}
if drop_params and effort not in _KNOWN_REASONING_EFFORTS:
@ -305,6 +305,12 @@ def _reasoning_for_target(
return reasoning if normalized == effort else {**reasoning_params, "effort": normalized}
def _responses_api_refuses_reasoning_effort(model: str, custom_llm_provider: str | None) -> bool:
return custom_llm_provider == LlmProviders.OPENAI.value and not OpenAIResponsesAPIConfig.supports_reasoning_param(
model
)
def _model_excludes_effort(
effort: str, *, model: str, custom_llm_provider: str | None, deployment_model_info: object
) -> bool:

View file

@ -92,7 +92,6 @@ _OBJECT_LIST: Final = TypeAdapter(list[object])
_NO_SETTINGS: Final[Mapping[str, object]] = MappingProxyType({})
_NO_OPERATOR_MODEL_INFO: Final[Mapping[str, object]] = MappingProxyType({})
def proxy_general_settings() -> Mapping[str, object]:
@ -243,16 +242,14 @@ def _mapped_model_info(model: str, custom_llm_provider: str | None) -> ModelInfo
def model_reasoning_efforts(model: str, custom_llm_provider: str | None) -> tuple[str, ...] | None:
from litellm.litellm_core_utils.reasoning_effort_capability import (
deployment_is_catalog_mapped,
resolve_supported_reasoning_efforts,
)
from litellm.litellm_core_utils.reasoning_effort_capability import resolve_supported_reasoning_efforts
from litellm.utils import has_catalog_entry
model_info: Final = _mapped_model_info(model, custom_llm_provider)
if model_info is None:
return None
return resolve_supported_reasoning_efforts(
model_info, deployment_is_mapped=deployment_is_catalog_mapped(model_info, _NO_OPERATOR_MODEL_INFO)
model_info, deployment_is_mapped=has_catalog_entry(model, custom_llm_provider)
)
@ -262,11 +259,6 @@ def _lowest_tier_instead_of_off(levels: tuple[str, ...] | None) -> str | None:
return next((tier for tier in _LOWEST_REASONING_TIERS if tier in levels), None)
def cost_map_row_lacks_reasoning(model: str, custom_llm_provider: str | None) -> bool:
model_info: Final = _mapped_model_info(model, custom_llm_provider)
return model_info is not None and not model_info.get("supports_reasoning")
def reasoning_off_level(model: str, custom_llm_provider: str | None, *, drop_params: bool) -> str | None:
from litellm.utils import supports_none_reasoning_effort, supports_reasoning

View file

@ -405,7 +405,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
return OpenAIGPT5Config.effort_resolves_to_none(model, effort)
@staticmethod
def _supports_reasoning_param(model: str) -> bool:
def supports_reasoning_param(model: str) -> bool:
from litellm.utils import get_model_info_helper
try:
@ -503,7 +503,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
if (
self.custom_llm_provider == LlmProviders.OPENAI
and self._requests_reasoning_effort(params.get("reasoning"))
and not self._supports_reasoning_param(model=model)
and not self.supports_reasoning_param(model=model)
):
if drop_params or litellm.drop_params:
dropped_reasoning: Final = params.pop("reasoning", None)

View file

@ -781,3 +781,39 @@ class TestDisabledOnAnOffMapOpenAIDeployment:
)
assert "reasoning" not in sent
@pytest.mark.asyncio
async def test_the_chat_route_sends_none_when_the_operator_sets_the_deployment_mode(
self, respx_mock: respx.MockRouter, local_model_cost_map: None
) -> None:
sent: Final = await _body_sent_upstream(
respx_mock,
{**_OFF_MAP_OPENAI_DEPLOYMENT, "model": "openai/vendor/off-map-hybrid-reasoner"},
{"mode": "responses"},
r"https://compatible\.example\.test/v1/responses",
_RESPONSE,
thinking={"type": "disabled"},
stop_sequences=["STOP"],
)
assert sent["reasoning"]["effort"] == "none"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"messages_params",
[{"thinking": {"type": "disabled"}}, {"thinking": {"type": "adaptive"}, "output_config": {"effort": "none"}}],
ids=["disabled", "adaptive-effort-none"],
)
async def test_the_responses_bridge_sends_none_where_its_transformation_accepts_it(
self, respx_mock: respx.MockRouter, local_model_cost_map: None, messages_params: Mapping[str, object]
) -> None:
sent: Final = await _body_sent_upstream(
respx_mock,
{**_OFF_MAP_OPENAI_DEPLOYMENT, "model": "openai/vendor/off-map-hybrid-reasoner"},
{},
r"https://compatible\.example\.test/v1/responses",
_RESPONSE,
**messages_params,
)
assert sent["reasoning"] == {"effort": "none"}