Skip to content
15 changes: 9 additions & 6 deletions litellm/integrations/opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,9 @@ def get_tracer_to_use_for_request(self, kwargs: dict) -> Tracer:

def _get_dynamic_otel_headers_from_kwargs(self, kwargs) -> Optional[dict]:
"""Extract dynamic headers from kwargs if available."""
standard_callback_dynamic_params: Optional[StandardCallbackDynamicParams] = (
kwargs.get("standard_callback_dynamic_params")
)
standard_callback_dynamic_params: Optional[
StandardCallbackDynamicParams
] = kwargs.get("standard_callback_dynamic_params")

if not standard_callback_dynamic_params:
return None
Expand All @@ -619,7 +619,9 @@ def _get_tracer_with_dynamic_headers(self, dynamic_headers: dict):
# Prevents thread exhaustion by reusing providers for the same credential sets (e.g. per-team keys)
cache_key = str(sorted(dynamic_headers.items()))
if cache_key in self._tracer_provider_cache:
return self._tracer_provider_cache[cache_key].get_tracer(LITELLM_TRACER_NAME)
return self._tracer_provider_cache[cache_key].get_tracer(
LITELLM_TRACER_NAME
)

# Create a temporary tracer provider with dynamic headers
temp_provider = TracerProvider(resource=self._get_litellm_resource(self.config))
Expand Down Expand Up @@ -1618,7 +1620,6 @@ def set_attributes( # noqa: PLR0915

for idx, choice in enumerate(response_obj.get("choices")):
if choice.get("finish_reason"):

message = choice.get("message")
tool_calls = message.get("tool_calls")
if tool_calls:
Expand All @@ -1631,7 +1632,9 @@ def set_attributes( # noqa: PLR0915
)

except Exception as e:
self.handle_callback_failure(callback_name=self.callback_name or "opentelemetry")
self.handle_callback_failure(
callback_name=self.callback_name or "opentelemetry"
)
verbose_logger.exception(
"OpenTelemetry logging error in set_attributes %s", str(e)
)
Expand Down
100 changes: 61 additions & 39 deletions litellm/llms/bedrock/chat/converse_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.types.llms.bedrock import *

from ..common_utils import is_claude_4_5_on_bedrock
from litellm.types.llms.openai import (
AllMessageValues,
ChatCompletionAssistantMessage,
Expand Down Expand Up @@ -306,9 +308,7 @@ def _is_nova_lite_2_model(self, model: str) -> bool:
return "nova-2-lite" in model_without_region

def _map_web_search_options(
self,
web_search_options: dict,
model: str
self, web_search_options: dict, model: str
) -> Optional[BedrockToolBlock]:
"""
Map web_search_options to Nova grounding systemTool.
Expand Down Expand Up @@ -634,18 +634,18 @@ def _filter_unsupported_beta_headers_for_bedrock(
Filtered list of beta headers
"""
filtered_betas = []

# 1. Filter out beta headers that are universally unsupported on Bedrock Converse
for beta in beta_list:
should_keep = True
for unsupported_pattern in UNSUPPORTED_BEDROCK_CONVERSE_BETA_PATTERNS:
if unsupported_pattern in beta.lower():
should_keep = False
break

if should_keep:
filtered_betas.append(beta)

return filtered_betas

def _separate_computer_use_tools(
Expand Down Expand Up @@ -808,11 +808,11 @@ def map_openai_params(
if param == "web_search_options" and isinstance(value, dict):
# Note: we use `isinstance(value, dict)` instead of `value and isinstance(value, dict)`
# because empty dict {} is falsy but is a valid way to enable Nova grounding
grounding_tool = self._map_web_search_options(value, model)
if grounding_tool is not None:
optional_params = self._add_tools_to_optional_params(
optional_params=optional_params, tools=[grounding_tool]
)
grounding_tool = self._map_web_search_options(value, model)
if grounding_tool is not None:
optional_params = self._add_tools_to_optional_params(
optional_params=optional_params, tools=[grounding_tool]
)

# Only update thinking tokens for non-GPT-OSS models and non-Nova-Lite-2 models
# Nova Lite 2 handles token budgeting differently through reasoningConfig
Expand Down Expand Up @@ -926,6 +926,7 @@ def _get_cache_point_block(
ChatCompletionAssistantMessage,
],
block_type: Literal["system"],
model: Optional[str] = None,
) -> Optional[SystemContentBlock]:
pass

Expand All @@ -939,6 +940,7 @@ def _get_cache_point_block(
ChatCompletionAssistantMessage,
],
block_type: Literal["content_block"],
model: Optional[str] = None,
) -> Optional[ContentBlock]:
pass

Expand All @@ -951,16 +953,26 @@ def _get_cache_point_block(
ChatCompletionAssistantMessage,
],
block_type: Literal["system", "content_block"],
model: Optional[str] = None,
) -> Optional[Union[SystemContentBlock, ContentBlock]]:
if message_block.get("cache_control", None) is None:
cache_control = message_block.get("cache_control", None)
if cache_control is None:
return None

cache_point = CachePointBlock(type="default")
if isinstance(cache_control, dict) and "ttl" in cache_control:
ttl = cache_control["ttl"]
if ttl in ["5m", "1h"] and model is not None:
if is_claude_4_5_on_bedrock(model):
cache_point["ttl"] = ttl

if block_type == "system":
return SystemContentBlock(cachePoint=CachePointBlock(type="default"))
return SystemContentBlock(cachePoint=cache_point)
else:
return ContentBlock(cachePoint=CachePointBlock(type="default"))
return ContentBlock(cachePoint=cache_point)

def _transform_system_message(
self, messages: List[AllMessageValues]
self, messages: List[AllMessageValues], model: Optional[str] = None
) -> Tuple[List[AllMessageValues], List[SystemContentBlock]]:
system_prompt_indices = []
system_content_blocks: List[SystemContentBlock] = []
Expand All @@ -972,7 +984,7 @@ def _transform_system_message(
SystemContentBlock(text=message["content"])
)
cache_block = self._get_cache_point_block(
message, block_type="system"
message, block_type="system", model=model
)
if cache_block:
system_content_blocks.append(cache_block)
Expand All @@ -983,7 +995,7 @@ def _transform_system_message(
SystemContentBlock(text=m["text"])
)
cache_block = self._get_cache_point_block(
m, block_type="system"
m, block_type="system", model=model
)
if cache_block:
system_content_blocks.append(cache_block)
Expand Down Expand Up @@ -1137,13 +1149,13 @@ def _process_tools_and_beta(
if beta not in seen:
unique_betas.append(beta)
seen.add(beta)

# Filter out unsupported beta headers for Bedrock Converse API
filtered_betas = self._filter_unsupported_beta_headers_for_bedrock(
model=model,
beta_list=unique_betas,
)

additional_request_params["anthropic_beta"] = filtered_betas

return bedrock_tools, anthropic_beta_list
Expand Down Expand Up @@ -1196,9 +1208,11 @@ def _transform_request_helper(
)

# Prepare and separate parameters
inference_params, additional_request_params, request_metadata = self._prepare_request_params(
optional_params, model
)
(
inference_params,
additional_request_params,
request_metadata,
) = self._prepare_request_params(optional_params, model)

original_tools = inference_params.pop("tools", [])

Expand Down Expand Up @@ -1250,7 +1264,9 @@ async def _async_transform_request(
litellm_params: dict,
headers: Optional[dict] = None,
) -> RequestObject:
messages, system_content_blocks = self._transform_system_message(messages)
messages, system_content_blocks = self._transform_system_message(
messages, model=model
)

# Convert last user message to guarded_text if guardrailConfig is present
messages = self._convert_consecutive_user_messages_to_guarded_text(
Expand Down Expand Up @@ -1306,7 +1322,9 @@ def _transform_request(
litellm_params: dict,
headers: Optional[dict] = None,
) -> RequestObject:
messages, system_content_blocks = self._transform_system_message(messages)
messages, system_content_blocks = self._transform_system_message(
messages, model=model
)

# Convert last user message to guarded_text if guardrailConfig is present
messages = self._convert_consecutive_user_messages_to_guarded_text(
Expand Down Expand Up @@ -1484,7 +1502,9 @@ def apply_tool_call_transformation_if_needed(

return message, returned_finish_reason

def _translate_message_content(self, content_blocks: List[ContentBlock]) -> Tuple[
def _translate_message_content(
self, content_blocks: List[ContentBlock]
) -> Tuple[
str,
List[ChatCompletionToolCallChunk],
Optional[List[BedrockConverseReasoningContentBlock]],
Expand All @@ -1501,9 +1521,9 @@ def _translate_message_content(self, content_blocks: List[ContentBlock]) -> Tupl
"""
content_str = ""
tools: List[ChatCompletionToolCallChunk] = []
reasoningContentBlocks: Optional[List[BedrockConverseReasoningContentBlock]] = (
None
)
reasoningContentBlocks: Optional[
List[BedrockConverseReasoningContentBlock]
] = None
citationsContentBlocks: Optional[List[CitationsContentBlock]] = None
for idx, content in enumerate(content_blocks):
"""
Expand Down Expand Up @@ -1557,7 +1577,7 @@ def _translate_message_content(self, content_blocks: List[ContentBlock]) -> Tupl

return content_str, tools, reasoningContentBlocks, citationsContentBlocks

def _transform_response( # noqa: PLR0915
def _transform_response( # noqa: PLR0915
self,
model: str,
response: httpx.Response,
Expand Down Expand Up @@ -1630,9 +1650,9 @@ def _transform_response( # noqa: PLR0915
chat_completion_message: ChatCompletionResponseMessage = {"role": "assistant"}
content_str = ""
tools: List[ChatCompletionToolCallChunk] = []
reasoningContentBlocks: Optional[List[BedrockConverseReasoningContentBlock]] = (
None
)
reasoningContentBlocks: Optional[
List[BedrockConverseReasoningContentBlock]
] = None
citationsContentBlocks: Optional[List[CitationsContentBlock]] = None

if message is not None:
Expand All @@ -1651,15 +1671,17 @@ def _transform_response( # noqa: PLR0915
provider_specific_fields["citationsContent"] = citationsContentBlocks

if provider_specific_fields:
chat_completion_message["provider_specific_fields"] = provider_specific_fields
chat_completion_message[
"provider_specific_fields"
] = provider_specific_fields

if reasoningContentBlocks is not None:
chat_completion_message["reasoning_content"] = (
self._transform_reasoning_content(reasoningContentBlocks)
)
chat_completion_message["thinking_blocks"] = (
self._transform_thinking_blocks(reasoningContentBlocks)
)
chat_completion_message[
"reasoning_content"
] = self._transform_reasoning_content(reasoningContentBlocks)
chat_completion_message[
"thinking_blocks"
] = self._transform_thinking_blocks(reasoningContentBlocks)
chat_completion_message["content"] = content_str
if (
json_mode is True
Expand Down
33 changes: 29 additions & 4 deletions litellm/llms/bedrock/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,29 @@ def get_bedrock_base_model(model: str) -> str:
return model


def is_claude_4_5_on_bedrock(model: str) -> bool:
"""
Check if the model is a Claude 4.5 model on Bedrock.
Claude 4.5 models support prompt caching with '5m' and '1h' TTL on Bedrock.
"""
model_lower = model.lower()
claude_4_5_patterns = [
"sonnet-4.5",
"sonnet_4.5",
"sonnet-4-5",
"sonnet_4_5",
"haiku-4.5",
"haiku_4.5",
"haiku-4-5",
"haiku_4_5",
"opus-4.5",
"opus_4.5",
"opus-4-5",
"opus_4_5",
]
return any(pattern in model_lower for pattern in claude_4_5_patterns)


# Import after standalone functions to avoid circular imports
from litellm.llms.bedrock.count_tokens.bedrock_token_counter import BedrockTokenCounter

Expand Down Expand Up @@ -815,21 +838,23 @@ def get_anthropic_beta_from_headers(headers: dict) -> List[str]:
# If it's already a list, return it
if isinstance(anthropic_beta_header, list):
return anthropic_beta_header

# Try to parse as JSON array first (e.g., '["interleaved-thinking-2025-05-14", "claude-code-20250219"]')
if isinstance(anthropic_beta_header, str):
anthropic_beta_header = anthropic_beta_header.strip()
if anthropic_beta_header.startswith("[") and anthropic_beta_header.endswith("]"):
if anthropic_beta_header.startswith("[") and anthropic_beta_header.endswith(
"]"
):
try:
parsed = json.loads(anthropic_beta_header)
if isinstance(parsed, list):
return [str(beta).strip() for beta in parsed]
except json.JSONDecodeError:
pass # Fall through to comma-separated parsing

# Fall back to comma-separated values
return [beta.strip() for beta in anthropic_beta_header.split(",")]

return []


Expand Down
Loading
Loading