fix: fail-closed thinking default (Veria review finding)

_threaded thinking was fail-open for a malformed config: with thinking: {}
(missing type) the request side (translate
anthropic_thinking_to_reasoning_effort) defaulted to disabled, but the
response side still converted provider reasoning_content into a thinking
block, leaking internal reasoning. _is_thinking_disabled now suppresses
unless the client explicitly opted in (type in enabled/adaptive).

Truth-table tests extended to cover {}, {budget_tokens: 500} and an
unknown type (all -> disabled). Bot review comments answered.
This commit is contained in:
Daniel Cherubini 2026-09-15 09:45:36 +02:00
parent ddde64e210
commit d1ce467d04
2 changed files with 43 additions and 14 deletions

View file

@ -320,8 +320,17 @@ ANTHROPIC_ADAPTER: Final = AnthropicAdapter()
class LiteLLMMessagesToCompletionTransformationHandler:
@staticmethod
def _is_thinking_disabled(thinking: Mapping | None) -> bool:
"""Return True when the client's thinking param is absent or explicitly disabled."""
return thinking is None or (isinstance(thinking, dict) and thinking.get("type") == "disabled")
"""Return True (suppressed) unless the client explicitly opted in.
Only ``{"type": "enabled"|"adaptive"}`` enables the reasoning
translation. Absent, disabled, or malformed objects (missing
``type``) fail closed: the request side
(``translate_anthropic_thinking_to_reasoning_effort``) already
defaults a missing ``type`` to ``disabled``, and a malformed
object must not surface provider ``reasoning_content`` through
a thinking block (review finding).
"""
return not (isinstance(thinking, dict) and thinking.get("type") in ("enabled", "adaptive"))
@staticmethod
def _route_openai_thinking_to_responses_api_if_needed(

View file

@ -1,10 +1,11 @@
"""Handler-level tests for ``thinking_disabled`` computation and threading.
Covers the boolean logic that decides whether thinking is disabled
(``thinking is None or thinking.type == "disabled"``) and verifies it is
threaded correctly to ``ANTHROPIC_ADAPTER`` output-translation calls for
both the async and sync handler entry points, in streaming and non-streaming
modes.
(fail-closed: only an explicit ``{\"type\": \"enabled"|\"adaptive\"}``
enables it; absent, disabled, or malformed objects disable it) and
verifies it is threaded correctly to ``ANTHROPIC_ADAPTER`` output-
translation calls for both the async and sync handler entry points, in
streaming and non-streaming modes.
Mocks ``litellm.acompletion`` / ``litellm.completion`` and
``ANTHROPIC_ADAPTER`` directly, alongside the preparation helpers that run
@ -22,7 +23,10 @@ from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
THINKING_PARAMS = [
(None, True),
({}, True),
({"type": "disabled"}, True),
({"budget_tokens": 1024}, True),
({"type": "weird"}, True),
({"type": "enabled", "budget_tokens": 1024}, False),
({"type": "adaptive"}, False),
]
@ -52,8 +56,12 @@ async def test_async_handler_streaming_threads_thinking_disabled(thinking_param,
"_prepare_completion_kwargs",
return_value=({}, {}),
),
patch("litellm.acompletion", return_value=MagicMock()), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch("litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER") as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.acompletion", return_value=MagicMock()
), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER"
) as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
):
mock_adapter.translate_completion_output_params_streaming.return_value = iter([])
await LiteLLMMessagesToCompletionTransformationHandler.async_anthropic_messages_handler(
@ -91,8 +99,12 @@ async def test_async_handler_non_streaming_threads_thinking_disabled(thinking_pa
"_prepare_completion_kwargs",
return_value=({}, {}),
),
patch("litellm.acompletion", return_value=MagicMock()), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch("litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER") as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.acompletion", return_value=MagicMock()
), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER"
) as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
):
mock_adapter.translate_completion_output_params.return_value = MagicMock()
await LiteLLMMessagesToCompletionTransformationHandler.async_anthropic_messages_handler(
@ -130,8 +142,12 @@ def test_sync_handler_streaming_threads_thinking_disabled(thinking_param, expect
"_prepare_completion_kwargs",
return_value=({}, {}),
),
patch("litellm.completion", return_value=MagicMock()), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch("litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER") as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.completion", return_value=MagicMock()
), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER"
) as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
):
mock_adapter.translate_completion_output_params_streaming.return_value = iter([])
LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(
@ -169,8 +185,12 @@ def test_sync_handler_non_streaming_threads_thinking_disabled(thinking_param, ex
"_prepare_completion_kwargs",
return_value=({}, {}),
),
patch("litellm.completion", return_value=MagicMock()), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch("litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER") as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.completion", return_value=MagicMock()
), # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
patch( # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
"litellm.llms.anthropic.experimental_pass_through.adapters.handler.ANTHROPIC_ADAPTER"
) as mock_adapter, # test-quality-ok: handler unit test - fakes completion dispatch + adapter seams; unit under test is the thinking_disabled translation wiring, not the transport
):
mock_adapter.translate_completion_output_params.return_value = MagicMock()
LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(