From 138c77023a4b4b0a112f6f6f737b3fe33f16148c Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:44:31 -0700 Subject: [PATCH 1/2] fix: accept bool thinking param instead of crashing with AttributeError litellm.completion(thinking=True) crashed pre-network in is_thinking_enabled with a retryable APIConnectionError ('bool' object has no attribute 'get'), so the router burned retries on a deterministic failure and proxy clients got a traceback instead of a usable response. validate_and_fix_thinking_param now coerces thinking=True to the enabled dict with the default medium budget and drops thinking=False, and the remaining dict-assuming thinking accessors (base config, bedrock converse, deepseek) guard with isinstance so raw bools can never crash a transform. --- litellm/llms/base_llm/chat/transformation.py | 13 ++++++++----- .../llms/bedrock/chat/converse_transformation.py | 5 ++++- litellm/llms/deepseek/chat/transformation.py | 4 +++- litellm/main.py | 1 - litellm/utils.py | 13 +++++++++++-- .../bedrock/chat/test_converse_transformation.py | 7 +++++++ .../chat/test_deepseek_chat_transformation.py | 5 +++++ tests/test_litellm/test_thinking_enabled.py | 2 ++ tests/test_litellm/test_utils.py | 14 ++++++++++++++ 9 files changed, 54 insertions(+), 10 deletions(-) diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index 0d6d942e686..d147063df73 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -5,7 +5,7 @@ Common base config for all LLM providers import types from abc import ABC, abstractmethod from collections.abc import AsyncIterator, Iterator -from typing import TYPE_CHECKING, Any, Final, Union, cast +from typing import TYPE_CHECKING, Any, Final, Union import httpx from pydantic import BaseModel @@ -90,9 +90,9 @@ class BaseConfig(ABC): return type_to_response_format_param(response_format=response_format) def is_thinking_enabled(self, non_default_params: dict) -> bool: - return (non_default_params.get("thinking") or {}).get("type") == "enabled" or non_default_params.get( - "reasoning_effort" - ) is not None + thinking: Final = non_default_params.get("thinking") + thinking_type: Final = thinking.get("type") if isinstance(thinking, dict) else None + return thinking is True or thinking_type == "enabled" or non_default_params.get("reasoning_effort") is not None def is_max_tokens_in_request(self, non_default_params: dict) -> bool: """ @@ -112,7 +112,10 @@ class BaseConfig(ABC): if is_thinking_enabled and ( "max_tokens" not in non_default_params and "max_completion_tokens" not in non_default_params ): - thinking_token_budget: Final = cast(dict, optional_params["thinking"]).get("budget_tokens", None) + thinking_value: Final = optional_params.get("thinking") + thinking_token_budget: Final = ( + thinking_value.get("budget_tokens") if isinstance(thinking_value, dict) else None + ) if thinking_token_budget is not None: optional_params["max_tokens"] = thinking_token_budget + DEFAULT_MAX_TOKENS diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index fd07999395b..cee89f42c2d 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1090,7 +1090,10 @@ class AmazonConverseConfig(BaseConfig): is_thinking_enabled: Final = self.is_thinking_enabled(optional_params) is_max_tokens_in_request: Final = self.is_max_tokens_in_request(non_default_params) if is_thinking_enabled and not is_max_tokens_in_request: - thinking_token_budget: Final = cast(dict, optional_params["thinking"]).get("budget_tokens", None) + thinking_value: Final = optional_params.get("thinking") + thinking_token_budget: Final = ( + thinking_value.get("budget_tokens") if isinstance(thinking_value, dict) else None + ) if thinking_token_budget is not None: optional_params["maxTokens"] = thinking_token_budget + DEFAULT_MAX_TOKENS diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 24da5b79261..566c960333a 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -131,9 +131,11 @@ class DeepSeekChatConfig(OpenAIGPTConfig): - model supports reasoning (capability check) - user explicitly passed thinking={"type": "enabled"} (opt-in check) """ + thinking: Final = optional_params.get("thinking") return ( supports_reasoning(model=model, custom_llm_provider="deepseek") - and (optional_params.get("thinking") or {}).get("type") == "enabled" + and isinstance(thinking, dict) + and thinking.get("type") == "enabled" ) @staticmethod diff --git a/litellm/main.py b/litellm/main.py index cc27da830d8..f0b20eba9b6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -5007,7 +5007,6 @@ def completion( tool_choice = validate_chat_completion_tool_choice(tool_choice=tool_choice) # validate optional params stop = validate_openai_optional_params(stop=stop) - # normalize camelCase thinking keys (e.g. budgetTokens -> budget_tokens) thinking = validate_and_fix_thinking_param(thinking=thinking) ######### unpacking kwargs ##################### diff --git a/litellm/utils.py b/litellm/utils.py index 1c880ee9521..a7b70c4129a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -65,6 +65,7 @@ from litellm.constants import ( DEFAULT_EMBEDDING_PARAM_VALUES, DEFAULT_MAX_LRU_CACHE_SIZE, DEFAULT_MINIMUM_PROMPT_CACHE_TOKEN_COUNT, + DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, DEFAULT_TRIM_RATIO, FUNCTION_DEFINITION_TOKEN_COUNT, INITIAL_RETRY_DELAY, @@ -7638,12 +7639,20 @@ def validate_and_fix_openai_tools(tools: list | None) -> list[dict] | None: def validate_and_fix_thinking_param( - thinking: AnthropicThinkingParam | None, + thinking: AnthropicThinkingParam | bool | None, ) -> AnthropicThinkingParam | None: """ - Normalizes camelCase keys in the thinking param to snake_case. + Coerces bool thinking values (True becomes enabled with the default medium budget, False becomes None) + and normalizes camelCase keys in the thinking param to snake_case. Handles clients that send budgetTokens instead of budget_tokens. """ + if thinking is True: + return cast( + "AnthropicThinkingParam", + {"type": "enabled", "budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET}, + ) + if thinking is False: + return None if thinking is None or not isinstance(thinking, dict): return thinking normalized: Final = dict(thinking) diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 2509f6480d5..ff3d38eb374 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -6043,3 +6043,10 @@ def test_streaming_usage_chunk_is_transformed(): assert chunk.usage.prompt_tokens == 11 assert chunk.usage.completion_tokens == 4 assert chunk.usage.total_tokens == 15 + + +def test_update_optional_params_with_thinking_tokens_bool_thinking_does_not_crash(): + config = AmazonConverseConfig() + optional_params = {"thinking": True} + config.update_optional_params_with_thinking_tokens(non_default_params={"thinking": True}, optional_params=optional_params) + assert "maxTokens" not in optional_params diff --git a/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py b/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py index ec51e5d303d..d5783e3567f 100644 --- a/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py +++ b/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -101,3 +101,8 @@ async def test_async_transform_request_strips_unsupported_tools_from_body(): assert [tool["type"] for tool in body["tools"]] == ["function"] assert body["tools"][0]["function"]["name"] == "shell" + + +def test_thinking_mode_active_bool_thinking_returns_false_without_crashing(): + config = DeepSeekChatConfig() + assert config._thinking_mode_active(model="deepseek-reasoner", optional_params={"thinking": True}) is False diff --git a/tests/test_litellm/test_thinking_enabled.py b/tests/test_litellm/test_thinking_enabled.py index 8ba406c395a..38c4534e45c 100644 --- a/tests/test_litellm/test_thinking_enabled.py +++ b/tests/test_litellm/test_thinking_enabled.py @@ -60,6 +60,8 @@ class TestIsThinkingEnabled: ({"reasoning_effort": "medium"}, True), # both thinking enabled and reasoning_effort returns True ({"thinking": {"type": "enabled"}, "reasoning_effort": "high"}, True), + # thinking=True (bool) should not crash, returns True + ({"thinking": True}, True), # falsy thinking values should not crash ({"thinking": False}, False), ({"thinking": 0}, False), diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index afdfdf170ac..efccdc4a986 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -3766,6 +3766,20 @@ class TestValidateAndFixThinkingParam: assert "budgetTokens" in thinking assert "budget_tokens" not in thinking + def test_bool_true_maps_to_enabled_with_default_budget(self): + from litellm.constants import DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET + from litellm.utils import validate_and_fix_thinking_param + + assert validate_and_fix_thinking_param(thinking=True) == { + "type": "enabled", + "budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, + } + + def test_bool_false_returns_none(self): + from litellm.utils import validate_and_fix_thinking_param + + assert validate_and_fix_thinking_param(thinking=False) is None + def test_deepseek_v4_models_in_cost_map(): """ From 54cc988a9e5f3db851d7d372b23a2893f3896707 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:55:22 -0700 Subject: [PATCH 2/2] test: drop restating comment and wrap long call in thinking tests --- .../llms/bedrock/chat/test_converse_transformation.py | 4 +++- tests/test_litellm/test_thinking_enabled.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index ff3d38eb374..298360789eb 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -6048,5 +6048,7 @@ def test_streaming_usage_chunk_is_transformed(): def test_update_optional_params_with_thinking_tokens_bool_thinking_does_not_crash(): config = AmazonConverseConfig() optional_params = {"thinking": True} - config.update_optional_params_with_thinking_tokens(non_default_params={"thinking": True}, optional_params=optional_params) + config.update_optional_params_with_thinking_tokens( + non_default_params={"thinking": True}, optional_params=optional_params + ) assert "maxTokens" not in optional_params diff --git a/tests/test_litellm/test_thinking_enabled.py b/tests/test_litellm/test_thinking_enabled.py index 38c4534e45c..744b258e617 100644 --- a/tests/test_litellm/test_thinking_enabled.py +++ b/tests/test_litellm/test_thinking_enabled.py @@ -60,7 +60,6 @@ class TestIsThinkingEnabled: ({"reasoning_effort": "medium"}, True), # both thinking enabled and reasoning_effort returns True ({"thinking": {"type": "enabled"}, "reasoning_effort": "high"}, True), - # thinking=True (bool) should not crash, returns True ({"thinking": True}, True), # falsy thinking values should not crash ({"thinking": False}, False),