fix(anthropic): keep sending reasoning_effort none on the chat route to an openai/ deployment the cost map does not list

The router registers an unlisted deployment as a stub row with no mode, which made its reasoning levels read as known-empty, so thinking disabled stopped sending none on the chat-completions route. The levels now read as unknown for such a row. The Responses bridge keeps sending no reasoning there, since the Responses transformation answers 400 to reasoning.effort on a row without supports_reasoning.
This commit is contained in:
mateo-berri 2026-11-03 07:50:22 -08:00
parent 9dd5e9a008
commit 927601ac0f
3 changed files with 61 additions and 3 deletions

View file

@ -52,6 +52,7 @@ 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,
@ -276,7 +277,9 @@ 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)
return None if off_level is None else {**reasoning_params, "effort": off_level}
if off_level is None or cost_map_row_lacks_reasoning(model, custom_llm_provider):
return None
return {**reasoning_params, "effort": off_level}
if drop_params and effort not in _KNOWN_REASONING_EFFORTS:
verbose_logger.warning(
"Dropping reasoning.effort=%r for %s, no reasoning API defines it, because drop_params is set",

View file

@ -92,6 +92,7 @@ _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]:
@ -242,12 +243,17 @@ 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 resolve_supported_reasoning_efforts
from litellm.litellm_core_utils.reasoning_effort_capability import (
deployment_is_catalog_mapped,
resolve_supported_reasoning_efforts,
)
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=True)
return resolve_supported_reasoning_efforts(
model_info, deployment_is_mapped=deployment_is_catalog_mapped(model_info, _NO_OPERATOR_MODEL_INFO)
)
def _lowest_tier_instead_of_off(levels: tuple[str, ...] | None) -> str | None:
@ -256,6 +262,11 @@ 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

@ -737,3 +737,47 @@ class TestAnOpenAIChatRouteRequestGetsTheResponsesBridgeThinkingControls:
)
assert "reasoning_effort" not in sent
_OFF_MAP_OPENAI_DEPLOYMENT: Final = {
"model": "openai/off-map-hybrid-reasoner",
"api_base": "https://compatible.example.test/v1",
"api_key": "fake-key",
}
class TestDisabledOnAnOffMapOpenAIDeployment:
@pytest.fixture(autouse=True)
def _httpx_transport(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
@pytest.mark.asyncio
async def test_the_chat_route_sends_reasoning_effort_none(
self, respx_mock: respx.MockRouter, local_model_cost_map: None
) -> None:
sent: Final = await _body_sent_upstream(
respx_mock,
_OFF_MAP_OPENAI_DEPLOYMENT,
{},
r"https://compatible\.example\.test/v1/chat/completions",
_CHAT_COMPLETION,
thinking={"type": "disabled"},
stop_sequences=["STOP"],
)
assert sent["reasoning_effort"] == "none"
@pytest.mark.asyncio
async def test_the_responses_bridge_sends_no_reasoning_its_transformation_would_refuse(
self, respx_mock: respx.MockRouter, local_model_cost_map: None
) -> None:
sent: Final = await _body_sent_upstream(
respx_mock,
_OFF_MAP_OPENAI_DEPLOYMENT,
{},
r"https://compatible\.example\.test/v1/responses",
_RESPONSE,
thinking={"type": "disabled"},
)
assert "reasoning" not in sent