Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
token_counter,
validate_and_fix_openai_messages,
validate_and_fix_openai_tools,
validate_and_fix_thinking_param,
validate_chat_completion_tool_choice,
validate_openai_optional_params,
)
Expand Down Expand Up @@ -1102,6 +1103,8 @@ def completion( # type: ignore # noqa: PLR0915
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 #####################
args = locals()
Expand Down
17 changes: 17 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7666,6 +7666,23 @@ def validate_and_fix_openai_tools(tools: Optional[List]) -> Optional[List[dict]]
return new_tools


def validate_and_fix_thinking_param(
thinking: Optional["AnthropicThinkingParam"],
) -> Optional["AnthropicThinkingParam"]:
"""
Normalizes camelCase keys in the thinking param to snake_case.
Handles clients that send budgetTokens instead of budget_tokens.
"""
if thinking is None or not isinstance(thinking, dict):
return thinking
normalized = dict(thinking)
if "budgetTokens" in normalized and "budget_tokens" not in normalized:
normalized["budget_tokens"] = normalized.pop("budgetTokens")
elif "budgetTokens" in normalized and "budget_tokens" in normalized:
normalized.pop("budgetTokens")
return cast("AnthropicThinkingParam", normalized)


def cleanup_none_field_in_message(message: AllMessageValues):
"""
Cleans up the message by removing the none field.
Expand Down
40 changes: 40 additions & 0 deletions tests/test_litellm/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3425,3 +3425,43 @@ def test_litellm_params_metadata_none(self):
litellm_params = {"metadata": None}
metadata = litellm_params.get("metadata") or {}
assert metadata == {}


class TestValidateAndFixThinkingParam:
"""Tests for validate_and_fix_thinking_param."""

def test_none_returns_none(self):
from litellm.utils import validate_and_fix_thinking_param

assert validate_and_fix_thinking_param(thinking=None) is None

def test_already_snake_case(self):
from litellm.utils import validate_and_fix_thinking_param

thinking = {"type": "enabled", "budget_tokens": 32000}
result = validate_and_fix_thinking_param(thinking=thinking)
assert result == {"type": "enabled", "budget_tokens": 32000}

def test_camel_case_normalized(self):
from litellm.utils import validate_and_fix_thinking_param

thinking = {"type": "enabled", "budgetTokens": 32000}
result = validate_and_fix_thinking_param(thinking=thinking)
assert result == {"type": "enabled", "budget_tokens": 32000}
assert "budgetTokens" not in result

def test_both_keys_snake_case_wins(self):
from litellm.utils import validate_and_fix_thinking_param

thinking = {"type": "enabled", "budget_tokens": 10000, "budgetTokens": 50000}
result = validate_and_fix_thinking_param(thinking=thinking)
assert result == {"type": "enabled", "budget_tokens": 10000}
assert "budgetTokens" not in result

def test_original_dict_not_mutated(self):
from litellm.utils import validate_and_fix_thinking_param

thinking = {"type": "enabled", "budgetTokens": 32000}
validate_and_fix_thinking_param(thinking=thinking)
assert "budgetTokens" in thinking
assert "budget_tokens" not in thinking
Loading