Spaces:
Sleeping
Sleeping
""" | |
Translating between OpenAI's `/chat/completion` format and Amazon's `/converse` format | |
""" | |
import copy | |
import time | |
import types | |
from typing import List, Literal, Optional, Tuple, Union, cast, overload | |
import httpx | |
import litellm | |
from litellm.litellm_core_utils.core_helpers import map_finish_reason | |
from litellm.litellm_core_utils.litellm_logging import Logging | |
from litellm.litellm_core_utils.prompt_templates.factory import ( | |
BedrockConverseMessagesProcessor, | |
_bedrock_converse_messages_pt, | |
_bedrock_tools_pt, | |
) | |
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 litellm.types.llms.openai import ( | |
AllMessageValues, | |
ChatCompletionRedactedThinkingBlock, | |
ChatCompletionResponseMessage, | |
ChatCompletionSystemMessage, | |
ChatCompletionThinkingBlock, | |
ChatCompletionToolCallChunk, | |
ChatCompletionToolCallFunctionChunk, | |
ChatCompletionToolParam, | |
ChatCompletionToolParamFunctionChunk, | |
ChatCompletionUserMessage, | |
OpenAIChatCompletionToolParam, | |
OpenAIMessageContentListBlock, | |
) | |
from litellm.types.utils import ModelResponse, PromptTokensDetailsWrapper, Usage | |
from litellm.utils import add_dummy_tool, has_tool_call_blocks | |
from ..common_utils import BedrockError, BedrockModelInfo, get_bedrock_tool_name | |
class AmazonConverseConfig(BaseConfig): | |
""" | |
Reference - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html | |
#2 - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html#conversation-inference-supported-models-features | |
""" | |
maxTokens: Optional[int] | |
stopSequences: Optional[List[str]] | |
temperature: Optional[int] | |
topP: Optional[int] | |
topK: Optional[int] | |
def __init__( | |
self, | |
maxTokens: Optional[int] = None, | |
stopSequences: Optional[List[str]] = None, | |
temperature: Optional[int] = None, | |
topP: Optional[int] = None, | |
topK: Optional[int] = None, | |
) -> None: | |
locals_ = locals().copy() | |
for key, value in locals_.items(): | |
if key != "self" and value is not None: | |
setattr(self.__class__, key, value) | |
def custom_llm_provider(self) -> Optional[str]: | |
return "bedrock_converse" | |
def get_config_blocks(cls) -> dict: | |
return { | |
"guardrailConfig": GuardrailConfigBlock, | |
"performanceConfig": PerformanceConfigBlock, | |
} | |
def get_config(cls): | |
return { | |
k: v | |
for k, v in cls.__dict__.items() | |
if not k.startswith("__") | |
and not isinstance( | |
v, | |
( | |
types.FunctionType, | |
types.BuiltinFunctionType, | |
classmethod, | |
staticmethod, | |
), | |
) | |
and v is not None | |
} | |
def get_supported_openai_params(self, model: str) -> List[str]: | |
supported_params = [ | |
"max_tokens", | |
"max_completion_tokens", | |
"stream", | |
"stream_options", | |
"stop", | |
"temperature", | |
"top_p", | |
"extra_headers", | |
"response_format", | |
] | |
if ( | |
"arn" in model | |
): # we can't infer the model from the arn, so just add all params | |
supported_params.append("tools") | |
supported_params.append("tool_choice") | |
supported_params.append("thinking") | |
supported_params.append("reasoning_effort") | |
return supported_params | |
## Filter out 'cross-region' from model name | |
base_model = BedrockModelInfo.get_base_model(model) | |
if ( | |
base_model.startswith("anthropic") | |
or base_model.startswith("mistral") | |
or base_model.startswith("cohere") | |
or base_model.startswith("meta.llama3-1") | |
or base_model.startswith("meta.llama3-2") | |
or base_model.startswith("meta.llama3-3") | |
or base_model.startswith("amazon.nova") | |
): | |
supported_params.append("tools") | |
if litellm.utils.supports_tool_choice( | |
model=model, custom_llm_provider=self.custom_llm_provider | |
): | |
# only anthropic and mistral support tool choice config. otherwise (E.g. cohere) will fail the call - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html | |
supported_params.append("tool_choice") | |
if ( | |
"claude-3-7" in model | |
): # [TODO]: move to a 'supports_reasoning_content' param from model cost map | |
supported_params.append("thinking") | |
supported_params.append("reasoning_effort") | |
return supported_params | |
def map_tool_choice_values( | |
self, model: str, tool_choice: Union[str, dict], drop_params: bool | |
) -> Optional[ToolChoiceValuesBlock]: | |
if tool_choice == "none": | |
if litellm.drop_params is True or drop_params is True: | |
return None | |
else: | |
raise litellm.utils.UnsupportedParamsError( | |
message="Bedrock doesn't support tool_choice={}. To drop it from the call, set `litellm.drop_params = True.".format( | |
tool_choice | |
), | |
status_code=400, | |
) | |
elif tool_choice == "required": | |
return ToolChoiceValuesBlock(any={}) | |
elif tool_choice == "auto": | |
return ToolChoiceValuesBlock(auto={}) | |
elif isinstance(tool_choice, dict): | |
# only supported for anthropic + mistral models - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html | |
specific_tool = SpecificToolChoiceBlock( | |
name=tool_choice.get("function", {}).get("name", "") | |
) | |
return ToolChoiceValuesBlock(tool=specific_tool) | |
else: | |
raise litellm.utils.UnsupportedParamsError( | |
message="Bedrock doesn't support tool_choice={}. Supported tool_choice values=['auto', 'required', json object]. To drop it from the call, set `litellm.drop_params = True.".format( | |
tool_choice | |
), | |
status_code=400, | |
) | |
def get_supported_image_types(self) -> List[str]: | |
return ["png", "jpeg", "gif", "webp"] | |
def get_supported_document_types(self) -> List[str]: | |
return ["pdf", "csv", "doc", "docx", "xls", "xlsx", "html", "txt", "md"] | |
def get_all_supported_content_types(self) -> List[str]: | |
return self.get_supported_image_types() + self.get_supported_document_types() | |
def _create_json_tool_call_for_response_format( | |
self, | |
json_schema: Optional[dict] = None, | |
schema_name: str = "json_tool_call", | |
description: Optional[str] = None, | |
) -> ChatCompletionToolParam: | |
""" | |
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 | |
""" | |
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 = { | |
"type": "object", | |
"additionalProperties": True, | |
"properties": {}, | |
} | |
else: | |
_input_schema = json_schema | |
tool_param_function_chunk = ChatCompletionToolParamFunctionChunk( | |
name=schema_name, parameters=_input_schema | |
) | |
if description: | |
tool_param_function_chunk["description"] = description | |
_tool = ChatCompletionToolParam( | |
type="function", | |
function=tool_param_function_chunk, | |
) | |
return _tool | |
def _apply_tool_call_transformation( | |
self, | |
tools: List[OpenAIChatCompletionToolParam], | |
model: str, | |
non_default_params: dict, | |
optional_params: dict, | |
): | |
optional_params = self._add_tools_to_optional_params( | |
optional_params=optional_params, tools=tools | |
) | |
if ( | |
"meta.llama3-3-70b-instruct-v1:0" in model | |
and non_default_params.get("stream", False) is True | |
): | |
optional_params["fake_stream"] = True | |
def map_openai_params( | |
self, | |
non_default_params: dict, | |
optional_params: dict, | |
model: str, | |
drop_params: bool, | |
) -> dict: | |
is_thinking_enabled = self.is_thinking_enabled(non_default_params) | |
for param, value in non_default_params.items(): | |
if param == "response_format" and isinstance(value, dict): | |
ignore_response_format_types = ["text"] | |
if value["type"] in ignore_response_format_types: # value is a no-op | |
continue | |
json_schema: Optional[dict] = None | |
schema_name: str = "" | |
description: Optional[str] = None | |
if "response_schema" in value: | |
json_schema = value["response_schema"] | |
schema_name = "json_tool_call" | |
elif "json_schema" in value: | |
json_schema = value["json_schema"]["schema"] | |
schema_name = value["json_schema"]["name"] | |
description = value["json_schema"].get("description") | |
if "type" in value and value["type"] == "text": | |
continue | |
""" | |
Follow similar approach to anthropic - translate to a single tool call. | |
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, | |
schema_name=schema_name if schema_name != "" else "json_tool_call", | |
description=description, | |
) | |
optional_params = self._add_tools_to_optional_params( | |
optional_params=optional_params, tools=[_tool] | |
) | |
if ( | |
litellm.utils.supports_tool_choice( | |
model=model, custom_llm_provider=self.custom_llm_provider | |
) | |
and not is_thinking_enabled | |
): | |
optional_params["tool_choice"] = ToolChoiceValuesBlock( | |
tool=SpecificToolChoiceBlock( | |
name=schema_name if schema_name != "" else "json_tool_call" | |
) | |
) | |
optional_params["json_mode"] = True | |
if non_default_params.get("stream", False) is True: | |
optional_params["fake_stream"] = True | |
if param == "max_tokens" or param == "max_completion_tokens": | |
optional_params["maxTokens"] = value | |
if param == "stream": | |
optional_params["stream"] = value | |
if param == "stop": | |
if isinstance(value, str): | |
if len(value) == 0: # converse raises error for empty strings | |
continue | |
value = [value] | |
optional_params["stopSequences"] = value | |
if param == "temperature": | |
optional_params["temperature"] = value | |
if param == "top_p": | |
optional_params["topP"] = value | |
if param == "tools" and isinstance(value, list): | |
self._apply_tool_call_transformation( | |
tools=cast(List[OpenAIChatCompletionToolParam], value), | |
model=model, | |
non_default_params=non_default_params, | |
optional_params=optional_params, | |
) | |
if param == "tool_choice": | |
_tool_choice_value = self.map_tool_choice_values( | |
model=model, tool_choice=value, drop_params=drop_params # type: ignore | |
) | |
if _tool_choice_value is not None: | |
optional_params["tool_choice"] = _tool_choice_value | |
if param == "thinking": | |
optional_params["thinking"] = value | |
elif param == "reasoning_effort" and isinstance(value, str): | |
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( | |
value | |
) | |
self.update_optional_params_with_thinking_tokens( | |
non_default_params=non_default_params, optional_params=optional_params | |
) | |
return optional_params | |
def _get_cache_point_block( | |
self, | |
message_block: Union[ | |
OpenAIMessageContentListBlock, | |
ChatCompletionUserMessage, | |
ChatCompletionSystemMessage, | |
], | |
block_type: Literal["system"], | |
) -> Optional[SystemContentBlock]: | |
pass | |
def _get_cache_point_block( | |
self, | |
message_block: Union[ | |
OpenAIMessageContentListBlock, | |
ChatCompletionUserMessage, | |
ChatCompletionSystemMessage, | |
], | |
block_type: Literal["content_block"], | |
) -> Optional[ContentBlock]: | |
pass | |
def _get_cache_point_block( | |
self, | |
message_block: Union[ | |
OpenAIMessageContentListBlock, | |
ChatCompletionUserMessage, | |
ChatCompletionSystemMessage, | |
], | |
block_type: Literal["system", "content_block"], | |
) -> Optional[Union[SystemContentBlock, ContentBlock]]: | |
if message_block.get("cache_control", None) is None: | |
return None | |
if block_type == "system": | |
return SystemContentBlock(cachePoint=CachePointBlock(type="default")) | |
else: | |
return ContentBlock(cachePoint=CachePointBlock(type="default")) | |
def _transform_system_message( | |
self, messages: List[AllMessageValues] | |
) -> Tuple[List[AllMessageValues], List[SystemContentBlock]]: | |
system_prompt_indices = [] | |
system_content_blocks: List[SystemContentBlock] = [] | |
for idx, message in enumerate(messages): | |
if message["role"] == "system": | |
system_prompt_indices.append(idx) | |
if isinstance(message["content"], str) and message["content"]: | |
system_content_blocks.append( | |
SystemContentBlock(text=message["content"]) | |
) | |
cache_block = self._get_cache_point_block( | |
message, block_type="system" | |
) | |
if cache_block: | |
system_content_blocks.append(cache_block) | |
elif isinstance(message["content"], list): | |
for m in message["content"]: | |
if m.get("type") == "text" and m.get("text"): | |
system_content_blocks.append( | |
SystemContentBlock(text=m["text"]) | |
) | |
cache_block = self._get_cache_point_block( | |
m, block_type="system" | |
) | |
if cache_block: | |
system_content_blocks.append(cache_block) | |
if len(system_prompt_indices) > 0: | |
for idx in reversed(system_prompt_indices): | |
messages.pop(idx) | |
return messages, system_content_blocks | |
def _transform_inference_params(self, inference_params: dict) -> InferenceConfig: | |
if "top_k" in inference_params: | |
inference_params["topK"] = inference_params.pop("top_k") | |
return InferenceConfig(**inference_params) | |
def _handle_top_k_value(self, model: str, inference_params: dict) -> dict: | |
base_model = BedrockModelInfo.get_base_model(model) | |
val_top_k = None | |
if "topK" in inference_params: | |
val_top_k = inference_params.pop("topK") | |
elif "top_k" in inference_params: | |
val_top_k = inference_params.pop("top_k") | |
if val_top_k: | |
if base_model.startswith("anthropic"): | |
return {"top_k": val_top_k} | |
if base_model.startswith("amazon.nova"): | |
return {"inferenceConfig": {"topK": val_top_k}} | |
return {} | |
def _transform_request_helper( | |
self, | |
model: str, | |
system_content_blocks: List[SystemContentBlock], | |
optional_params: dict, | |
messages: Optional[List[AllMessageValues]] = None, | |
) -> CommonRequestObject: | |
## VALIDATE REQUEST | |
""" | |
Bedrock doesn't support tool calling without `tools=` param specified. | |
""" | |
if ( | |
"tools" not in optional_params | |
and messages is not None | |
and has_tool_call_blocks(messages) | |
): | |
if litellm.modify_params: | |
optional_params["tools"] = add_dummy_tool( | |
custom_llm_provider="bedrock_converse" | |
) | |
else: | |
raise litellm.UnsupportedParamsError( | |
message="Bedrock 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="bedrock", | |
) | |
inference_params = copy.deepcopy(optional_params) | |
supported_converse_params = list( | |
AmazonConverseConfig.__annotations__.keys() | |
) + ["top_k"] | |
supported_tool_call_params = ["tools", "tool_choice"] | |
supported_config_params = list(self.get_config_blocks().keys()) | |
total_supported_params = ( | |
supported_converse_params | |
+ supported_tool_call_params | |
+ supported_config_params | |
) | |
inference_params.pop("json_mode", None) # used for handling json_schema | |
# keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params' | |
additional_request_params = { | |
k: v for k, v in inference_params.items() if k not in total_supported_params | |
} | |
inference_params = { | |
k: v for k, v in inference_params.items() if k in total_supported_params | |
} | |
# Only set the topK value in for models that support it | |
additional_request_params.update( | |
self._handle_top_k_value(model, inference_params) | |
) | |
bedrock_tools: List[ToolBlock] = _bedrock_tools_pt( | |
inference_params.pop("tools", []) | |
) | |
bedrock_tool_config: Optional[ToolConfigBlock] = None | |
if len(bedrock_tools) > 0: | |
tool_choice_values: ToolChoiceValuesBlock = inference_params.pop( | |
"tool_choice", None | |
) | |
bedrock_tool_config = ToolConfigBlock( | |
tools=bedrock_tools, | |
) | |
if tool_choice_values is not None: | |
bedrock_tool_config["toolChoice"] = tool_choice_values | |
data: CommonRequestObject = { | |
"additionalModelRequestFields": additional_request_params, | |
"system": system_content_blocks, | |
"inferenceConfig": self._transform_inference_params( | |
inference_params=inference_params | |
), | |
} | |
# Handle all config blocks | |
for config_name, config_class in self.get_config_blocks().items(): | |
config_value = inference_params.pop(config_name, None) | |
if config_value is not None: | |
data[config_name] = config_class(**config_value) # type: ignore | |
# Tool Config | |
if bedrock_tool_config is not None: | |
data["toolConfig"] = bedrock_tool_config | |
return data | |
async def _async_transform_request( | |
self, | |
model: str, | |
messages: List[AllMessageValues], | |
optional_params: dict, | |
litellm_params: dict, | |
) -> RequestObject: | |
messages, system_content_blocks = self._transform_system_message(messages) | |
## TRANSFORMATION ## | |
_data: CommonRequestObject = self._transform_request_helper( | |
model=model, | |
system_content_blocks=system_content_blocks, | |
optional_params=optional_params, | |
messages=messages, | |
) | |
bedrock_messages = ( | |
await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( | |
messages=messages, | |
model=model, | |
llm_provider="bedrock_converse", | |
user_continue_message=litellm_params.pop("user_continue_message", None), | |
) | |
) | |
data: RequestObject = {"messages": bedrock_messages, **_data} | |
return data | |
def transform_request( | |
self, | |
model: str, | |
messages: List[AllMessageValues], | |
optional_params: dict, | |
litellm_params: dict, | |
headers: dict, | |
) -> dict: | |
return cast( | |
dict, | |
self._transform_request( | |
model=model, | |
messages=messages, | |
optional_params=optional_params, | |
litellm_params=litellm_params, | |
), | |
) | |
def _transform_request( | |
self, | |
model: str, | |
messages: List[AllMessageValues], | |
optional_params: dict, | |
litellm_params: dict, | |
) -> RequestObject: | |
messages, system_content_blocks = self._transform_system_message(messages) | |
_data: CommonRequestObject = self._transform_request_helper( | |
model=model, | |
system_content_blocks=system_content_blocks, | |
optional_params=optional_params, | |
messages=messages, | |
) | |
## TRANSFORMATION ## | |
bedrock_messages: List[MessageBlock] = _bedrock_converse_messages_pt( | |
messages=messages, | |
model=model, | |
llm_provider="bedrock_converse", | |
user_continue_message=litellm_params.pop("user_continue_message", None), | |
) | |
data: RequestObject = {"messages": bedrock_messages, **_data} | |
return data | |
def transform_response( | |
self, | |
model: str, | |
raw_response: httpx.Response, | |
model_response: ModelResponse, | |
logging_obj: Logging, | |
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: | |
return self._transform_response( | |
model=model, | |
response=raw_response, | |
model_response=model_response, | |
stream=optional_params.get("stream", False), | |
logging_obj=logging_obj, | |
optional_params=optional_params, | |
api_key=api_key, | |
data=request_data, | |
messages=messages, | |
encoding=encoding, | |
) | |
def _transform_reasoning_content( | |
self, reasoning_content_blocks: List[BedrockConverseReasoningContentBlock] | |
) -> str: | |
""" | |
Extract the reasoning text from the reasoning content blocks | |
Ensures deepseek reasoning content compatible output. | |
""" | |
reasoning_content_str = "" | |
for block in reasoning_content_blocks: | |
if "reasoningText" in block: | |
reasoning_content_str += block["reasoningText"]["text"] | |
return reasoning_content_str | |
def _transform_thinking_blocks( | |
self, thinking_blocks: List[BedrockConverseReasoningContentBlock] | |
) -> List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]]: | |
"""Return a consistent format for thinking blocks between Anthropic and Bedrock.""" | |
thinking_blocks_list: List[ | |
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] | |
] = [] | |
for block in thinking_blocks: | |
if "reasoningText" in block: | |
_thinking_block = ChatCompletionThinkingBlock(type="thinking") | |
_text = block["reasoningText"].get("text") | |
_signature = block["reasoningText"].get("signature") | |
if _text is not None: | |
_thinking_block["thinking"] = _text | |
if _signature is not None: | |
_thinking_block["signature"] = _signature | |
thinking_blocks_list.append(_thinking_block) | |
elif "redactedContent" in block: | |
_redacted_block = ChatCompletionRedactedThinkingBlock( | |
type="redacted_thinking", data=block["redactedContent"] | |
) | |
thinking_blocks_list.append(_redacted_block) | |
return thinking_blocks_list | |
def _transform_usage(self, usage: ConverseTokenUsageBlock) -> Usage: | |
input_tokens = usage["inputTokens"] | |
output_tokens = usage["outputTokens"] | |
total_tokens = usage["totalTokens"] | |
cache_creation_input_tokens: int = 0 | |
cache_read_input_tokens: int = 0 | |
if "cacheReadInputTokens" in usage: | |
cache_read_input_tokens = usage["cacheReadInputTokens"] | |
input_tokens += cache_read_input_tokens | |
if "cacheWriteInputTokens" in usage: | |
""" | |
Do not increment prompt_tokens with cacheWriteInputTokens | |
""" | |
cache_creation_input_tokens = usage["cacheWriteInputTokens"] | |
prompt_tokens_details = PromptTokensDetailsWrapper( | |
cached_tokens=cache_read_input_tokens | |
) | |
openai_usage = Usage( | |
prompt_tokens=input_tokens, | |
completion_tokens=output_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, | |
) | |
return openai_usage | |
def _transform_response( | |
self, | |
model: str, | |
response: httpx.Response, | |
model_response: ModelResponse, | |
stream: bool, | |
logging_obj: Optional[Logging], | |
optional_params: dict, | |
api_key: Optional[str], | |
data: Union[dict, str], | |
messages: List, | |
encoding, | |
) -> ModelResponse: | |
## LOGGING | |
if logging_obj is not None: | |
logging_obj.post_call( | |
input=messages, | |
api_key=api_key, | |
original_response=response.text, | |
additional_args={"complete_input_dict": data}, | |
) | |
json_mode: Optional[bool] = optional_params.pop("json_mode", None) | |
## RESPONSE OBJECT | |
try: | |
completion_response = ConverseResponseBlock(**response.json()) # type: ignore | |
except Exception as e: | |
raise BedrockError( | |
message="Received={}, Error converting to valid response block={}. File an issue if litellm error - https://github.com/BerriAI/litellm/issues".format( | |
response.text, str(e) | |
), | |
status_code=422, | |
) | |
""" | |
Bedrock Response Object has optional message block | |
completion_response["output"].get("message", None) | |
A message block looks like this (Example 1): | |
"output": { | |
"message": { | |
"role": "assistant", | |
"content": [ | |
{ | |
"text": "Is there anything else you'd like to talk about? Perhaps I can help with some economic questions or provide some information about economic concepts?" | |
} | |
] | |
} | |
}, | |
(Example 2): | |
"output": { | |
"message": { | |
"role": "assistant", | |
"content": [ | |
{ | |
"toolUse": { | |
"toolUseId": "tooluse_hbTgdi0CSLq_hM4P8csZJA", | |
"name": "top_song", | |
"input": { | |
"sign": "WZPZ" | |
} | |
} | |
} | |
] | |
} | |
} | |
""" | |
message: Optional[MessageBlock] = completion_response["output"]["message"] | |
chat_completion_message: ChatCompletionResponseMessage = {"role": "assistant"} | |
content_str = "" | |
tools: List[ChatCompletionToolCallChunk] = [] | |
reasoningContentBlocks: Optional[ | |
List[BedrockConverseReasoningContentBlock] | |
] = None | |
if message is not None: | |
for idx, content in enumerate(message["content"]): | |
""" | |
- Content is either a tool response or text | |
""" | |
if "text" in content: | |
content_str += content["text"] | |
if "toolUse" in content: | |
## check tool name was formatted by litellm | |
_response_tool_name = content["toolUse"]["name"] | |
response_tool_name = get_bedrock_tool_name( | |
response_tool_name=_response_tool_name | |
) | |
_function_chunk = ChatCompletionToolCallFunctionChunk( | |
name=response_tool_name, | |
arguments=json.dumps(content["toolUse"]["input"]), | |
) | |
_tool_response_chunk = ChatCompletionToolCallChunk( | |
id=content["toolUse"]["toolUseId"], | |
type="function", | |
function=_function_chunk, | |
index=idx, | |
) | |
tools.append(_tool_response_chunk) | |
if "reasoningContent" in content: | |
if reasoningContentBlocks is None: | |
reasoningContentBlocks = [] | |
reasoningContentBlocks.append(content["reasoningContent"]) | |
if reasoningContentBlocks is not None: | |
chat_completion_message["provider_specific_fields"] = { | |
"reasoningContentBlocks": 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 and tools is not None and len(tools) == 1: | |
# to support 'json_schema' logic on bedrock models | |
json_mode_content_str: Optional[str] = tools[0]["function"].get("arguments") | |
if json_mode_content_str is not None: | |
chat_completion_message["content"] = json_mode_content_str | |
else: | |
chat_completion_message["tool_calls"] = tools | |
## CALCULATING USAGE - bedrock returns usage in the headers | |
usage = self._transform_usage(completion_response["usage"]) | |
model_response.choices = [ | |
litellm.Choices( | |
finish_reason=map_finish_reason(completion_response["stopReason"]), | |
index=0, | |
message=litellm.Message(**chat_completion_message), | |
) | |
] | |
model_response.created = int(time.time()) | |
model_response.model = model | |
setattr(model_response, "usage", usage) | |
# Add "trace" from Bedrock guardrails - if user has opted in to returning it | |
if "trace" in completion_response: | |
setattr(model_response, "trace", completion_response["trace"]) | |
return model_response | |
def get_error_class( | |
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] | |
) -> BaseLLMException: | |
return BedrockError( | |
message=error_message, | |
status_code=status_code, | |
headers=headers, | |
) | |
def validate_environment( | |
self, | |
headers: dict, | |
model: str, | |
messages: List[AllMessageValues], | |
optional_params: dict, | |
litellm_params: dict, | |
api_key: Optional[str] = None, | |
api_base: Optional[str] = None, | |
) -> dict: | |
if api_key: | |
headers["Authorization"] = f"Bearer {api_key}" | |
return headers | |