1922 lines
78 KiB
Python
1922 lines
78 KiB
Python
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import time
|
|||
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
|
|||
|
|
|
|||
|
|
import httpx
|
|||
|
|
|
|||
|
|
import litellm
|
|||
|
|
from litellm.constants import (
|
|||
|
|
ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES,
|
|||
|
|
DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS,
|
|||
|
|
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
|||
|
|
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
|
|||
|
|
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
|
|||
|
|
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
|
|||
|
|
RESPONSE_FORMAT_TOOL_NAME,
|
|||
|
|
)
|
|||
|
|
from litellm.litellm_core_utils.core_helpers import map_finish_reason
|
|||
|
|
from litellm.llms.base_llm.base_utils import type_to_response_format_param
|
|||
|
|
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
|||
|
|
from litellm.types.llms.anthropic import (
|
|||
|
|
ANTHROPIC_BETA_HEADER_VALUES,
|
|||
|
|
ANTHROPIC_HOSTED_TOOLS,
|
|||
|
|
AllAnthropicMessageValues,
|
|||
|
|
AllAnthropicToolsValues,
|
|||
|
|
AnthropicCodeExecutionTool,
|
|||
|
|
AnthropicComputerTool,
|
|||
|
|
AnthropicHostedTools,
|
|||
|
|
AnthropicInputSchema,
|
|||
|
|
AnthropicMcpServerTool,
|
|||
|
|
AnthropicMessagesTool,
|
|||
|
|
AnthropicMessagesToolChoice,
|
|||
|
|
AnthropicOutputSchema,
|
|||
|
|
AnthropicSystemMessageContent,
|
|||
|
|
AnthropicThinkingParam,
|
|||
|
|
AnthropicWebSearchTool,
|
|||
|
|
AnthropicWebSearchUserLocation,
|
|||
|
|
)
|
|||
|
|
from litellm.types.llms.openai import (
|
|||
|
|
REASONING_EFFORT,
|
|||
|
|
AllMessageValues,
|
|||
|
|
ChatCompletionCachedContent,
|
|||
|
|
ChatCompletionRedactedThinkingBlock,
|
|||
|
|
ChatCompletionSystemMessage,
|
|||
|
|
ChatCompletionThinkingBlock,
|
|||
|
|
ChatCompletionToolCallChunk,
|
|||
|
|
ChatCompletionToolCallFunctionChunk,
|
|||
|
|
ChatCompletionToolParam,
|
|||
|
|
OpenAIChatCompletionFinishReason,
|
|||
|
|
OpenAIMcpServerTool,
|
|||
|
|
OpenAIWebSearchOptions,
|
|||
|
|
)
|
|||
|
|
from litellm.types.utils import (
|
|||
|
|
CacheCreationTokenDetails,
|
|||
|
|
CompletionTokensDetailsWrapper,
|
|||
|
|
)
|
|||
|
|
from litellm.types.utils import Message as LitellmMessage
|
|||
|
|
from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse
|
|||
|
|
from litellm.utils import (
|
|||
|
|
ModelResponse,
|
|||
|
|
Usage,
|
|||
|
|
add_dummy_tool,
|
|||
|
|
any_assistant_message_has_thinking_blocks,
|
|||
|
|
get_max_tokens,
|
|||
|
|
has_tool_call_blocks,
|
|||
|
|
last_assistant_with_tool_calls_has_no_thinking_blocks,
|
|||
|
|
supports_reasoning,
|
|||
|
|
token_counter,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
from ..common_utils import AnthropicError, AnthropicModelInfo, process_anthropic_headers
|
|||
|
|
|
|||
|
|
if TYPE_CHECKING:
|
|||
|
|
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
|||
|
|
|
|||
|
|
LoggingClass = LiteLLMLoggingObj
|
|||
|
|
else:
|
|||
|
|
LoggingClass = Any
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
|
"""
|
|||
|
|
Reference: https://docs.anthropic.com/claude/reference/messages_post
|
|||
|
|
|
|||
|
|
to pass metadata to anthropic, it's {"user_id": "any-relevant-information"}
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
max_tokens: Optional[int] = None
|
|||
|
|
stop_sequences: Optional[list] = None
|
|||
|
|
temperature: Optional[int] = None
|
|||
|
|
top_p: Optional[int] = None
|
|||
|
|
top_k: Optional[int] = None
|
|||
|
|
metadata: Optional[dict] = None
|
|||
|
|
system: Optional[str] = None
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
max_tokens: Optional[int] = None,
|
|||
|
|
stop_sequences: Optional[list] = None,
|
|||
|
|
temperature: Optional[int] = None,
|
|||
|
|
top_p: Optional[int] = None,
|
|||
|
|
top_k: Optional[int] = None,
|
|||
|
|
metadata: Optional[dict] = None,
|
|||
|
|
system: Optional[str] = None,
|
|||
|
|
) -> None:
|
|||
|
|
locals_ = locals().copy()
|
|||
|
|
for key, value in locals_.items():
|
|||
|
|
if key != "self" and value is not None:
|
|||
|
|
setattr(self.__class__, key, value)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def custom_llm_provider(self) -> Optional[str]:
|
|||
|
|
return "anthropic"
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def get_config(cls, *, model: Optional[str] = None):
|
|||
|
|
config = super().get_config()
|
|||
|
|
|
|||
|
|
# anthropic requires a default value for max_tokens
|
|||
|
|
if config.get("max_tokens") is None:
|
|||
|
|
config["max_tokens"] = cls.get_max_tokens_for_model(model)
|
|||
|
|
|
|||
|
|
return config
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def get_max_tokens_for_model(model: Optional[str] = None) -> int:
|
|||
|
|
"""
|
|||
|
|
Get the max output tokens for a given model.
|
|||
|
|
Falls back to DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS (configurable via env var) if model is not found.
|
|||
|
|
"""
|
|||
|
|
if model is None:
|
|||
|
|
return DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS
|
|||
|
|
try:
|
|||
|
|
max_tokens = get_max_tokens(model)
|
|||
|
|
if max_tokens is None:
|
|||
|
|
return DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS
|
|||
|
|
return max_tokens
|
|||
|
|
except Exception:
|
|||
|
|
return DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def convert_tool_use_to_openai_format(
|
|||
|
|
anthropic_tool_content: Dict[str, Any],
|
|||
|
|
index: int,
|
|||
|
|
) -> ChatCompletionToolCallChunk:
|
|||
|
|
"""
|
|||
|
|
Convert Anthropic tool_use format to OpenAI ChatCompletionToolCallChunk format.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
anthropic_tool_content: Anthropic tool_use content block with format:
|
|||
|
|
{"type": "tool_use", "id": "...", "name": "...", "input": {...}}
|
|||
|
|
index: The index of this tool call
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
ChatCompletionToolCallChunk in OpenAI format
|
|||
|
|
"""
|
|||
|
|
tool_call = ChatCompletionToolCallChunk(
|
|||
|
|
id=anthropic_tool_content["id"],
|
|||
|
|
type="function",
|
|||
|
|
function=ChatCompletionToolCallFunctionChunk(
|
|||
|
|
name=anthropic_tool_content["name"],
|
|||
|
|
arguments=json.dumps(anthropic_tool_content["input"]),
|
|||
|
|
),
|
|||
|
|
index=index,
|
|||
|
|
)
|
|||
|
|
# Include caller information if present (for programmatic tool calling)
|
|||
|
|
if "caller" in anthropic_tool_content:
|
|||
|
|
tool_call["caller"] = cast(Dict[str, Any], anthropic_tool_content["caller"]) # type: ignore[typeddict-item]
|
|||
|
|
return tool_call
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _is_opus_4_6_model(model: str) -> bool:
|
|||
|
|
"""Check if the model is specifically Claude Opus 4.6."""
|
|||
|
|
model_lower = model.lower()
|
|||
|
|
return any(
|
|||
|
|
v in model_lower for v in ("opus-4-6", "opus_4_6", "opus-4.6", "opus_4.6")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def get_supported_openai_params(self, model: str):
|
|||
|
|
params = [
|
|||
|
|
"stream",
|
|||
|
|
"stop",
|
|||
|
|
"temperature",
|
|||
|
|
"top_p",
|
|||
|
|
"max_tokens",
|
|||
|
|
"max_completion_tokens",
|
|||
|
|
"tools",
|
|||
|
|
"tool_choice",
|
|||
|
|
"extra_headers",
|
|||
|
|
"parallel_tool_calls",
|
|||
|
|
"response_format",
|
|||
|
|
"user",
|
|||
|
|
"web_search_options",
|
|||
|
|
"speed",
|
|||
|
|
"context_management",
|
|||
|
|
"cache_control",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
"claude-3-7-sonnet" in model
|
|||
|
|
or AnthropicConfig._is_claude_4_6_model(model)
|
|||
|
|
or supports_reasoning(
|
|||
|
|
model=model,
|
|||
|
|
custom_llm_provider=self.custom_llm_provider,
|
|||
|
|
)
|
|||
|
|
):
|
|||
|
|
params.append("thinking")
|
|||
|
|
params.append("reasoning_effort")
|
|||
|
|
|
|||
|
|
return params
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
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/minItems: Not supported for array types
|
|||
|
|
- minimum/maximum: Not supported for numeric types
|
|||
|
|
- minLength/maxLength: Not supported for string types
|
|||
|
|
|
|||
|
|
This mirrors the transformation done by the Anthropic Python SDK.
|
|||
|
|
See: https://platform.claude.com/docs/en/build-with-claude/structured-outputs#how-sdk-transformation-works
|
|||
|
|
|
|||
|
|
The SDK approach:
|
|||
|
|
1. Remove unsupported constraints from schema
|
|||
|
|
2. Add constraint info to description (e.g., "Must be at least 100")
|
|||
|
|
3. Validate responses against original schema
|
|||
|
|
Args:
|
|||
|
|
schema: The JSON schema dictionary to filter
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
A new dictionary with unsupported fields removed and descriptions updated
|
|||
|
|
|
|||
|
|
Related issues:
|
|||
|
|
- https://github.com/BerriAI/litellm/issues/19444
|
|||
|
|
"""
|
|||
|
|
if not isinstance(schema, dict):
|
|||
|
|
return schema
|
|||
|
|
|
|||
|
|
# All numeric/string/array constraints not supported by Anthropic
|
|||
|
|
unsupported_fields = {
|
|||
|
|
"maxItems",
|
|||
|
|
"minItems", # array constraints
|
|||
|
|
"minimum",
|
|||
|
|
"maximum", # numeric constraints
|
|||
|
|
"exclusiveMinimum",
|
|||
|
|
"exclusiveMaximum", # numeric constraints
|
|||
|
|
"minLength",
|
|||
|
|
"maxLength", # string constraints
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Build description additions from removed constraints
|
|||
|
|
constraint_descriptions: list = []
|
|||
|
|
constraint_labels = {
|
|||
|
|
"minItems": "minimum number of items: {}",
|
|||
|
|
"maxItems": "maximum number of items: {}",
|
|||
|
|
"minimum": "minimum value: {}",
|
|||
|
|
"maximum": "maximum value: {}",
|
|||
|
|
"exclusiveMinimum": "exclusive minimum value: {}",
|
|||
|
|
"exclusiveMaximum": "exclusive maximum value: {}",
|
|||
|
|
"minLength": "minimum length: {}",
|
|||
|
|
"maxLength": "maximum length: {}",
|
|||
|
|
}
|
|||
|
|
for field in unsupported_fields:
|
|||
|
|
if field in schema:
|
|||
|
|
constraint_descriptions.append(
|
|||
|
|
constraint_labels[field].format(schema[field])
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result: Dict[str, Any] = {}
|
|||
|
|
|
|||
|
|
# Update description with removed constraint info
|
|||
|
|
if constraint_descriptions:
|
|||
|
|
existing_desc = schema.get("description", "")
|
|||
|
|
constraint_note = "Note: " + ", ".join(constraint_descriptions) + "."
|
|||
|
|
if existing_desc:
|
|||
|
|
result["description"] = existing_desc + " " + constraint_note
|
|||
|
|
else:
|
|||
|
|
result["description"] = constraint_note
|
|||
|
|
|
|||
|
|
for key, value in schema.items():
|
|||
|
|
if key in unsupported_fields:
|
|||
|
|
continue
|
|||
|
|
if key == "description" and "description" in result:
|
|||
|
|
# Already handled above
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if key == "properties" and isinstance(value, dict):
|
|||
|
|
result[key] = {
|
|||
|
|
k: AnthropicConfig.filter_anthropic_output_schema(v)
|
|||
|
|
for k, v in value.items()
|
|||
|
|
}
|
|||
|
|
elif key == "items" and isinstance(value, dict):
|
|||
|
|
result[key] = AnthropicConfig.filter_anthropic_output_schema(value)
|
|||
|
|
elif key == "$defs" and isinstance(value, dict):
|
|||
|
|
result[key] = {
|
|||
|
|
k: AnthropicConfig.filter_anthropic_output_schema(v)
|
|||
|
|
for k, v in value.items()
|
|||
|
|
}
|
|||
|
|
elif key == "anyOf" and isinstance(value, list):
|
|||
|
|
result[key] = [
|
|||
|
|
AnthropicConfig.filter_anthropic_output_schema(item)
|
|||
|
|
for item in value
|
|||
|
|
]
|
|||
|
|
elif key == "allOf" and isinstance(value, list):
|
|||
|
|
result[key] = [
|
|||
|
|
AnthropicConfig.filter_anthropic_output_schema(item)
|
|||
|
|
for item in value
|
|||
|
|
]
|
|||
|
|
elif key == "oneOf" and isinstance(value, list):
|
|||
|
|
result[key] = [
|
|||
|
|
AnthropicConfig.filter_anthropic_output_schema(item)
|
|||
|
|
for item in value
|
|||
|
|
]
|
|||
|
|
else:
|
|||
|
|
result[key] = value
|
|||
|
|
|
|||
|
|
# Anthropic requires additionalProperties=false for object schemas
|
|||
|
|
# See: https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs
|
|||
|
|
if result.get("type") == "object" and "additionalProperties" not in result:
|
|||
|
|
result["additionalProperties"] = False
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
def get_json_schema_from_pydantic_object(
|
|||
|
|
self, response_format: Union[Any, Dict, None]
|
|||
|
|
) -> Optional[dict]:
|
|||
|
|
return type_to_response_format_param(
|
|||
|
|
response_format, ref_template="/$defs/{model}"
|
|||
|
|
) # Relevant issue: https://github.com/BerriAI/litellm/issues/7755
|
|||
|
|
|
|||
|
|
def get_cache_control_headers(self) -> dict:
|
|||
|
|
# Anthropic no longer requires the prompt-caching beta header
|
|||
|
|
# Prompt caching now works automatically when cache_control is used in messages
|
|||
|
|
# Reference: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
|||
|
|
return {
|
|||
|
|
"anthropic-version": "2023-06-01",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _map_tool_choice(
|
|||
|
|
self, tool_choice: Optional[str], parallel_tool_use: Optional[bool]
|
|||
|
|
) -> Optional[AnthropicMessagesToolChoice]:
|
|||
|
|
_tool_choice: Optional[AnthropicMessagesToolChoice] = None
|
|||
|
|
if tool_choice == "auto":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(
|
|||
|
|
type="auto",
|
|||
|
|
)
|
|||
|
|
elif tool_choice == "required":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="any")
|
|||
|
|
elif tool_choice == "none":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="none")
|
|||
|
|
elif isinstance(tool_choice, dict):
|
|||
|
|
if "type" in tool_choice and "function" not in tool_choice:
|
|||
|
|
tool_type = tool_choice.get("type")
|
|||
|
|
if tool_type == "auto":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="auto")
|
|||
|
|
elif tool_type == "required" or tool_type == "any":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="any")
|
|||
|
|
elif tool_type == "none":
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="none")
|
|||
|
|
else:
|
|||
|
|
_tool_name = tool_choice.get("function", {}).get("name")
|
|||
|
|
if _tool_name is not None:
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(type="tool")
|
|||
|
|
_tool_choice["name"] = _tool_name
|
|||
|
|
|
|||
|
|
if parallel_tool_use is not None:
|
|||
|
|
# Anthropic uses 'disable_parallel_tool_use' flag to determine if parallel tool use is allowed
|
|||
|
|
# this is the inverse of the openai flag.
|
|||
|
|
if tool_choice == "none":
|
|||
|
|
pass
|
|||
|
|
elif _tool_choice is not None:
|
|||
|
|
_tool_choice["disable_parallel_tool_use"] = not parallel_tool_use
|
|||
|
|
else: # use anthropic defaults and make sure to send the disable_parallel_tool_use flag
|
|||
|
|
_tool_choice = AnthropicMessagesToolChoice(
|
|||
|
|
type="auto",
|
|||
|
|
disable_parallel_tool_use=not parallel_tool_use,
|
|||
|
|
)
|
|||
|
|
return _tool_choice
|
|||
|
|
|
|||
|
|
def _map_tool_helper( # noqa: PLR0915
|
|||
|
|
self, tool: ChatCompletionToolParam
|
|||
|
|
) -> Tuple[Optional[AllAnthropicToolsValues], Optional[AnthropicMcpServerTool]]:
|
|||
|
|
returned_tool: Optional[AllAnthropicToolsValues] = None
|
|||
|
|
mcp_server: Optional[AnthropicMcpServerTool] = None
|
|||
|
|
|
|||
|
|
if tool["type"] == "function" or tool["type"] == "custom":
|
|||
|
|
_input_schema: dict = tool["function"].get(
|
|||
|
|
"parameters",
|
|||
|
|
{
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {},
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Anthropic requires input_schema.type to be "object". Normalize
|
|||
|
|
# schemas from external sources (MCP servers, OpenAI callers) that
|
|||
|
|
# may omit the type field or use a non-object type.
|
|||
|
|
if _input_schema.get("type") != "object":
|
|||
|
|
litellm.verbose_logger.debug(
|
|||
|
|
"_map_tool_helper: coercing input_schema type from %r to "
|
|||
|
|
"'object' for Anthropic compatibility (tool: %s)",
|
|||
|
|
_input_schema.get("type"),
|
|||
|
|
tool["function"].get("name"),
|
|||
|
|
)
|
|||
|
|
_input_schema = dict(_input_schema) # avoid mutating caller's dict
|
|||
|
|
_input_schema["type"] = "object"
|
|||
|
|
if "properties" not in _input_schema:
|
|||
|
|
_input_schema["properties"] = {}
|
|||
|
|
|
|||
|
|
_allowed_properties = set(AnthropicInputSchema.__annotations__.keys())
|
|||
|
|
input_schema_filtered = {
|
|||
|
|
k: v for k, v in _input_schema.items() if k in _allowed_properties
|
|||
|
|
}
|
|||
|
|
input_anthropic_schema: AnthropicInputSchema = AnthropicInputSchema(
|
|||
|
|
**input_schema_filtered
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_tool = AnthropicMessagesTool(
|
|||
|
|
name=tool["function"]["name"],
|
|||
|
|
input_schema=input_anthropic_schema,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_description = tool["function"].get("description")
|
|||
|
|
if _description is not None:
|
|||
|
|
_tool["description"] = _description
|
|||
|
|
|
|||
|
|
returned_tool = _tool
|
|||
|
|
|
|||
|
|
elif tool["type"].startswith("computer_"):
|
|||
|
|
## check if all required 'display_' params are given
|
|||
|
|
if "parameters" not in tool["function"]:
|
|||
|
|
raise ValueError("Missing required parameter: parameters")
|
|||
|
|
|
|||
|
|
_display_width_px: Optional[int] = tool["function"]["parameters"].get(
|
|||
|
|
"display_width_px"
|
|||
|
|
)
|
|||
|
|
_display_height_px: Optional[int] = tool["function"]["parameters"].get(
|
|||
|
|
"display_height_px"
|
|||
|
|
)
|
|||
|
|
if _display_width_px is None or _display_height_px is None:
|
|||
|
|
raise ValueError(
|
|||
|
|
"Missing required parameter: display_width_px or display_height_px"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_computer_tool = AnthropicComputerTool(
|
|||
|
|
type=tool["type"],
|
|||
|
|
name=tool["function"].get("name", "computer"),
|
|||
|
|
display_width_px=_display_width_px,
|
|||
|
|
display_height_px=_display_height_px,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_display_number = tool["function"]["parameters"].get("display_number")
|
|||
|
|
if _display_number is not None:
|
|||
|
|
_computer_tool["display_number"] = _display_number
|
|||
|
|
|
|||
|
|
returned_tool = _computer_tool
|
|||
|
|
elif any(tool["type"].startswith(t) for t in ANTHROPIC_HOSTED_TOOLS):
|
|||
|
|
function_name_obj = tool.get("name", tool.get("function", {}).get("name"))
|
|||
|
|
if function_name_obj is None or not isinstance(function_name_obj, str):
|
|||
|
|
raise ValueError("Missing required parameter: name")
|
|||
|
|
function_name = function_name_obj
|
|||
|
|
|
|||
|
|
additional_tool_params = {}
|
|||
|
|
for k, v in tool.items():
|
|||
|
|
if k != "type" and k != "name":
|
|||
|
|
additional_tool_params[k] = v
|
|||
|
|
|
|||
|
|
returned_tool = AnthropicHostedTools(
|
|||
|
|
type=tool["type"], name=function_name, **additional_tool_params # type: ignore
|
|||
|
|
)
|
|||
|
|
elif tool["type"] == "url": # mcp server tool
|
|||
|
|
mcp_server = AnthropicMcpServerTool(**tool) # type: ignore
|
|||
|
|
elif tool["type"] == "mcp":
|
|||
|
|
mcp_server = self._map_openai_mcp_server_tool(
|
|||
|
|
cast(OpenAIMcpServerTool, tool)
|
|||
|
|
)
|
|||
|
|
elif tool["type"] == "tool_search_tool_regex_20251119":
|
|||
|
|
# Tool search tool using regex
|
|||
|
|
from litellm.types.llms.anthropic import AnthropicToolSearchToolRegex
|
|||
|
|
|
|||
|
|
tool_name_obj = tool.get("name", "tool_search_tool_regex")
|
|||
|
|
if not isinstance(tool_name_obj, str):
|
|||
|
|
raise ValueError("Tool search tool must have a valid name")
|
|||
|
|
tool_name = tool_name_obj
|
|||
|
|
returned_tool = AnthropicToolSearchToolRegex(
|
|||
|
|
type="tool_search_tool_regex_20251119",
|
|||
|
|
name=tool_name,
|
|||
|
|
)
|
|||
|
|
elif tool["type"] == "tool_search_tool_bm25_20251119":
|
|||
|
|
# Tool search tool using BM25
|
|||
|
|
from litellm.types.llms.anthropic import AnthropicToolSearchToolBM25
|
|||
|
|
|
|||
|
|
tool_name_obj = tool.get("name", "tool_search_tool_bm25")
|
|||
|
|
if not isinstance(tool_name_obj, str):
|
|||
|
|
raise ValueError("Tool search tool must have a valid name")
|
|||
|
|
tool_name = tool_name_obj
|
|||
|
|
returned_tool = AnthropicToolSearchToolBM25(
|
|||
|
|
type="tool_search_tool_bm25_20251119",
|
|||
|
|
name=tool_name,
|
|||
|
|
)
|
|||
|
|
if returned_tool is None and mcp_server is None:
|
|||
|
|
raise ValueError(f"Unsupported tool type: {tool['type']}")
|
|||
|
|
|
|||
|
|
## check if cache_control is set in the tool
|
|||
|
|
_cache_control = tool.get("cache_control", None)
|
|||
|
|
_cache_control_function = tool.get("function", {}).get("cache_control", None)
|
|||
|
|
if returned_tool is not None:
|
|||
|
|
# Only set cache_control on tools that support it (not tool search tools)
|
|||
|
|
tool_type = returned_tool.get("type", "")
|
|||
|
|
if tool_type not in (
|
|||
|
|
"tool_search_tool_regex_20251119",
|
|||
|
|
"tool_search_tool_bm25_20251119",
|
|||
|
|
):
|
|||
|
|
if _cache_control is not None:
|
|||
|
|
returned_tool["cache_control"] = _cache_control # type: ignore[typeddict-item]
|
|||
|
|
elif _cache_control_function is not None and isinstance(
|
|||
|
|
_cache_control_function, dict
|
|||
|
|
):
|
|||
|
|
returned_tool["cache_control"] = ChatCompletionCachedContent( # type: ignore[typeddict-item]
|
|||
|
|
**_cache_control_function # type: ignore
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
## check if defer_loading is set in the tool
|
|||
|
|
_defer_loading = tool.get("defer_loading", None)
|
|||
|
|
_defer_loading_function = tool.get("function", {}).get("defer_loading", None)
|
|||
|
|
if returned_tool is not None:
|
|||
|
|
# Only set defer_loading on tools that support it (not tool search tools or computer tools)
|
|||
|
|
tool_type = returned_tool.get("type", "")
|
|||
|
|
if tool_type not in (
|
|||
|
|
"tool_search_tool_regex_20251119",
|
|||
|
|
"tool_search_tool_bm25_20251119",
|
|||
|
|
"computer_20241022",
|
|||
|
|
"computer_20250124",
|
|||
|
|
):
|
|||
|
|
if _defer_loading is not None:
|
|||
|
|
if not isinstance(_defer_loading, bool):
|
|||
|
|
raise ValueError("defer_loading must be a boolean")
|
|||
|
|
returned_tool["defer_loading"] = _defer_loading # type: ignore[typeddict-item]
|
|||
|
|
elif _defer_loading_function is not None:
|
|||
|
|
if not isinstance(_defer_loading_function, bool):
|
|||
|
|
raise ValueError("defer_loading must be a boolean")
|
|||
|
|
returned_tool["defer_loading"] = _defer_loading_function # type: ignore[typeddict-item]
|
|||
|
|
|
|||
|
|
## check if allowed_callers is set in the tool
|
|||
|
|
_allowed_callers = tool.get("allowed_callers", None)
|
|||
|
|
_allowed_callers_function = tool.get("function", {}).get(
|
|||
|
|
"allowed_callers", None
|
|||
|
|
)
|
|||
|
|
if returned_tool is not None:
|
|||
|
|
# Only set allowed_callers on tools that support it (not tool search tools or computer tools)
|
|||
|
|
tool_type = returned_tool.get("type", "")
|
|||
|
|
if tool_type not in (
|
|||
|
|
"tool_search_tool_regex_20251119",
|
|||
|
|
"tool_search_tool_bm25_20251119",
|
|||
|
|
"computer_20241022",
|
|||
|
|
"computer_20250124",
|
|||
|
|
):
|
|||
|
|
if _allowed_callers is not None:
|
|||
|
|
if not isinstance(_allowed_callers, list) or not all(
|
|||
|
|
isinstance(item, str) for item in _allowed_callers
|
|||
|
|
):
|
|||
|
|
raise ValueError("allowed_callers must be a list of strings")
|
|||
|
|
returned_tool["allowed_callers"] = _allowed_callers # type: ignore[typeddict-item]
|
|||
|
|
elif _allowed_callers_function is not None:
|
|||
|
|
if not isinstance(_allowed_callers_function, list) or not all(
|
|||
|
|
isinstance(item, str) for item in _allowed_callers_function
|
|||
|
|
):
|
|||
|
|
raise ValueError("allowed_callers must be a list of strings")
|
|||
|
|
returned_tool["allowed_callers"] = _allowed_callers_function # type: ignore[typeddict-item]
|
|||
|
|
|
|||
|
|
## check if input_examples is set in the tool
|
|||
|
|
_input_examples = tool.get("input_examples", None)
|
|||
|
|
_input_examples_function = tool.get("function", {}).get("input_examples", None)
|
|||
|
|
if returned_tool is not None:
|
|||
|
|
# Only set input_examples on user-defined tools (type "custom" or no type)
|
|||
|
|
tool_type = returned_tool.get("type", "")
|
|||
|
|
if tool_type == "custom" or (tool_type == "" and "name" in returned_tool):
|
|||
|
|
if _input_examples is not None and isinstance(_input_examples, list):
|
|||
|
|
returned_tool["input_examples"] = _input_examples # type: ignore[typeddict-item]
|
|||
|
|
elif _input_examples_function is not None and isinstance(
|
|||
|
|
_input_examples_function, list
|
|||
|
|
):
|
|||
|
|
returned_tool["input_examples"] = _input_examples_function # type: ignore[typeddict-item]
|
|||
|
|
|
|||
|
|
return returned_tool, mcp_server
|
|||
|
|
|
|||
|
|
def _map_openai_mcp_server_tool(
|
|||
|
|
self, tool: OpenAIMcpServerTool
|
|||
|
|
) -> AnthropicMcpServerTool:
|
|||
|
|
from litellm.types.llms.anthropic import AnthropicMcpServerToolConfiguration
|
|||
|
|
|
|||
|
|
allowed_tools = tool.get("allowed_tools", None)
|
|||
|
|
tool_configuration: Optional[AnthropicMcpServerToolConfiguration] = None
|
|||
|
|
if allowed_tools is not None:
|
|||
|
|
tool_configuration = AnthropicMcpServerToolConfiguration(
|
|||
|
|
allowed_tools=tool.get("allowed_tools", None),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
headers = tool.get("headers", {})
|
|||
|
|
authorization_token: Optional[str] = None
|
|||
|
|
if headers is not None:
|
|||
|
|
bearer_token = headers.get("Authorization", None)
|
|||
|
|
if bearer_token is not None:
|
|||
|
|
authorization_token = bearer_token.replace("Bearer ", "")
|
|||
|
|
|
|||
|
|
initial_tool = AnthropicMcpServerTool(
|
|||
|
|
type="url",
|
|||
|
|
url=tool["server_url"],
|
|||
|
|
name=tool["server_label"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if tool_configuration is not None:
|
|||
|
|
initial_tool["tool_configuration"] = tool_configuration
|
|||
|
|
if authorization_token is not None:
|
|||
|
|
initial_tool["authorization_token"] = authorization_token
|
|||
|
|
return initial_tool
|
|||
|
|
|
|||
|
|
def _map_tools(
|
|||
|
|
self, tools: List
|
|||
|
|
) -> Tuple[List[AllAnthropicToolsValues], List[AnthropicMcpServerTool]]:
|
|||
|
|
anthropic_tools = []
|
|||
|
|
mcp_servers = []
|
|||
|
|
for tool in tools:
|
|||
|
|
if "input_schema" in tool: # assume in anthropic format
|
|||
|
|
anthropic_tools.append(tool)
|
|||
|
|
else: # assume openai tool call
|
|||
|
|
new_tool, mcp_server_tool = self._map_tool_helper(tool)
|
|||
|
|
|
|||
|
|
if new_tool is not None:
|
|||
|
|
anthropic_tools.append(new_tool)
|
|||
|
|
if mcp_server_tool is not None:
|
|||
|
|
mcp_servers.append(mcp_server_tool)
|
|||
|
|
return anthropic_tools, mcp_servers
|
|||
|
|
|
|||
|
|
def _detect_tool_search_tools(self, tools: Optional[List]) -> bool:
|
|||
|
|
"""Check if tool search tools are present in the tools list."""
|
|||
|
|
if not tools:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
for tool in tools:
|
|||
|
|
tool_type = tool.get("type", "")
|
|||
|
|
if tool_type in [
|
|||
|
|
"tool_search_tool_regex_20251119",
|
|||
|
|
"tool_search_tool_bm25_20251119",
|
|||
|
|
]:
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _separate_deferred_tools(self, tools: List) -> Tuple[List, List]:
|
|||
|
|
"""
|
|||
|
|
Separate tools into deferred and non-deferred lists.
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Tuple of (non_deferred_tools, deferred_tools)
|
|||
|
|
"""
|
|||
|
|
non_deferred = []
|
|||
|
|
deferred = []
|
|||
|
|
|
|||
|
|
for tool in tools:
|
|||
|
|
if tool.get("defer_loading", False):
|
|||
|
|
deferred.append(tool)
|
|||
|
|
else:
|
|||
|
|
non_deferred.append(tool)
|
|||
|
|
|
|||
|
|
return non_deferred, deferred
|
|||
|
|
|
|||
|
|
def _expand_tool_references(
|
|||
|
|
self,
|
|||
|
|
content: List,
|
|||
|
|
deferred_tools: List,
|
|||
|
|
) -> List:
|
|||
|
|
"""
|
|||
|
|
Expand tool_reference blocks to full tool definitions.
|
|||
|
|
|
|||
|
|
When Anthropic's tool search returns results, it includes tool_reference blocks
|
|||
|
|
that reference tools by name. This method expands those references to full
|
|||
|
|
tool definitions from the deferred_tools catalog.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Response content that may contain tool_reference blocks
|
|||
|
|
deferred_tools: List of deferred tools that can be referenced
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Content with tool_reference blocks expanded to full tool definitions
|
|||
|
|
"""
|
|||
|
|
if not deferred_tools:
|
|||
|
|
return content
|
|||
|
|
|
|||
|
|
# Create a mapping of tool names to tool definitions
|
|||
|
|
tool_map = {}
|
|||
|
|
for tool in deferred_tools:
|
|||
|
|
tool_name = tool.get("name") or tool.get("function", {}).get("name")
|
|||
|
|
if tool_name:
|
|||
|
|
tool_map[tool_name] = tool
|
|||
|
|
|
|||
|
|
# Expand tool references in content
|
|||
|
|
expanded_content = []
|
|||
|
|
for item in content:
|
|||
|
|
if isinstance(item, dict) and item.get("type") == "tool_reference":
|
|||
|
|
tool_name = item.get("tool_name")
|
|||
|
|
if tool_name and tool_name in tool_map:
|
|||
|
|
# Replace reference with full tool definition
|
|||
|
|
expanded_content.append(tool_map[tool_name])
|
|||
|
|
else:
|
|||
|
|
# Keep the reference if we can't find the tool
|
|||
|
|
expanded_content.append(item)
|
|||
|
|
else:
|
|||
|
|
expanded_content.append(item)
|
|||
|
|
|
|||
|
|
return expanded_content
|
|||
|
|
|
|||
|
|
def _map_stop_sequences(
|
|||
|
|
self, stop: Optional[Union[str, List[str]]]
|
|||
|
|
) -> Optional[List[str]]:
|
|||
|
|
new_stop: Optional[List[str]] = None
|
|||
|
|
if isinstance(stop, str):
|
|||
|
|
if (
|
|||
|
|
stop.isspace() and litellm.drop_params is True
|
|||
|
|
): # anthropic doesn't allow whitespace characters as stop-sequences
|
|||
|
|
return new_stop
|
|||
|
|
new_stop = [stop]
|
|||
|
|
elif isinstance(stop, list):
|
|||
|
|
new_v = []
|
|||
|
|
for v in stop:
|
|||
|
|
if (
|
|||
|
|
v.isspace() and litellm.drop_params is True
|
|||
|
|
): # anthropic doesn't allow whitespace characters as stop-sequences
|
|||
|
|
continue
|
|||
|
|
new_v.append(v)
|
|||
|
|
if len(new_v) > 0:
|
|||
|
|
new_stop = new_v
|
|||
|
|
return new_stop
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _map_reasoning_effort(
|
|||
|
|
reasoning_effort: Optional[Union[REASONING_EFFORT, str]],
|
|||
|
|
model: str,
|
|||
|
|
) -> Optional[AnthropicThinkingParam]:
|
|||
|
|
if reasoning_effort is None or reasoning_effort == "none":
|
|||
|
|
return None
|
|||
|
|
if AnthropicConfig._is_claude_4_6_model(model):
|
|||
|
|
return AnthropicThinkingParam(
|
|||
|
|
type="adaptive",
|
|||
|
|
)
|
|||
|
|
elif reasoning_effort == "low":
|
|||
|
|
return AnthropicThinkingParam(
|
|||
|
|
type="enabled",
|
|||
|
|
budget_tokens=DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
|
|||
|
|
)
|
|||
|
|
elif reasoning_effort == "medium":
|
|||
|
|
return AnthropicThinkingParam(
|
|||
|
|
type="enabled",
|
|||
|
|
budget_tokens=DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
|
|||
|
|
)
|
|||
|
|
elif reasoning_effort == "high":
|
|||
|
|
return AnthropicThinkingParam(
|
|||
|
|
type="enabled",
|
|||
|
|
budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
|||
|
|
)
|
|||
|
|
elif reasoning_effort == "minimal":
|
|||
|
|
return AnthropicThinkingParam(
|
|||
|
|
type="enabled",
|
|||
|
|
budget_tokens=DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}")
|
|||
|
|
|
|||
|
|
def _extract_json_schema_from_response_format(
|
|||
|
|
self, value: Optional[dict]
|
|||
|
|
) -> Optional[dict]:
|
|||
|
|
if value is None:
|
|||
|
|
return None
|
|||
|
|
json_schema: Optional[dict] = None
|
|||
|
|
if "response_schema" in value:
|
|||
|
|
json_schema = value["response_schema"]
|
|||
|
|
elif "json_schema" in value:
|
|||
|
|
json_schema = value["json_schema"]["schema"]
|
|||
|
|
|
|||
|
|
return json_schema
|
|||
|
|
|
|||
|
|
def map_response_format_to_anthropic_output_format(
|
|||
|
|
self, value: Optional[dict]
|
|||
|
|
) -> Optional[AnthropicOutputSchema]:
|
|||
|
|
json_schema: Optional[dict] = self._extract_json_schema_from_response_format(
|
|||
|
|
value
|
|||
|
|
)
|
|||
|
|
if json_schema is None:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Resolve $ref/$defs before filtering — Anthropic doesn't support
|
|||
|
|
# external schema references (e.g., /$defs/CalendarEvent).
|
|||
|
|
import copy
|
|||
|
|
|
|||
|
|
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
|
|
unpack_defs,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
json_schema = copy.deepcopy(json_schema)
|
|||
|
|
defs = json_schema.pop("$defs", json_schema.pop("definitions", {}))
|
|||
|
|
if defs:
|
|||
|
|
unpack_defs(json_schema, defs)
|
|||
|
|
|
|||
|
|
# 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,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def map_response_format_to_anthropic_tool(
|
|||
|
|
self, value: Optional[dict], optional_params: dict, is_thinking_enabled: bool
|
|||
|
|
) -> Optional[AnthropicMessagesTool]:
|
|||
|
|
ignore_response_format_types = ["text"]
|
|||
|
|
if (
|
|||
|
|
value is None or value["type"] in ignore_response_format_types
|
|||
|
|
): # value is a no-op
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
json_schema: Optional[dict] = self._extract_json_schema_from_response_format(
|
|||
|
|
value
|
|||
|
|
)
|
|||
|
|
if json_schema is None:
|
|||
|
|
return None
|
|||
|
|
"""
|
|||
|
|
When using tools in this way: - https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode
|
|||
|
|
- You usually want to provide a single tool
|
|||
|
|
- You should set tool_choice (see Forcing tool use) to instruct the model to explicitly use that tool
|
|||
|
|
- Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model’s perspective.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
_tool = self._create_json_tool_call_for_response_format(
|
|||
|
|
json_schema=json_schema,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return _tool
|
|||
|
|
|
|||
|
|
def map_web_search_tool(
|
|||
|
|
self,
|
|||
|
|
value: OpenAIWebSearchOptions,
|
|||
|
|
) -> AnthropicWebSearchTool:
|
|||
|
|
value_typed = cast(OpenAIWebSearchOptions, value)
|
|||
|
|
hosted_web_search_tool = AnthropicWebSearchTool(
|
|||
|
|
type="web_search_20250305",
|
|||
|
|
name="web_search",
|
|||
|
|
)
|
|||
|
|
user_location = value_typed.get("user_location")
|
|||
|
|
if user_location is not None:
|
|||
|
|
anthropic_user_location = AnthropicWebSearchUserLocation(type="approximate")
|
|||
|
|
anthropic_user_location_keys = (
|
|||
|
|
AnthropicWebSearchUserLocation.__annotations__.keys()
|
|||
|
|
)
|
|||
|
|
user_location_approximate = user_location.get("approximate")
|
|||
|
|
if user_location_approximate is not None:
|
|||
|
|
for key, user_location_value in user_location_approximate.items():
|
|||
|
|
if key in anthropic_user_location_keys and key != "type":
|
|||
|
|
anthropic_user_location[key] = user_location_value # type: ignore
|
|||
|
|
hosted_web_search_tool["user_location"] = anthropic_user_location
|
|||
|
|
|
|||
|
|
## MAP SEARCH CONTEXT SIZE
|
|||
|
|
search_context_size = value_typed.get("search_context_size")
|
|||
|
|
if search_context_size is not None:
|
|||
|
|
hosted_web_search_tool["max_uses"] = ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES[
|
|||
|
|
search_context_size
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
return hosted_web_search_tool
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def map_openai_context_management_to_anthropic(
|
|||
|
|
context_management: Union[List[Dict[str, Any]], Dict[str, Any]],
|
|||
|
|
) -> Optional[Dict[str, Any]]:
|
|||
|
|
"""
|
|||
|
|
OpenAI format: [{"type": "compaction", "compact_threshold": 200000}]
|
|||
|
|
Anthropic format: {
|
|||
|
|
"edits": [
|
|||
|
|
{
|
|||
|
|
"type": "compact_20260112",
|
|||
|
|
"trigger": {"type": "input_tokens", "value": 150000}
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
context_management: OpenAI or Anthropic context_management parameter
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Anthropic-formatted context_management dict, or None if invalid
|
|||
|
|
"""
|
|||
|
|
# If already in Anthropic format (dict with 'edits'), pass through
|
|||
|
|
if isinstance(context_management, dict) and "edits" in context_management:
|
|||
|
|
return context_management
|
|||
|
|
|
|||
|
|
# If in OpenAI format (list), transform to Anthropic format
|
|||
|
|
if isinstance(context_management, list):
|
|||
|
|
anthropic_edits = []
|
|||
|
|
for entry in context_management:
|
|||
|
|
if not isinstance(entry, dict):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
entry_type = entry.get("type")
|
|||
|
|
if entry_type == "compaction":
|
|||
|
|
anthropic_edit: Dict[str, Any] = {"type": "compact_20260112"}
|
|||
|
|
compact_threshold = entry.get("compact_threshold")
|
|||
|
|
# Rewrite to 'trigger' with correct nesting if threshold exists
|
|||
|
|
if compact_threshold is not None and isinstance(
|
|||
|
|
compact_threshold, (int, float)
|
|||
|
|
):
|
|||
|
|
anthropic_edit["trigger"] = {
|
|||
|
|
"type": "input_tokens",
|
|||
|
|
"value": int(compact_threshold),
|
|||
|
|
}
|
|||
|
|
# Map any other keys by passthrough except handled ones
|
|||
|
|
for k in entry:
|
|||
|
|
if k not in {
|
|||
|
|
"type",
|
|||
|
|
"compact_threshold",
|
|||
|
|
}: # only passthrough other keys
|
|||
|
|
anthropic_edit[k] = entry[k]
|
|||
|
|
|
|||
|
|
anthropic_edits.append(anthropic_edit)
|
|||
|
|
|
|||
|
|
if anthropic_edits:
|
|||
|
|
return {"edits": anthropic_edits}
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def map_openai_params( # noqa: PLR0915
|
|||
|
|
self,
|
|||
|
|
non_default_params: dict,
|
|||
|
|
optional_params: dict,
|
|||
|
|
model: str,
|
|||
|
|
drop_params: bool,
|
|||
|
|
) -> dict:
|
|||
|
|
is_thinking_enabled = self.is_thinking_enabled(
|
|||
|
|
non_default_params=non_default_params
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
for param, value in non_default_params.items():
|
|||
|
|
if param == "max_tokens":
|
|||
|
|
optional_params["max_tokens"] = (
|
|||
|
|
value if isinstance(value, int) else max(1, int(round(value)))
|
|||
|
|
)
|
|||
|
|
elif param == "max_completion_tokens":
|
|||
|
|
optional_params["max_tokens"] = (
|
|||
|
|
value if isinstance(value, int) else max(1, int(round(value)))
|
|||
|
|
)
|
|||
|
|
elif param == "tools":
|
|||
|
|
# check if optional params already has tools
|
|||
|
|
anthropic_tools, mcp_servers = self._map_tools(value)
|
|||
|
|
optional_params = self._add_tools_to_optional_params(
|
|||
|
|
optional_params=optional_params, tools=anthropic_tools
|
|||
|
|
)
|
|||
|
|
if mcp_servers:
|
|||
|
|
optional_params["mcp_servers"] = mcp_servers
|
|||
|
|
elif 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"),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if _tool_choice is not None:
|
|||
|
|
optional_params["tool_choice"] = _tool_choice
|
|||
|
|
elif param == "stream" and value is True:
|
|||
|
|
optional_params["stream"] = value
|
|||
|
|
elif param == "stop" and (
|
|||
|
|
isinstance(value, str) or isinstance(value, list)
|
|||
|
|
):
|
|||
|
|
_value = self._map_stop_sequences(value)
|
|||
|
|
if _value is not None:
|
|||
|
|
optional_params["stop_sequences"] = _value
|
|||
|
|
elif param == "temperature":
|
|||
|
|
optional_params["temperature"] = value
|
|||
|
|
elif param == "top_p":
|
|||
|
|
optional_params["top_p"] = value
|
|||
|
|
elif param == "response_format" and isinstance(value, dict):
|
|||
|
|
if any(
|
|||
|
|
substring in model
|
|||
|
|
for substring in {
|
|||
|
|
"sonnet-4.5",
|
|||
|
|
"sonnet-4-5",
|
|||
|
|
"opus-4.1",
|
|||
|
|
"opus-4-1",
|
|||
|
|
"opus-4.5",
|
|||
|
|
"opus-4-5",
|
|||
|
|
"opus-4.6",
|
|||
|
|
"opus-4-6",
|
|||
|
|
"sonnet-4.6",
|
|||
|
|
"sonnet-4-6",
|
|||
|
|
"sonnet_4.6",
|
|||
|
|
"sonnet_4_6",
|
|||
|
|
}
|
|||
|
|
):
|
|||
|
|
_output_format = (
|
|||
|
|
self.map_response_format_to_anthropic_output_format(value)
|
|||
|
|
)
|
|||
|
|
if _output_format is not None:
|
|||
|
|
optional_params["output_format"] = _output_format
|
|||
|
|
else:
|
|||
|
|
_tool = self.map_response_format_to_anthropic_tool(
|
|||
|
|
value, optional_params, is_thinking_enabled
|
|||
|
|
)
|
|||
|
|
if _tool is None:
|
|||
|
|
continue
|
|||
|
|
if not is_thinking_enabled:
|
|||
|
|
_tool_choice = {
|
|||
|
|
"name": RESPONSE_FORMAT_TOOL_NAME,
|
|||
|
|
"type": "tool",
|
|||
|
|
}
|
|||
|
|
optional_params["tool_choice"] = _tool_choice
|
|||
|
|
|
|||
|
|
optional_params = self._add_tools_to_optional_params(
|
|||
|
|
optional_params=optional_params, tools=[_tool]
|
|||
|
|
)
|
|||
|
|
optional_params["json_mode"] = True
|
|||
|
|
elif (
|
|||
|
|
param == "user"
|
|||
|
|
and value is not None
|
|||
|
|
and isinstance(value, str)
|
|||
|
|
and _valid_user_id(value) # anthropic fails on emails
|
|||
|
|
):
|
|||
|
|
optional_params["metadata"] = {"user_id": value}
|
|||
|
|
elif param == "thinking":
|
|||
|
|
optional_params["thinking"] = value
|
|||
|
|
elif param == "reasoning_effort" and isinstance(value, str):
|
|||
|
|
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort(
|
|||
|
|
reasoning_effort=value, model=model
|
|||
|
|
)
|
|||
|
|
# For Claude 4.6 models, effort is controlled via output_config,
|
|||
|
|
# not thinking budget_tokens. Map reasoning_effort to output_config.
|
|||
|
|
if AnthropicConfig._is_claude_4_6_model(model):
|
|||
|
|
effort_map = {
|
|||
|
|
"low": "low",
|
|||
|
|
"minimal": "low",
|
|||
|
|
"medium": "medium",
|
|||
|
|
"high": "high",
|
|||
|
|
"max": "max",
|
|||
|
|
}
|
|||
|
|
mapped_effort = effort_map.get(value, value)
|
|||
|
|
optional_params["output_config"] = {"effort": mapped_effort}
|
|||
|
|
elif param == "web_search_options" and isinstance(value, dict):
|
|||
|
|
hosted_web_search_tool = self.map_web_search_tool(
|
|||
|
|
cast(OpenAIWebSearchOptions, value)
|
|||
|
|
)
|
|||
|
|
self._add_tools_to_optional_params(
|
|||
|
|
optional_params=optional_params, tools=[hosted_web_search_tool]
|
|||
|
|
)
|
|||
|
|
elif param == "extra_headers":
|
|||
|
|
optional_params["extra_headers"] = value
|
|||
|
|
elif param == "context_management":
|
|||
|
|
# Supports both OpenAI list format and Anthropic dict format
|
|||
|
|
if isinstance(value, (list, dict)):
|
|||
|
|
anthropic_context_management = (
|
|||
|
|
self.map_openai_context_management_to_anthropic(value)
|
|||
|
|
)
|
|||
|
|
if anthropic_context_management is not None:
|
|||
|
|
optional_params[
|
|||
|
|
"context_management"
|
|||
|
|
] = anthropic_context_management
|
|||
|
|
elif param == "speed" and isinstance(value, str):
|
|||
|
|
# Pass through Anthropic-specific speed parameter for fast mode
|
|||
|
|
optional_params["speed"] = value
|
|||
|
|
elif param == "cache_control" and isinstance(value, dict):
|
|||
|
|
# Pass through top-level cache_control for automatic prompt caching
|
|||
|
|
optional_params["cache_control"] = value
|
|||
|
|
|
|||
|
|
## handle thinking tokens
|
|||
|
|
self.update_optional_params_with_thinking_tokens(
|
|||
|
|
non_default_params=non_default_params, optional_params=optional_params
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return optional_params
|
|||
|
|
|
|||
|
|
def _create_json_tool_call_for_response_format(
|
|||
|
|
self,
|
|||
|
|
json_schema: Optional[dict] = None,
|
|||
|
|
) -> AnthropicMessagesTool:
|
|||
|
|
"""
|
|||
|
|
Handles creating a tool call for getting responses in JSON format.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
json_schema (Optional[dict]): The JSON schema the response should be in
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
AnthropicMessagesTool: The tool call to send to Anthropic API to get responses in JSON format
|
|||
|
|
"""
|
|||
|
|
_input_schema: AnthropicInputSchema = AnthropicInputSchema(
|
|||
|
|
type="object",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if json_schema is None:
|
|||
|
|
# Anthropic raises a 400 BadRequest error if properties is passed as None
|
|||
|
|
# see usage with additionalProperties (Example 5) https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/extracting_structured_json.ipynb
|
|||
|
|
_input_schema["additionalProperties"] = True
|
|||
|
|
_input_schema["properties"] = {}
|
|||
|
|
else:
|
|||
|
|
_input_schema.update(cast(AnthropicInputSchema, json_schema))
|
|||
|
|
|
|||
|
|
_tool = AnthropicMessagesTool(
|
|||
|
|
name=RESPONSE_FORMAT_TOOL_NAME, input_schema=_input_schema
|
|||
|
|
)
|
|||
|
|
return _tool
|
|||
|
|
|
|||
|
|
def translate_system_message(
|
|||
|
|
self, messages: List[AllMessageValues]
|
|||
|
|
) -> List[AnthropicSystemMessageContent]:
|
|||
|
|
"""
|
|||
|
|
Translate system message to anthropic format.
|
|||
|
|
|
|||
|
|
Removes system message from the original list and returns a new list of anthropic system message content.
|
|||
|
|
Filters out system messages containing x-anthropic-billing-header metadata.
|
|||
|
|
"""
|
|||
|
|
system_prompt_indices = []
|
|||
|
|
anthropic_system_message_list: List[AnthropicSystemMessageContent] = []
|
|||
|
|
for idx, message in enumerate(messages):
|
|||
|
|
if message["role"] == "system":
|
|||
|
|
system_prompt_indices.append(idx)
|
|||
|
|
system_message_block = ChatCompletionSystemMessage(**message)
|
|||
|
|
if isinstance(system_message_block["content"], str):
|
|||
|
|
# Skip empty text blocks - Anthropic API raises errors for empty text
|
|||
|
|
if not system_message_block["content"]:
|
|||
|
|
continue
|
|||
|
|
# Skip system messages containing x-anthropic-billing-header metadata
|
|||
|
|
if system_message_block["content"].startswith(
|
|||
|
|
"x-anthropic-billing-header:"
|
|||
|
|
):
|
|||
|
|
continue
|
|||
|
|
anthropic_system_message_content = AnthropicSystemMessageContent(
|
|||
|
|
type="text",
|
|||
|
|
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_list.append(
|
|||
|
|
anthropic_system_message_content
|
|||
|
|
)
|
|||
|
|
elif isinstance(message["content"], list):
|
|||
|
|
for _content in message["content"]:
|
|||
|
|
# Skip empty text blocks - Anthropic API raises errors for empty text
|
|||
|
|
text_value = _content.get("text")
|
|||
|
|
if _content.get("type") == "text" and not text_value:
|
|||
|
|
continue
|
|||
|
|
# Skip system messages containing x-anthropic-billing-header metadata
|
|||
|
|
if (
|
|||
|
|
_content.get("type") == "text"
|
|||
|
|
and text_value
|
|||
|
|
and text_value.startswith("x-anthropic-billing-header:")
|
|||
|
|
):
|
|||
|
|
continue
|
|||
|
|
anthropic_system_message_content = (
|
|||
|
|
AnthropicSystemMessageContent(
|
|||
|
|
type=_content.get("type"),
|
|||
|
|
text=text_value,
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
if "cache_control" in _content:
|
|||
|
|
anthropic_system_message_content[
|
|||
|
|
"cache_control"
|
|||
|
|
] = _content["cache_control"]
|
|||
|
|
|
|||
|
|
anthropic_system_message_list.append(
|
|||
|
|
anthropic_system_message_content
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if len(system_prompt_indices) > 0:
|
|||
|
|
for idx in reversed(system_prompt_indices):
|
|||
|
|
messages.pop(idx)
|
|||
|
|
|
|||
|
|
return anthropic_system_message_list
|
|||
|
|
|
|||
|
|
def add_code_execution_tool(
|
|||
|
|
self,
|
|||
|
|
messages: List[AllAnthropicMessageValues],
|
|||
|
|
tools: List[Union[AllAnthropicToolsValues, Dict]],
|
|||
|
|
) -> List[Union[AllAnthropicToolsValues, Dict]]:
|
|||
|
|
"""if 'container_upload' in messages, add code_execution tool"""
|
|||
|
|
add_code_execution_tool = False
|
|||
|
|
for message in messages:
|
|||
|
|
message_content = message.get("content", None)
|
|||
|
|
if message_content and isinstance(message_content, list):
|
|||
|
|
for content in message_content:
|
|||
|
|
content_type = content.get("type", None)
|
|||
|
|
if content_type == "container_upload":
|
|||
|
|
add_code_execution_tool = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if add_code_execution_tool:
|
|||
|
|
## check if code_execution tool is already in tools
|
|||
|
|
for tool in tools:
|
|||
|
|
tool_type = tool.get("type", None)
|
|||
|
|
if (
|
|||
|
|
tool_type
|
|||
|
|
and isinstance(tool_type, str)
|
|||
|
|
and tool_type.startswith("code_execution")
|
|||
|
|
):
|
|||
|
|
return tools
|
|||
|
|
tools.append(
|
|||
|
|
AnthropicCodeExecutionTool(
|
|||
|
|
name="code_execution",
|
|||
|
|
type="code_execution_20250522",
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
return tools
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
"""
|
|||
|
|
existing_beta = headers.get("anthropic-beta")
|
|||
|
|
if existing_beta is None:
|
|||
|
|
headers["anthropic-beta"] = beta_value
|
|||
|
|
return
|
|||
|
|
existing_values = [beta.strip() for beta in existing_beta.split(",")]
|
|||
|
|
if beta_value not in existing_values:
|
|||
|
|
headers["anthropic-beta"] = f"{existing_beta}, {beta_value}"
|
|||
|
|
|
|||
|
|
def _ensure_context_management_beta_header(
|
|||
|
|
self, headers: dict, context_management: object
|
|||
|
|
) -> None:
|
|||
|
|
"""
|
|||
|
|
Add appropriate beta headers based on context_management edits.
|
|||
|
|
"""
|
|||
|
|
edits = []
|
|||
|
|
# If anthropic format (dict with "edits" key)
|
|||
|
|
if isinstance(context_management, dict) and "edits" in context_management:
|
|||
|
|
edits = context_management.get("edits", [])
|
|||
|
|
# If OpenAI format: list of context management entries
|
|||
|
|
elif isinstance(context_management, list):
|
|||
|
|
edits = context_management
|
|||
|
|
# Defensive: ignore/fallback if context_management not valid
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
has_compact = False
|
|||
|
|
has_other = False
|
|||
|
|
|
|||
|
|
for edit in edits:
|
|||
|
|
edit_type = edit.get("type", "")
|
|||
|
|
if edit_type == "compact_20260112" or edit_type == "compaction":
|
|||
|
|
has_compact = True
|
|||
|
|
else:
|
|||
|
|
has_other = True
|
|||
|
|
|
|||
|
|
# Add compact header if any compact edits/entries exist
|
|||
|
|
if has_compact:
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
headers, ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Add context management header if any other edits/entries exist
|
|||
|
|
if has_other:
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
headers,
|
|||
|
|
ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
if is_vertex_request:
|
|||
|
|
return headers
|
|||
|
|
|
|||
|
|
_tools = optional_params.get("tools", [])
|
|||
|
|
for tool in _tools:
|
|||
|
|
if tool.get("type", None) and tool.get("type").startswith(
|
|||
|
|
ANTHROPIC_HOSTED_TOOLS.WEB_FETCH.value
|
|||
|
|
):
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
headers, ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value
|
|||
|
|
)
|
|||
|
|
elif tool.get("type", None) and tool.get("type").startswith(
|
|||
|
|
ANTHROPIC_HOSTED_TOOLS.MEMORY.value
|
|||
|
|
):
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
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, optional_params["context_management"]
|
|||
|
|
)
|
|||
|
|
if optional_params.get("output_format") is not None:
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
headers, ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value
|
|||
|
|
)
|
|||
|
|
if optional_params.get("speed") == "fast":
|
|||
|
|
self._ensure_beta_header(
|
|||
|
|
headers, ANTHROPIC_BETA_HEADER_VALUES.FAST_MODE_2026_02_01.value
|
|||
|
|
)
|
|||
|
|
return headers
|
|||
|
|
|
|||
|
|
def transform_request(
|
|||
|
|
self,
|
|||
|
|
model: str,
|
|||
|
|
messages: List[AllMessageValues],
|
|||
|
|
optional_params: dict,
|
|||
|
|
litellm_params: dict,
|
|||
|
|
headers: dict,
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
Translate messages to anthropic format.
|
|||
|
|
"""
|
|||
|
|
## VALIDATE REQUEST
|
|||
|
|
"""
|
|||
|
|
Anthropic doesn't support tool calling without `tools=` param specified.
|
|||
|
|
"""
|
|||
|
|
from litellm.litellm_core_utils.prompt_templates.factory import (
|
|||
|
|
anthropic_messages_pt,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
"tools" not in optional_params
|
|||
|
|
and messages is not None
|
|||
|
|
and has_tool_call_blocks(messages)
|
|||
|
|
):
|
|||
|
|
if litellm.modify_params:
|
|||
|
|
optional_params["tools"], _ = self._map_tools(
|
|||
|
|
add_dummy_tool(custom_llm_provider="anthropic")
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
raise litellm.UnsupportedParamsError(
|
|||
|
|
message="Anthropic doesn't support tool calling without `tools=` param specified. Pass `tools=` param OR set `litellm.modify_params = True` // `litellm_settings::modify_params: True` to add dummy tool to the request.",
|
|||
|
|
model="",
|
|||
|
|
llm_provider="anthropic",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Drop thinking param if thinking is enabled but thinking_blocks are missing
|
|||
|
|
# This prevents the error: "Expected thinking or redacted_thinking, but found tool_use"
|
|||
|
|
#
|
|||
|
|
# IMPORTANT: Only drop thinking if NO assistant messages have thinking_blocks.
|
|||
|
|
# If any message has thinking_blocks, we must keep thinking enabled, otherwise
|
|||
|
|
# Anthropic errors with: "When thinking is disabled, an assistant message cannot contain thinking"
|
|||
|
|
# Related issue: https://github.com/BerriAI/litellm/issues/18926
|
|||
|
|
if (
|
|||
|
|
optional_params.get("thinking") is not None
|
|||
|
|
and messages is not None
|
|||
|
|
and last_assistant_with_tool_calls_has_no_thinking_blocks(messages)
|
|||
|
|
and not any_assistant_message_has_thinking_blocks(messages)
|
|||
|
|
):
|
|||
|
|
if litellm.modify_params:
|
|||
|
|
optional_params.pop("thinking", None)
|
|||
|
|
litellm.verbose_logger.warning(
|
|||
|
|
"Dropping 'thinking' param because the last assistant message with tool_calls "
|
|||
|
|
"has no thinking_blocks. The model won't use extended thinking for this turn."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
headers = self.update_headers_with_optional_anthropic_beta(
|
|||
|
|
headers=headers, optional_params=optional_params
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Separate system prompt from rest of message
|
|||
|
|
anthropic_system_message_list = self.translate_system_message(messages=messages)
|
|||
|
|
# Handling anthropic API Prompt Caching
|
|||
|
|
if len(anthropic_system_message_list) > 0:
|
|||
|
|
optional_params["system"] = anthropic_system_message_list
|
|||
|
|
# Format rest of message according to anthropic guidelines
|
|||
|
|
try:
|
|||
|
|
anthropic_messages = anthropic_messages_pt(
|
|||
|
|
model=model,
|
|||
|
|
messages=messages,
|
|||
|
|
llm_provider=self.custom_llm_provider or "anthropic",
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
raise AnthropicError(
|
|||
|
|
status_code=400,
|
|||
|
|
message="{}\nReceived Messages={}".format(str(e), messages),
|
|||
|
|
) # don't use verbose_logger.exception, if exception is raised
|
|||
|
|
|
|||
|
|
## Add code_execution tool if container_upload is in messages
|
|||
|
|
_tools = (
|
|||
|
|
cast(
|
|||
|
|
Optional[List[Union[AllAnthropicToolsValues, Dict]]],
|
|||
|
|
optional_params.get("tools"),
|
|||
|
|
)
|
|||
|
|
or []
|
|||
|
|
)
|
|||
|
|
tools = self.add_code_execution_tool(messages=anthropic_messages, tools=_tools)
|
|||
|
|
if len(tools) > 1:
|
|||
|
|
optional_params["tools"] = tools
|
|||
|
|
|
|||
|
|
## Load Config
|
|||
|
|
config = litellm.AnthropicConfig.get_config(model=model)
|
|||
|
|
for k, v in config.items():
|
|||
|
|
if (
|
|||
|
|
k not in optional_params
|
|||
|
|
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in
|
|||
|
|
optional_params[k] = v
|
|||
|
|
|
|||
|
|
## Handle user_id in metadata
|
|||
|
|
_litellm_metadata = litellm_params.get("metadata", None)
|
|||
|
|
if (
|
|||
|
|
_litellm_metadata
|
|||
|
|
and isinstance(_litellm_metadata, dict)
|
|||
|
|
and "user_id" in _litellm_metadata
|
|||
|
|
and _litellm_metadata["user_id"] is not None
|
|||
|
|
and _valid_user_id(_litellm_metadata["user_id"])
|
|||
|
|
):
|
|||
|
|
optional_params["metadata"] = {"user_id": _litellm_metadata["user_id"]}
|
|||
|
|
|
|||
|
|
# Remove internal LiteLLM parameters that should not be sent to Anthropic API
|
|||
|
|
optional_params.pop("is_vertex_request", None)
|
|||
|
|
|
|||
|
|
data = {
|
|||
|
|
"model": model,
|
|||
|
|
"messages": anthropic_messages,
|
|||
|
|
**optional_params,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
## Handle output_config (Anthropic-specific parameter)
|
|||
|
|
if "output_config" in optional_params:
|
|||
|
|
output_config = optional_params.get("output_config")
|
|||
|
|
if output_config and isinstance(output_config, dict):
|
|||
|
|
effort = output_config.get("effort")
|
|||
|
|
if effort and effort not in ["high", "medium", "low", "max"]:
|
|||
|
|
raise ValueError(
|
|||
|
|
f"Invalid effort value: {effort}. Must be one of: 'high', 'medium', 'low', 'max'"
|
|||
|
|
)
|
|||
|
|
if effort == "max" and not self._is_opus_4_6_model(model):
|
|||
|
|
raise ValueError(
|
|||
|
|
f"effort='max' is only supported by Claude Opus 4.6. Got model: {model}"
|
|||
|
|
)
|
|||
|
|
data["output_config"] = output_config
|
|||
|
|
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
def _transform_response_for_json_mode(
|
|||
|
|
self,
|
|||
|
|
json_mode: Optional[bool],
|
|||
|
|
tool_calls: List[ChatCompletionToolCallChunk],
|
|||
|
|
) -> Optional[LitellmMessage]:
|
|||
|
|
_message: Optional[LitellmMessage] = None
|
|||
|
|
if json_mode is True and len(tool_calls) == 1:
|
|||
|
|
# check if tool name is the default tool name
|
|||
|
|
json_mode_content_str: Optional[str] = None
|
|||
|
|
if (
|
|||
|
|
"name" in tool_calls[0]["function"]
|
|||
|
|
and tool_calls[0]["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME
|
|||
|
|
):
|
|||
|
|
json_mode_content_str = tool_calls[0]["function"].get("arguments")
|
|||
|
|
if json_mode_content_str is not None:
|
|||
|
|
_message = AnthropicConfig._convert_tool_response_to_message(
|
|||
|
|
tool_calls=tool_calls,
|
|||
|
|
)
|
|||
|
|
return _message
|
|||
|
|
|
|||
|
|
def extract_response_content(
|
|||
|
|
self, completion_response: dict
|
|||
|
|
) -> Tuple[
|
|||
|
|
str,
|
|||
|
|
Optional[List[Any]],
|
|||
|
|
Optional[
|
|||
|
|
List[
|
|||
|
|
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]
|
|||
|
|
]
|
|||
|
|
],
|
|||
|
|
Optional[str],
|
|||
|
|
List[ChatCompletionToolCallChunk],
|
|||
|
|
Optional[List[Any]],
|
|||
|
|
Optional[List[Any]],
|
|||
|
|
Optional[List[Any]],
|
|||
|
|
]:
|
|||
|
|
text_content = ""
|
|||
|
|
citations: Optional[List[Any]] = None
|
|||
|
|
thinking_blocks: Optional[
|
|||
|
|
List[
|
|||
|
|
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]
|
|||
|
|
]
|
|||
|
|
] = None
|
|||
|
|
reasoning_content: Optional[str] = None
|
|||
|
|
tool_calls: List[ChatCompletionToolCallChunk] = []
|
|||
|
|
web_search_results: Optional[List[Any]] = None
|
|||
|
|
tool_results: Optional[List[Any]] = None
|
|||
|
|
compaction_blocks: Optional[List[Any]] = None
|
|||
|
|
for idx, content in enumerate(completion_response["content"]):
|
|||
|
|
if content["type"] == "text":
|
|||
|
|
text_content += content["text"]
|
|||
|
|
## TOOL CALLING
|
|||
|
|
elif content["type"] == "tool_use" or content["type"] == "server_tool_use":
|
|||
|
|
tool_call = AnthropicConfig.convert_tool_use_to_openai_format(
|
|||
|
|
anthropic_tool_content=content,
|
|||
|
|
index=idx,
|
|||
|
|
)
|
|||
|
|
tool_calls.append(tool_call)
|
|||
|
|
|
|||
|
|
## TOOL RESULTS - handle all tool result types (code execution, etc.)
|
|||
|
|
elif content["type"].endswith("_tool_result"):
|
|||
|
|
# Skip tool_search_tool_result as it's internal metadata
|
|||
|
|
if content["type"] == "tool_search_tool_result":
|
|||
|
|
continue
|
|||
|
|
# Handle web_search_tool_result separately for backwards compatibility
|
|||
|
|
if content["type"] == "web_search_tool_result":
|
|||
|
|
if web_search_results is None:
|
|||
|
|
web_search_results = []
|
|||
|
|
web_search_results.append(content)
|
|||
|
|
elif content["type"] == "web_fetch_tool_result":
|
|||
|
|
if web_search_results is None:
|
|||
|
|
web_search_results = []
|
|||
|
|
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:
|
|||
|
|
tool_results = []
|
|||
|
|
tool_results.append(content)
|
|||
|
|
|
|||
|
|
elif content.get("thinking", None) is not None:
|
|||
|
|
if thinking_blocks is None:
|
|||
|
|
thinking_blocks = []
|
|||
|
|
thinking_blocks.append(cast(ChatCompletionThinkingBlock, content))
|
|||
|
|
elif content["type"] == "redacted_thinking":
|
|||
|
|
if thinking_blocks is None:
|
|||
|
|
thinking_blocks = []
|
|||
|
|
thinking_blocks.append(
|
|||
|
|
cast(ChatCompletionRedactedThinkingBlock, content)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
## COMPACTION
|
|||
|
|
elif content["type"] == "compaction":
|
|||
|
|
if compaction_blocks is None:
|
|||
|
|
compaction_blocks = []
|
|||
|
|
compaction_blocks.append(content)
|
|||
|
|
|
|||
|
|
## CITATIONS
|
|||
|
|
if content.get("citations") is not None:
|
|||
|
|
if citations is None:
|
|||
|
|
citations = []
|
|||
|
|
citations.append(
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
**citation,
|
|||
|
|
"supported_text": content.get("text", ""),
|
|||
|
|
}
|
|||
|
|
for citation in content["citations"]
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
if thinking_blocks is not None:
|
|||
|
|
reasoning_content = ""
|
|||
|
|
for block in thinking_blocks:
|
|||
|
|
thinking_content = cast(Optional[str], block.get("thinking"))
|
|||
|
|
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,
|
|||
|
|
compaction_blocks,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def calculate_usage(
|
|||
|
|
self,
|
|||
|
|
usage_object: dict,
|
|||
|
|
reasoning_content: Optional[str],
|
|||
|
|
completion_response: Optional[dict] = None,
|
|||
|
|
speed: Optional[str] = None,
|
|||
|
|
) -> Usage:
|
|||
|
|
# NOTE: Sometimes the usage object has None set explicitly for token counts, meaning .get() & key access returns None, and we need to account for this
|
|||
|
|
prompt_tokens = usage_object.get("input_tokens", 0) or 0
|
|||
|
|
completion_tokens = usage_object.get("output_tokens", 0) or 0
|
|||
|
|
_usage = usage_object
|
|||
|
|
cache_creation_input_tokens: int = 0
|
|||
|
|
cache_read_input_tokens: int = 0
|
|||
|
|
cache_creation_token_details: Optional[CacheCreationTokenDetails] = None
|
|||
|
|
web_search_requests: Optional[int] = None
|
|||
|
|
tool_search_requests: Optional[int] = None
|
|||
|
|
inference_geo: Optional[str] = None
|
|||
|
|
if "inference_geo" in _usage and _usage["inference_geo"] is not None:
|
|||
|
|
inference_geo = _usage["inference_geo"]
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
"cache_creation_input_tokens" in _usage
|
|||
|
|
and _usage["cache_creation_input_tokens"] is not None
|
|||
|
|
):
|
|||
|
|
cache_creation_input_tokens = _usage["cache_creation_input_tokens"]
|
|||
|
|
prompt_tokens += cache_creation_input_tokens
|
|||
|
|
if (
|
|||
|
|
"cache_read_input_tokens" in _usage
|
|||
|
|
and _usage["cache_read_input_tokens"] is not None
|
|||
|
|
):
|
|||
|
|
cache_read_input_tokens = _usage["cache_read_input_tokens"]
|
|||
|
|
prompt_tokens += cache_read_input_tokens
|
|||
|
|
if "server_tool_use" in _usage and _usage["server_tool_use"] is not None:
|
|||
|
|
if (
|
|||
|
|
"web_search_requests" in _usage["server_tool_use"]
|
|||
|
|
and _usage["server_tool_use"]["web_search_requests"] is not None
|
|||
|
|
):
|
|||
|
|
web_search_requests = cast(
|
|||
|
|
int, _usage["server_tool_use"]["web_search_requests"]
|
|||
|
|
)
|
|||
|
|
if (
|
|||
|
|
"tool_search_requests" in _usage["server_tool_use"]
|
|||
|
|
and _usage["server_tool_use"]["tool_search_requests"] is not None
|
|||
|
|
):
|
|||
|
|
tool_search_requests = cast(
|
|||
|
|
int, _usage["server_tool_use"]["tool_search_requests"]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Count tool_search_requests from content blocks if not in usage
|
|||
|
|
# Anthropic doesn't always include tool_search_requests in the usage object
|
|||
|
|
if tool_search_requests is None and completion_response is not None:
|
|||
|
|
tool_search_count = 0
|
|||
|
|
for content in completion_response.get("content", []):
|
|||
|
|
if content.get("type") == "server_tool_use":
|
|||
|
|
tool_name = content.get("name", "")
|
|||
|
|
if "tool_search" in tool_name:
|
|||
|
|
tool_search_count += 1
|
|||
|
|
if tool_search_count > 0:
|
|||
|
|
tool_search_requests = tool_search_count
|
|||
|
|
|
|||
|
|
if "cache_creation" in _usage and _usage["cache_creation"] is not None:
|
|||
|
|
cache_creation_token_details = CacheCreationTokenDetails(
|
|||
|
|
ephemeral_5m_input_tokens=_usage["cache_creation"].get(
|
|||
|
|
"ephemeral_5m_input_tokens"
|
|||
|
|
),
|
|||
|
|
ephemeral_1h_input_tokens=_usage["cache_creation"].get(
|
|||
|
|
"ephemeral_1h_input_tokens"
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
prompt_tokens_details = PromptTokensDetailsWrapper(
|
|||
|
|
cached_tokens=cache_read_input_tokens,
|
|||
|
|
cache_creation_tokens=cache_creation_input_tokens,
|
|||
|
|
cache_creation_token_details=cache_creation_token_details,
|
|||
|
|
)
|
|||
|
|
# Always populate completion_token_details, not just when there's reasoning_content
|
|||
|
|
reasoning_tokens = (
|
|||
|
|
token_counter(text=reasoning_content, count_response_tokens=True)
|
|||
|
|
if reasoning_content
|
|||
|
|
else 0
|
|||
|
|
)
|
|||
|
|
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
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
total_tokens = prompt_tokens + completion_tokens
|
|||
|
|
|
|||
|
|
usage = Usage(
|
|||
|
|
prompt_tokens=prompt_tokens,
|
|||
|
|
completion_tokens=completion_tokens,
|
|||
|
|
total_tokens=total_tokens,
|
|||
|
|
prompt_tokens_details=prompt_tokens_details,
|
|||
|
|
cache_creation_input_tokens=cache_creation_input_tokens,
|
|||
|
|
cache_read_input_tokens=cache_read_input_tokens,
|
|||
|
|
completion_tokens_details=completion_token_details,
|
|||
|
|
server_tool_use=(
|
|||
|
|
ServerToolUse(
|
|||
|
|
web_search_requests=web_search_requests,
|
|||
|
|
tool_search_requests=tool_search_requests,
|
|||
|
|
)
|
|||
|
|
if (web_search_requests is not None or tool_search_requests is not None)
|
|||
|
|
else None
|
|||
|
|
),
|
|||
|
|
inference_geo=inference_geo,
|
|||
|
|
speed=speed,
|
|||
|
|
)
|
|||
|
|
return usage
|
|||
|
|
|
|||
|
|
def transform_parsed_response(
|
|||
|
|
self,
|
|||
|
|
completion_response: dict,
|
|||
|
|
raw_response: httpx.Response,
|
|||
|
|
model_response: ModelResponse,
|
|||
|
|
json_mode: Optional[bool] = None,
|
|||
|
|
prefix_prompt: Optional[str] = None,
|
|||
|
|
speed: Optional[str] = None,
|
|||
|
|
):
|
|||
|
|
_hidden_params: Dict = {}
|
|||
|
|
_hidden_params["additional_headers"] = process_anthropic_headers(
|
|||
|
|
dict(raw_response.headers)
|
|||
|
|
)
|
|||
|
|
if "error" in completion_response:
|
|||
|
|
response_headers = getattr(raw_response, "headers", None)
|
|||
|
|
raise AnthropicError(
|
|||
|
|
message=str(completion_response["error"]),
|
|||
|
|
status_code=raw_response.status_code,
|
|||
|
|
headers=response_headers,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
text_content = ""
|
|||
|
|
citations: Optional[List[Any]] = None
|
|||
|
|
thinking_blocks: Optional[
|
|||
|
|
List[
|
|||
|
|
Union[
|
|||
|
|
ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
] = None
|
|||
|
|
reasoning_content: Optional[str] = None
|
|||
|
|
tool_calls: List[ChatCompletionToolCallChunk] = []
|
|||
|
|
|
|||
|
|
(
|
|||
|
|
text_content,
|
|||
|
|
citations,
|
|||
|
|
thinking_blocks,
|
|||
|
|
reasoning_content,
|
|||
|
|
tool_calls,
|
|||
|
|
web_search_results,
|
|||
|
|
tool_results,
|
|||
|
|
compaction_blocks,
|
|||
|
|
) = self.extract_response_content(completion_response=completion_response)
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
prefix_prompt is not None
|
|||
|
|
and not text_content.startswith(prefix_prompt)
|
|||
|
|
and not litellm.disable_add_prefix_to_prompt
|
|||
|
|
):
|
|||
|
|
text_content = prefix_prompt + text_content
|
|||
|
|
|
|||
|
|
context_management: Optional[Dict] = completion_response.get(
|
|||
|
|
"context_management"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
container: Optional[Dict] = completion_response.get("container")
|
|||
|
|
|
|||
|
|
provider_specific_fields: Dict[str, Any] = {
|
|||
|
|
"citations": citations,
|
|||
|
|
"thinking_blocks": thinking_blocks,
|
|||
|
|
}
|
|||
|
|
if context_management is not None:
|
|||
|
|
provider_specific_fields["context_management"] = context_management
|
|||
|
|
if web_search_results is not None:
|
|||
|
|
provider_specific_fields["web_search_results"] = web_search_results
|
|||
|
|
if tool_results is not None:
|
|||
|
|
provider_specific_fields["tool_results"] = tool_results
|
|||
|
|
if container is not None:
|
|||
|
|
provider_specific_fields["container"] = container
|
|||
|
|
if compaction_blocks is not None:
|
|||
|
|
provider_specific_fields["compaction_blocks"] = compaction_blocks
|
|||
|
|
|
|||
|
|
_message = litellm.Message(
|
|||
|
|
tool_calls=tool_calls,
|
|||
|
|
content=text_content or None,
|
|||
|
|
provider_specific_fields=provider_specific_fields,
|
|||
|
|
thinking_blocks=thinking_blocks,
|
|||
|
|
reasoning_content=reasoning_content,
|
|||
|
|
)
|
|||
|
|
_message.provider_specific_fields = provider_specific_fields
|
|||
|
|
|
|||
|
|
## HANDLE JSON MODE - anthropic returns single function call
|
|||
|
|
json_mode_message = self._transform_response_for_json_mode(
|
|||
|
|
json_mode=json_mode,
|
|||
|
|
tool_calls=tool_calls,
|
|||
|
|
)
|
|||
|
|
if json_mode_message is not None:
|
|||
|
|
completion_response["stop_reason"] = "stop"
|
|||
|
|
_message = json_mode_message
|
|||
|
|
|
|||
|
|
model_response.choices[0].message = _message # type: ignore
|
|||
|
|
model_response._hidden_params["original_response"] = completion_response[
|
|||
|
|
"content"
|
|||
|
|
] # allow user to access raw anthropic tool calling response
|
|||
|
|
|
|||
|
|
model_response.choices[0].finish_reason = cast(
|
|||
|
|
OpenAIChatCompletionFinishReason,
|
|||
|
|
map_finish_reason(completion_response["stop_reason"]),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
## CALCULATING USAGE
|
|||
|
|
usage = self.calculate_usage(
|
|||
|
|
usage_object=completion_response["usage"],
|
|||
|
|
reasoning_content=reasoning_content,
|
|||
|
|
completion_response=completion_response,
|
|||
|
|
speed=speed,
|
|||
|
|
)
|
|||
|
|
setattr(model_response, "usage", usage) # type: ignore
|
|||
|
|
|
|||
|
|
model_response.created = int(time.time())
|
|||
|
|
model_response.model = completion_response["model"]
|
|||
|
|
|
|||
|
|
model_response._hidden_params = _hidden_params
|
|||
|
|
return model_response
|
|||
|
|
|
|||
|
|
def get_prefix_prompt(self, messages: List[AllMessageValues]) -> Optional[str]:
|
|||
|
|
"""
|
|||
|
|
Get the prefix prompt from the messages.
|
|||
|
|
|
|||
|
|
Check last message
|
|||
|
|
- if it's assistant message, with 'prefix': true, return the content
|
|||
|
|
|
|||
|
|
E.g. : {"role": "assistant", "content": "Argentina", "prefix": True}
|
|||
|
|
"""
|
|||
|
|
if len(messages) == 0:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
message = messages[-1]
|
|||
|
|
message_content = message.get("content")
|
|||
|
|
if (
|
|||
|
|
message["role"] == "assistant"
|
|||
|
|
and message.get("prefix", False)
|
|||
|
|
and isinstance(message_content, str)
|
|||
|
|
):
|
|||
|
|
return message_content
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def transform_response(
|
|||
|
|
self,
|
|||
|
|
model: str,
|
|||
|
|
raw_response: httpx.Response,
|
|||
|
|
model_response: ModelResponse,
|
|||
|
|
logging_obj: LoggingClass,
|
|||
|
|
request_data: Dict,
|
|||
|
|
messages: List[AllMessageValues],
|
|||
|
|
optional_params: Dict,
|
|||
|
|
litellm_params: dict,
|
|||
|
|
encoding: Any,
|
|||
|
|
api_key: Optional[str] = None,
|
|||
|
|
json_mode: Optional[bool] = None,
|
|||
|
|
) -> ModelResponse:
|
|||
|
|
## LOGGING
|
|||
|
|
logging_obj.post_call(
|
|||
|
|
input=messages,
|
|||
|
|
api_key=api_key,
|
|||
|
|
original_response=raw_response.text,
|
|||
|
|
additional_args={"complete_input_dict": request_data},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
## RESPONSE OBJECT
|
|||
|
|
try:
|
|||
|
|
completion_response = raw_response.json()
|
|||
|
|
except Exception as e:
|
|||
|
|
response_headers = getattr(raw_response, "headers", None)
|
|||
|
|
raise AnthropicError(
|
|||
|
|
message="Unable to get json response - {}, Original Response: {}".format(
|
|||
|
|
str(e), raw_response.text
|
|||
|
|
),
|
|||
|
|
status_code=raw_response.status_code,
|
|||
|
|
headers=response_headers,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
prefix_prompt = self.get_prefix_prompt(messages=messages)
|
|||
|
|
speed = optional_params.get("speed")
|
|||
|
|
|
|||
|
|
model_response = self.transform_parsed_response(
|
|||
|
|
completion_response=completion_response,
|
|||
|
|
raw_response=raw_response,
|
|||
|
|
model_response=model_response,
|
|||
|
|
json_mode=json_mode,
|
|||
|
|
prefix_prompt=prefix_prompt,
|
|||
|
|
speed=speed,
|
|||
|
|
)
|
|||
|
|
return model_response
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _convert_tool_response_to_message(
|
|||
|
|
tool_calls: List[ChatCompletionToolCallChunk],
|
|||
|
|
) -> Optional[LitellmMessage]:
|
|||
|
|
"""
|
|||
|
|
In JSON mode, Anthropic API returns JSON schema as a tool call, we need to convert it to a message to follow the OpenAI format
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
## HANDLE JSON MODE - anthropic returns single function call
|
|||
|
|
json_mode_content_str: Optional[str] = tool_calls[0]["function"].get(
|
|||
|
|
"arguments"
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
if json_mode_content_str is not None:
|
|||
|
|
args = json.loads(json_mode_content_str)
|
|||
|
|
if (
|
|||
|
|
isinstance(args, dict)
|
|||
|
|
and (values := args.get("values")) is not None
|
|||
|
|
):
|
|||
|
|
_message = litellm.Message(content=json.dumps(values))
|
|||
|
|
return _message
|
|||
|
|
else:
|
|||
|
|
# a lot of the times the `values` key is not present in the tool response
|
|||
|
|
# relevant issue: https://github.com/BerriAI/litellm/issues/6741
|
|||
|
|
_message = litellm.Message(content=json.dumps(args))
|
|||
|
|
return _message
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
# json decode error does occur, return the original tool response str
|
|||
|
|
return litellm.Message(content=json_mode_content_str)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def get_error_class(
|
|||
|
|
self, error_message: str, status_code: int, headers: Union[Dict, httpx.Headers]
|
|||
|
|
) -> BaseLLMException:
|
|||
|
|
return AnthropicError(
|
|||
|
|
status_code=status_code,
|
|||
|
|
message=error_message,
|
|||
|
|
headers=cast(httpx.Headers, headers),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _valid_user_id(user_id: str) -> bool:
|
|||
|
|
"""
|
|||
|
|
Validate that user_id is not an email or phone number.
|
|||
|
|
Returns: bool: True if valid (not email or phone), False otherwise
|
|||
|
|
"""
|
|||
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|||
|
|
phone_pattern = r"^\+?[\d\s\(\)-]{7,}$"
|
|||
|
|
|
|||
|
|
if re.match(email_pattern, user_id):
|
|||
|
|
return False
|
|||
|
|
if re.match(phone_pattern, user_id):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|