Skip to content
Closed
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
135 changes: 98 additions & 37 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ def _is_claude_opus_4_5(self, model: str) -> bool:
"""Check if the model is Claude Opus 4.5."""
return "opus-4-5" in model.lower() or "opus_4_5" in model.lower()

def _is_claude_opus_4_6(self, model: str) -> bool:
"""Check if the model is Claude Opus 4.6."""
model_lower = model.lower()
return any(
p in model_lower for p in ["opus-4-6", "opus_4_6", "opus-4.6", "opus_4.6"]
)

def get_supported_openai_params(self, model: str):
params = [
"stream",
Expand Down Expand Up @@ -204,20 +211,20 @@ def get_supported_openai_params(self, model: str):
def filter_anthropic_output_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
"""
Filter out unsupported fields from JSON schema for Anthropic's output_format API.

Anthropic's output_format doesn't support certain JSON schema properties:
- maxItems: Not supported for array types
- minItems: Not supported for array types

This function recursively removes these unsupported fields while preserving
all other valid schema properties.

Args:
schema: The JSON schema dictionary to filter

Returns:
A new dictionary with unsupported fields removed

Related issue: https://github.com/BerriAI/litellm/issues/19444
"""
if not isinstance(schema, dict):
Expand Down Expand Up @@ -686,6 +693,19 @@ def _map_reasoning_effort(
else:
raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}")

@staticmethod
def _get_adaptive_thinking_param() -> AnthropicThinkingParam:
return AnthropicThinkingParam(type="adaptive")

@staticmethod
def _budget_tokens_to_effort(budget_tokens: int) -> str:
if budget_tokens <= DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET:
return "low"
elif budget_tokens <= DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET:
return "medium"
else:
return "high"

def _extract_json_schema_from_response_format(
self, value: Optional[dict]
) -> Optional[dict]:
Expand All @@ -707,10 +727,10 @@ def map_response_format_to_anthropic_output_format(
)
if json_schema is None:
return None

# Filter out unsupported fields for Anthropic's output_format API
filtered_schema = self.filter_anthropic_output_schema(json_schema)

return AnthropicOutputSchema(
type="json_schema",
schema=filtered_schema,
Expand Down Expand Up @@ -799,11 +819,11 @@ def map_openai_params( # noqa: PLR0915
if mcp_servers:
optional_params["mcp_servers"] = mcp_servers
if param == "tool_choice" or param == "parallel_tool_calls":
_tool_choice: Optional[AnthropicMessagesToolChoice] = (
self._map_tool_choice(
tool_choice=non_default_params.get("tool_choice"),
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
)
_tool_choice: Optional[
AnthropicMessagesToolChoice
] = self._map_tool_choice(
tool_choice=non_default_params.get("tool_choice"),
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
)

if _tool_choice is not None:
Expand All @@ -826,6 +846,8 @@ def map_openai_params( # noqa: PLR0915
"sonnet-4-5",
"opus-4.1",
"opus-4-1",
"opus-4.6",
"opus-4-6",
}
):
_output_format = (
Expand Down Expand Up @@ -858,16 +880,42 @@ def map_openai_params( # noqa: PLR0915
):
optional_params["metadata"] = {"user_id": value}
if param == "thinking":
optional_params["thinking"] = value
# For Opus 4.6, convert deprecated type="enabled" to adaptive
if self._is_claude_opus_4_6(model) and isinstance(value, dict):
if value.get("type") == "enabled":
optional_params[
"thinking"
] = AnthropicConfig._get_adaptive_thinking_param()
budget = value.get("budget_tokens")
if budget is not None:
optional_params["output_config"] = {
"effort": AnthropicConfig._budget_tokens_to_effort(
budget
)
}
else:
# type="adaptive" passed directly -- use as-is
optional_params["thinking"] = value
else:
optional_params["thinking"] = value
elif param == "reasoning_effort" and isinstance(value, str):
# For Claude Opus 4.5, map reasoning_effort to output_config
if self._is_claude_opus_4_5(model):
if self._is_claude_opus_4_6(model):
optional_params[
"thinking"
] = AnthropicConfig._get_adaptive_thinking_param()
effort_value = (
value if value in ("low", "medium", "high") else "low"
)
optional_params["output_config"] = {"effort": effort_value}
elif self._is_claude_opus_4_5(model):
optional_params["output_config"] = {"effort": value}

# For other models, map to thinking parameter
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort(
value
)
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort(
value
)
else:
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort(
value
)
elif param == "web_search_options" and isinstance(value, dict):
hosted_web_search_tool = self.map_web_search_tool(
cast(OpenAIWebSearchOptions, value)
Expand Down Expand Up @@ -938,9 +986,9 @@ def translate_system_message(
text=system_message_block["content"],
)
if "cache_control" in system_message_block:
anthropic_system_message_content["cache_control"] = (
system_message_block["cache_control"]
)
anthropic_system_message_content[
"cache_control"
] = system_message_block["cache_control"]
anthropic_system_message_list.append(
anthropic_system_message_content
)
Expand All @@ -958,9 +1006,9 @@ def translate_system_message(
)
)
if "cache_control" in _content:
anthropic_system_message_content["cache_control"] = (
_content["cache_control"]
)
anthropic_system_message_content[
"cache_control"
] = _content["cache_control"]

anthropic_system_message_list.append(
anthropic_system_message_content
Expand Down Expand Up @@ -1013,7 +1061,7 @@ def _ensure_beta_header(self, headers: dict, beta_value: str) -> None:
"""
Ensure a beta header value is present in the anthropic-beta header.
Merges with existing values instead of overriding them.

Args:
headers: Dictionary of headers to update
beta_value: The beta header value to add
Expand All @@ -1034,7 +1082,7 @@ def update_headers_with_optional_anthropic_beta(
self, headers: dict, optional_params: dict
) -> dict:
"""Update headers with optional anthropic beta."""

# Skip adding beta headers for Vertex requests
# Vertex AI handles these headers differently
is_vertex_request = optional_params.get("is_vertex_request", False)
Expand All @@ -1053,7 +1101,8 @@ def update_headers_with_optional_anthropic_beta(
ANTHROPIC_HOSTED_TOOLS.MEMORY.value
):
self._ensure_beta_header(
headers, ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value
headers,
ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value,
)
if optional_params.get("context_management") is not None:
self._ensure_context_management_beta_header(headers)
Expand Down Expand Up @@ -1213,7 +1262,9 @@ def _transform_response_for_json_mode(
)
return _message

def extract_response_content(self, completion_response: dict) -> Tuple[
def extract_response_content(
self, completion_response: dict
) -> Tuple[
str,
Optional[List[Any]],
Optional[
Expand Down Expand Up @@ -1261,7 +1312,7 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
elif content["type"] == "web_fetch_tool_result":
if web_search_results is None:
web_search_results = []
web_search_results.append(content)
web_search_results.append(content)
else:
# All other tool results (bash_code_execution_tool_result, text_editor_code_execution_tool_result, etc.)
if tool_results is None:
Expand Down Expand Up @@ -1299,7 +1350,15 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
if thinking_content is not None:
reasoning_content += thinking_content

return text_content, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results
return (
text_content,
citations,
thinking_blocks,
reasoning_content,
tool_calls,
web_search_results,
tool_results,
)

def calculate_usage(
self,
Expand Down Expand Up @@ -1379,7 +1438,9 @@ def calculate_usage(
)
completion_token_details = CompletionTokensDetailsWrapper(
reasoning_tokens=reasoning_tokens if reasoning_tokens > 0 else 0,
text_tokens=completion_tokens - reasoning_tokens if reasoning_tokens > 0 else completion_tokens,
text_tokens=completion_tokens - reasoning_tokens
if reasoning_tokens > 0
else completion_tokens,
)
total_tokens = prompt_tokens + completion_tokens

Expand Down Expand Up @@ -1469,7 +1530,7 @@ def transform_parsed_response(
provider_specific_fields["tool_results"] = tool_results
if container is not None:
provider_specific_fields["container"] = container

_message = litellm.Message(
tool_calls=tool_calls,
content=text_content or None,
Expand Down Expand Up @@ -1511,9 +1572,9 @@ def transform_parsed_response(
if context_management_response is not None:
_hidden_params["context_management"] = context_management_response
try:
model_response.__dict__["context_management"] = (
context_management_response
)
model_response.__dict__[
"context_management"
] = context_management_response
except Exception:
pass

Expand Down
Loading
Loading