Merge pull request #37423 from BerriAI/litellm_fix_thinking_bool_crash

fix: accept bool thinking param instead of crashing with AttributeError
This commit is contained in:
Mateo Wang 2026-08-18 22:30:14 -07:00 committed by GitHub
commit 559310f077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 55 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 #####################

View file

@ -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)

View file

@ -6043,3 +6043,12 @@ 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

View file

@ -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

View file

@ -60,6 +60,7 @@ class TestIsThinkingEnabled:
({"reasoning_effort": "medium"}, True),
# both thinking enabled and reasoning_effort returns True
({"thinking": {"type": "enabled"}, "reasoning_effort": "high"}, True),
({"thinking": True}, True),
# falsy thinking values should not crash
({"thinking": False}, False),
({"thinking": 0}, False),

View file

@ -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():
"""