Spaces:
Sleeping
Sleeping
""" | |
TODO: DELETE FILE. Bedrock LLM is no longer used. Goto `litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py` | |
""" | |
import copy | |
import json | |
import time | |
import types | |
import urllib.parse | |
import uuid | |
from functools import partial | |
from typing import ( | |
Any, | |
AsyncIterator, | |
Callable, | |
Iterator, | |
List, | |
Optional, | |
Tuple, | |
Union, | |
cast, | |
get_args, | |
) | |
import httpx # type: ignore | |
import litellm | |
from litellm import verbose_logger | |
from litellm.caching.caching import InMemoryCache | |
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.logging_utils import track_llm_api_timing | |
from litellm.litellm_core_utils.prompt_templates.factory import ( | |
cohere_message_pt, | |
construct_tool_use_system_prompt, | |
contains_tag, | |
custom_prompt, | |
extract_between_tags, | |
parse_xml_params, | |
prompt_factory, | |
) | |
from litellm.llms.anthropic.chat.handler import ( | |
ModelResponseIterator as AnthropicModelResponseIterator, | |
) | |
from litellm.llms.custom_httpx.http_handler import ( | |
AsyncHTTPHandler, | |
HTTPHandler, | |
_get_httpx_client, | |
get_async_httpx_client, | |
) | |
from litellm.types.llms.bedrock import * | |
from litellm.types.llms.openai import ( | |
ChatCompletionRedactedThinkingBlock, | |
ChatCompletionThinkingBlock, | |
ChatCompletionToolCallChunk, | |
ChatCompletionToolCallFunctionChunk, | |
ChatCompletionUsageBlock, | |
) | |
from litellm.types.utils import ChatCompletionMessageToolCall, Choices, Delta | |
from litellm.types.utils import GenericStreamingChunk as GChunk | |
from litellm.types.utils import ( | |
ModelResponse, | |
ModelResponseStream, | |
StreamingChoices, | |
Usage, | |
) | |
from litellm.utils import CustomStreamWrapper, get_secret | |
from ..base_aws_llm import BaseAWSLLM | |
from ..common_utils import BedrockError, ModelResponseIterator, get_bedrock_tool_name | |
_response_stream_shape_cache = None | |
bedrock_tool_name_mappings: InMemoryCache = InMemoryCache( | |
max_size_in_memory=50, default_ttl=600 | |
) | |
from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig | |
converse_config = AmazonConverseConfig() | |
class AmazonCohereChatConfig: | |
""" | |
Reference - https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-command-r-plus.html | |
""" | |
documents: Optional[List[Document]] = None | |
search_queries_only: Optional[bool] = None | |
preamble: Optional[str] = None | |
max_tokens: Optional[int] = None | |
temperature: Optional[float] = None | |
p: Optional[float] = None | |
k: Optional[float] = None | |
prompt_truncation: Optional[str] = None | |
frequency_penalty: Optional[float] = None | |
presence_penalty: Optional[float] = None | |
seed: Optional[int] = None | |
return_prompt: Optional[bool] = None | |
stop_sequences: Optional[List[str]] = None | |
raw_prompting: Optional[bool] = None | |
def __init__( | |
self, | |
documents: Optional[List[Document]] = None, | |
search_queries_only: Optional[bool] = None, | |
preamble: Optional[str] = None, | |
max_tokens: Optional[int] = None, | |
temperature: Optional[float] = None, | |
p: Optional[float] = None, | |
k: Optional[float] = None, | |
prompt_truncation: Optional[str] = None, | |
frequency_penalty: Optional[float] = None, | |
presence_penalty: Optional[float] = None, | |
seed: Optional[int] = None, | |
return_prompt: Optional[bool] = None, | |
stop_sequences: Optional[str] = None, | |
raw_prompting: Optional[bool] = 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 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) -> List[str]: | |
return [ | |
"max_tokens", | |
"max_completion_tokens", | |
"stream", | |
"stop", | |
"temperature", | |
"top_p", | |
"frequency_penalty", | |
"presence_penalty", | |
"seed", | |
"stop", | |
"tools", | |
"tool_choice", | |
] | |
def map_openai_params( | |
self, non_default_params: dict, optional_params: dict | |
) -> dict: | |
for param, value in non_default_params.items(): | |
if param == "max_tokens" or param == "max_completion_tokens": | |
optional_params["max_tokens"] = value | |
if param == "stream": | |
optional_params["stream"] = value | |
if param == "stop": | |
if isinstance(value, str): | |
value = [value] | |
optional_params["stop_sequences"] = value | |
if param == "temperature": | |
optional_params["temperature"] = value | |
if param == "top_p": | |
optional_params["p"] = value | |
if param == "frequency_penalty": | |
optional_params["frequency_penalty"] = value | |
if param == "presence_penalty": | |
optional_params["presence_penalty"] = value | |
if "seed": | |
optional_params["seed"] = value | |
return optional_params | |
async def make_call( | |
client: Optional[AsyncHTTPHandler], | |
api_base: str, | |
headers: dict, | |
data: str, | |
model: str, | |
messages: list, | |
logging_obj: Logging, | |
fake_stream: bool = False, | |
json_mode: Optional[bool] = False, | |
bedrock_invoke_provider: Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL] = None, | |
): | |
try: | |
if client is None: | |
client = get_async_httpx_client( | |
llm_provider=litellm.LlmProviders.BEDROCK | |
) # Create a new client if none provided | |
response = await client.post( | |
api_base, | |
headers=headers, | |
data=data, | |
stream=not fake_stream, | |
logging_obj=logging_obj, | |
) | |
if response.status_code != 200: | |
raise BedrockError(status_code=response.status_code, message=response.text) | |
if fake_stream: | |
model_response: ( | |
ModelResponse | |
) = litellm.AmazonConverseConfig()._transform_response( | |
model=model, | |
response=response, | |
model_response=litellm.ModelResponse(), | |
stream=True, | |
logging_obj=logging_obj, | |
optional_params={}, | |
api_key="", | |
data=data, | |
messages=messages, | |
encoding=litellm.encoding, | |
) # type: ignore | |
completion_stream: Any = MockResponseIterator( | |
model_response=model_response, json_mode=json_mode | |
) | |
elif bedrock_invoke_provider == "anthropic": | |
decoder: AWSEventStreamDecoder = AmazonAnthropicClaudeStreamDecoder( | |
model=model, | |
sync_stream=False, | |
json_mode=json_mode, | |
) | |
completion_stream = decoder.aiter_bytes( | |
response.aiter_bytes(chunk_size=1024) | |
) | |
elif bedrock_invoke_provider == "deepseek_r1": | |
decoder = AmazonDeepSeekR1StreamDecoder( | |
model=model, | |
sync_stream=False, | |
) | |
completion_stream = decoder.aiter_bytes( | |
response.aiter_bytes(chunk_size=1024) | |
) | |
else: | |
decoder = AWSEventStreamDecoder(model=model) | |
completion_stream = decoder.aiter_bytes( | |
response.aiter_bytes(chunk_size=1024) | |
) | |
# LOGGING | |
logging_obj.post_call( | |
input=messages, | |
api_key="", | |
original_response="first stream response received", | |
additional_args={"complete_input_dict": data}, | |
) | |
return completion_stream | |
except httpx.HTTPStatusError as err: | |
error_code = err.response.status_code | |
raise BedrockError(status_code=error_code, message=err.response.text) | |
except httpx.TimeoutException: | |
raise BedrockError(status_code=408, message="Timeout error occurred.") | |
except Exception as e: | |
raise BedrockError(status_code=500, message=str(e)) | |
def make_sync_call( | |
client: Optional[HTTPHandler], | |
api_base: str, | |
headers: dict, | |
data: str, | |
model: str, | |
messages: list, | |
logging_obj: Logging, | |
fake_stream: bool = False, | |
json_mode: Optional[bool] = False, | |
bedrock_invoke_provider: Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL] = None, | |
): | |
try: | |
if client is None: | |
client = _get_httpx_client(params={}) | |
response = client.post( | |
api_base, | |
headers=headers, | |
data=data, | |
stream=not fake_stream, | |
logging_obj=logging_obj, | |
) | |
if response.status_code != 200: | |
raise BedrockError(status_code=response.status_code, message=response.text) | |
if fake_stream: | |
model_response: ( | |
ModelResponse | |
) = litellm.AmazonConverseConfig()._transform_response( | |
model=model, | |
response=response, | |
model_response=litellm.ModelResponse(), | |
stream=True, | |
logging_obj=logging_obj, | |
optional_params={}, | |
api_key="", | |
data=data, | |
messages=messages, | |
encoding=litellm.encoding, | |
) # type: ignore | |
completion_stream: Any = MockResponseIterator( | |
model_response=model_response, json_mode=json_mode | |
) | |
elif bedrock_invoke_provider == "anthropic": | |
decoder: AWSEventStreamDecoder = AmazonAnthropicClaudeStreamDecoder( | |
model=model, | |
sync_stream=True, | |
json_mode=json_mode, | |
) | |
completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) | |
elif bedrock_invoke_provider == "deepseek_r1": | |
decoder = AmazonDeepSeekR1StreamDecoder( | |
model=model, | |
sync_stream=True, | |
) | |
completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) | |
else: | |
decoder = AWSEventStreamDecoder(model=model) | |
completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) | |
# LOGGING | |
logging_obj.post_call( | |
input=messages, | |
api_key="", | |
original_response="first stream response received", | |
additional_args={"complete_input_dict": data}, | |
) | |
return completion_stream | |
except httpx.HTTPStatusError as err: | |
error_code = err.response.status_code | |
raise BedrockError(status_code=error_code, message=err.response.text) | |
except httpx.TimeoutException: | |
raise BedrockError(status_code=408, message="Timeout error occurred.") | |
except Exception as e: | |
raise BedrockError(status_code=500, message=str(e)) | |
class BedrockLLM(BaseAWSLLM): | |
""" | |
Example call | |
``` | |
curl --location --request POST 'https://bedrock-runtime.{aws_region_name}.amazonaws.com/model/{bedrock_model_name}/invoke' \ | |
--header 'Content-Type: application/json' \ | |
--header 'Accept: application/json' \ | |
--user "$AWS_ACCESS_KEY_ID":"$AWS_SECRET_ACCESS_KEY" \ | |
--aws-sigv4 "aws:amz:us-east-1:bedrock" \ | |
--data-raw '{ | |
"prompt": "Hi", | |
"temperature": 0, | |
"p": 0.9, | |
"max_tokens": 4096 | |
}' | |
``` | |
""" | |
def __init__(self) -> None: | |
super().__init__() | |
def convert_messages_to_prompt( | |
self, model, messages, provider, custom_prompt_dict | |
) -> Tuple[str, Optional[list]]: | |
# handle anthropic prompts and amazon titan prompts | |
prompt = "" | |
chat_history: Optional[list] = None | |
## CUSTOM PROMPT | |
if model in custom_prompt_dict: | |
# check if the model has a registered custom prompt | |
model_prompt_details = custom_prompt_dict[model] | |
prompt = custom_prompt( | |
role_dict=model_prompt_details["roles"], | |
initial_prompt_value=model_prompt_details.get( | |
"initial_prompt_value", "" | |
), | |
final_prompt_value=model_prompt_details.get("final_prompt_value", ""), | |
messages=messages, | |
) | |
return prompt, None | |
## ELSE | |
if provider == "anthropic" or provider == "amazon": | |
prompt = prompt_factory( | |
model=model, messages=messages, custom_llm_provider="bedrock" | |
) | |
elif provider == "mistral": | |
prompt = prompt_factory( | |
model=model, messages=messages, custom_llm_provider="bedrock" | |
) | |
elif provider == "meta" or provider == "llama": | |
prompt = prompt_factory( | |
model=model, messages=messages, custom_llm_provider="bedrock" | |
) | |
elif provider == "cohere": | |
prompt, chat_history = cohere_message_pt(messages=messages) | |
else: | |
prompt = "" | |
for message in messages: | |
if "role" in message: | |
if message["role"] == "user": | |
prompt += f"{message['content']}" | |
else: | |
prompt += f"{message['content']}" | |
else: | |
prompt += f"{message['content']}" | |
return prompt, chat_history # type: ignore | |
def process_response( # noqa: PLR0915 | |
self, | |
model: str, | |
response: httpx.Response, | |
model_response: ModelResponse, | |
stream: Optional[bool], | |
logging_obj: Logging, | |
optional_params: dict, | |
api_key: str, | |
data: Union[dict, str], | |
messages: List, | |
print_verbose, | |
encoding, | |
) -> Union[ModelResponse, CustomStreamWrapper]: | |
provider = self.get_bedrock_invoke_provider(model) | |
## LOGGING | |
logging_obj.post_call( | |
input=messages, | |
api_key=api_key, | |
original_response=response.text, | |
additional_args={"complete_input_dict": data}, | |
) | |
print_verbose(f"raw model_response: {response.text}") | |
## RESPONSE OBJECT | |
try: | |
completion_response = response.json() | |
except Exception: | |
raise BedrockError(message=response.text, status_code=422) | |
outputText: Optional[str] = None | |
try: | |
if provider == "cohere": | |
if "text" in completion_response: | |
outputText = completion_response["text"] # type: ignore | |
elif "generations" in completion_response: | |
outputText = completion_response["generations"][0]["text"] | |
model_response.choices[0].finish_reason = map_finish_reason( | |
completion_response["generations"][0]["finish_reason"] | |
) | |
elif provider == "anthropic": | |
if model.startswith("anthropic.claude-3"): | |
json_schemas: dict = {} | |
_is_function_call = False | |
## Handle Tool Calling | |
if "tools" in optional_params: | |
_is_function_call = True | |
for tool in optional_params["tools"]: | |
json_schemas[tool["function"]["name"]] = tool[ | |
"function" | |
].get("parameters", None) | |
outputText = completion_response.get("content")[0].get("text", None) | |
if outputText is not None and contains_tag( | |
"invoke", outputText | |
): # OUTPUT PARSE FUNCTION CALL | |
function_name = extract_between_tags("tool_name", outputText)[0] | |
function_arguments_str = extract_between_tags( | |
"invoke", outputText | |
)[0].strip() | |
function_arguments_str = ( | |
f"<invoke>{function_arguments_str}</invoke>" | |
) | |
function_arguments = parse_xml_params( | |
function_arguments_str, | |
json_schema=json_schemas.get( | |
function_name, None | |
), # check if we have a json schema for this function name) | |
) | |
_message = litellm.Message( | |
tool_calls=[ | |
{ | |
"id": f"call_{uuid.uuid4()}", | |
"type": "function", | |
"function": { | |
"name": function_name, | |
"arguments": json.dumps(function_arguments), | |
}, | |
} | |
], | |
content=None, | |
) | |
model_response.choices[0].message = _message # type: ignore | |
model_response._hidden_params[ | |
"original_response" | |
] = outputText # allow user to access raw anthropic tool calling response | |
if ( | |
_is_function_call is True | |
and stream is not None | |
and stream is True | |
): | |
print_verbose( | |
"INSIDE BEDROCK STREAMING TOOL CALLING CONDITION BLOCK" | |
) | |
# return an iterator | |
streaming_model_response = ModelResponse(stream=True) | |
streaming_model_response.choices[0].finish_reason = getattr( | |
model_response.choices[0], "finish_reason", "stop" | |
) | |
# streaming_model_response.choices = [litellm.utils.StreamingChoices()] | |
streaming_choice = litellm.utils.StreamingChoices() | |
streaming_choice.index = model_response.choices[0].index | |
_tool_calls = [] | |
print_verbose( | |
f"type of model_response.choices[0]: {type(model_response.choices[0])}" | |
) | |
print_verbose( | |
f"type of streaming_choice: {type(streaming_choice)}" | |
) | |
if isinstance(model_response.choices[0], litellm.Choices): | |
if getattr( | |
model_response.choices[0].message, "tool_calls", None | |
) is not None and isinstance( | |
model_response.choices[0].message.tool_calls, list | |
): | |
for tool_call in model_response.choices[ | |
0 | |
].message.tool_calls: | |
_tool_call = {**tool_call.dict(), "index": 0} | |
_tool_calls.append(_tool_call) | |
delta_obj = Delta( | |
content=getattr( | |
model_response.choices[0].message, "content", None | |
), | |
role=model_response.choices[0].message.role, | |
tool_calls=_tool_calls, | |
) | |
streaming_choice.delta = delta_obj | |
streaming_model_response.choices = [streaming_choice] | |
completion_stream = ModelResponseIterator( | |
model_response=streaming_model_response | |
) | |
print_verbose( | |
"Returns anthropic CustomStreamWrapper with 'cached_response' streaming object" | |
) | |
return litellm.CustomStreamWrapper( | |
completion_stream=completion_stream, | |
model=model, | |
custom_llm_provider="cached_response", | |
logging_obj=logging_obj, | |
) | |
model_response.choices[0].finish_reason = map_finish_reason( | |
completion_response.get("stop_reason", "") | |
) | |
_usage = litellm.Usage( | |
prompt_tokens=completion_response["usage"]["input_tokens"], | |
completion_tokens=completion_response["usage"]["output_tokens"], | |
total_tokens=completion_response["usage"]["input_tokens"] | |
+ completion_response["usage"]["output_tokens"], | |
) | |
setattr(model_response, "usage", _usage) | |
else: | |
outputText = completion_response["completion"] | |
model_response.choices[0].finish_reason = completion_response[ | |
"stop_reason" | |
] | |
elif provider == "ai21": | |
outputText = ( | |
completion_response.get("completions")[0].get("data").get("text") | |
) | |
elif provider == "meta" or provider == "llama": | |
outputText = completion_response["generation"] | |
elif provider == "mistral": | |
outputText = completion_response["outputs"][0]["text"] | |
model_response.choices[0].finish_reason = completion_response[ | |
"outputs" | |
][0]["stop_reason"] | |
else: # amazon titan | |
outputText = completion_response.get("results")[0].get("outputText") | |
except Exception as e: | |
raise BedrockError( | |
message="Error processing={}, Received error={}".format( | |
response.text, str(e) | |
), | |
status_code=422, | |
) | |
try: | |
if ( | |
outputText is not None | |
and len(outputText) > 0 | |
and hasattr(model_response.choices[0], "message") | |
and getattr(model_response.choices[0].message, "tool_calls", None) # type: ignore | |
is None | |
): | |
model_response.choices[0].message.content = outputText # type: ignore | |
elif ( | |
hasattr(model_response.choices[0], "message") | |
and getattr(model_response.choices[0].message, "tool_calls", None) # type: ignore | |
is not None | |
): | |
pass | |
else: | |
raise Exception() | |
except Exception as e: | |
raise BedrockError( | |
message="Error parsing received text={}.\nError-{}".format( | |
outputText, str(e) | |
), | |
status_code=response.status_code, | |
) | |
if stream and provider == "ai21": | |
streaming_model_response = ModelResponse(stream=True) | |
streaming_model_response.choices[0].finish_reason = model_response.choices[ # type: ignore | |
0 | |
].finish_reason | |
# streaming_model_response.choices = [litellm.utils.StreamingChoices()] | |
streaming_choice = litellm.utils.StreamingChoices() | |
streaming_choice.index = model_response.choices[0].index | |
delta_obj = litellm.utils.Delta( | |
content=getattr(model_response.choices[0].message, "content", None), # type: ignore | |
role=model_response.choices[0].message.role, # type: ignore | |
) | |
streaming_choice.delta = delta_obj | |
streaming_model_response.choices = [streaming_choice] | |
mri = ModelResponseIterator(model_response=streaming_model_response) | |
return CustomStreamWrapper( | |
completion_stream=mri, | |
model=model, | |
custom_llm_provider="cached_response", | |
logging_obj=logging_obj, | |
) | |
## CALCULATING USAGE - bedrock returns usage in the headers | |
bedrock_input_tokens = response.headers.get( | |
"x-amzn-bedrock-input-token-count", None | |
) | |
bedrock_output_tokens = response.headers.get( | |
"x-amzn-bedrock-output-token-count", None | |
) | |
prompt_tokens = int( | |
bedrock_input_tokens or litellm.token_counter(messages=messages) | |
) | |
completion_tokens = int( | |
bedrock_output_tokens | |
or litellm.token_counter( | |
text=model_response.choices[0].message.content, # type: ignore | |
count_response_tokens=True, | |
) | |
) | |
model_response.created = int(time.time()) | |
model_response.model = model | |
usage = Usage( | |
prompt_tokens=prompt_tokens, | |
completion_tokens=completion_tokens, | |
total_tokens=prompt_tokens + completion_tokens, | |
) | |
setattr(model_response, "usage", usage) | |
return model_response | |
def encode_model_id(self, model_id: str) -> str: | |
""" | |
Double encode the model ID to ensure it matches the expected double-encoded format. | |
Args: | |
model_id (str): The model ID to encode. | |
Returns: | |
str: The double-encoded model ID. | |
""" | |
return urllib.parse.quote(model_id, safe="") | |
def completion( # noqa: PLR0915 | |
self, | |
model: str, | |
messages: list, | |
api_base: Optional[str], | |
custom_prompt_dict: dict, | |
model_response: ModelResponse, | |
print_verbose: Callable, | |
encoding, | |
logging_obj: Logging, | |
optional_params: dict, | |
acompletion: bool, | |
timeout: Optional[Union[float, httpx.Timeout]], | |
litellm_params=None, | |
logger_fn=None, | |
extra_headers: Optional[dict] = None, | |
client: Optional[Union[AsyncHTTPHandler, HTTPHandler]] = None, | |
) -> Union[ModelResponse, CustomStreamWrapper]: | |
try: | |
from botocore.auth import SigV4Auth | |
from botocore.awsrequest import AWSRequest | |
from botocore.credentials import Credentials | |
except ImportError: | |
raise ImportError("Missing boto3 to call bedrock. Run 'pip install boto3'.") | |
## SETUP ## | |
stream = optional_params.pop("stream", None) | |
provider = self.get_bedrock_invoke_provider(model) | |
modelId = self.get_bedrock_model_id( | |
model=model, | |
provider=provider, | |
optional_params=optional_params, | |
) | |
## CREDENTIALS ## | |
# pop aws_secret_access_key, aws_access_key_id, aws_session_token, aws_region_name from kwargs, since completion calls fail with them | |
aws_secret_access_key = optional_params.pop("aws_secret_access_key", None) | |
aws_access_key_id = optional_params.pop("aws_access_key_id", None) | |
aws_session_token = optional_params.pop("aws_session_token", None) | |
aws_region_name = optional_params.pop("aws_region_name", None) | |
aws_role_name = optional_params.pop("aws_role_name", None) | |
aws_session_name = optional_params.pop("aws_session_name", None) | |
aws_profile_name = optional_params.pop("aws_profile_name", None) | |
aws_bedrock_runtime_endpoint = optional_params.pop( | |
"aws_bedrock_runtime_endpoint", None | |
) # https://bedrock-runtime.{region_name}.amazonaws.com | |
aws_web_identity_token = optional_params.pop("aws_web_identity_token", None) | |
aws_sts_endpoint = optional_params.pop("aws_sts_endpoint", None) | |
### SET REGION NAME ### | |
if aws_region_name is None: | |
# check env # | |
litellm_aws_region_name = get_secret("AWS_REGION_NAME", None) | |
if litellm_aws_region_name is not None and isinstance( | |
litellm_aws_region_name, str | |
): | |
aws_region_name = litellm_aws_region_name | |
standard_aws_region_name = get_secret("AWS_REGION", None) | |
if standard_aws_region_name is not None and isinstance( | |
standard_aws_region_name, str | |
): | |
aws_region_name = standard_aws_region_name | |
if aws_region_name is None: | |
aws_region_name = "us-west-2" | |
credentials: Credentials = self.get_credentials( | |
aws_access_key_id=aws_access_key_id, | |
aws_secret_access_key=aws_secret_access_key, | |
aws_session_token=aws_session_token, | |
aws_region_name=aws_region_name, | |
aws_session_name=aws_session_name, | |
aws_profile_name=aws_profile_name, | |
aws_role_name=aws_role_name, | |
aws_web_identity_token=aws_web_identity_token, | |
aws_sts_endpoint=aws_sts_endpoint, | |
) | |
### SET RUNTIME ENDPOINT ### | |
endpoint_url, proxy_endpoint_url = self.get_runtime_endpoint( | |
api_base=api_base, | |
aws_bedrock_runtime_endpoint=aws_bedrock_runtime_endpoint, | |
aws_region_name=aws_region_name, | |
) | |
if (stream is not None and stream is True) and provider != "ai21": | |
endpoint_url = f"{endpoint_url}/model/{modelId}/invoke-with-response-stream" | |
proxy_endpoint_url = ( | |
f"{proxy_endpoint_url}/model/{modelId}/invoke-with-response-stream" | |
) | |
else: | |
endpoint_url = f"{endpoint_url}/model/{modelId}/invoke" | |
proxy_endpoint_url = f"{proxy_endpoint_url}/model/{modelId}/invoke" | |
sigv4 = SigV4Auth(credentials, "bedrock", aws_region_name) | |
prompt, chat_history = self.convert_messages_to_prompt( | |
model, messages, provider, custom_prompt_dict | |
) | |
inference_params = copy.deepcopy(optional_params) | |
json_schemas: dict = {} | |
if provider == "cohere": | |
if model.startswith("cohere.command-r"): | |
## LOAD CONFIG | |
config = litellm.AmazonCohereChatConfig().get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
_data = {"message": prompt, **inference_params} | |
if chat_history is not None: | |
_data["chat_history"] = chat_history | |
data = json.dumps(_data) | |
else: | |
## LOAD CONFIG | |
config = litellm.AmazonCohereConfig.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
if stream is True: | |
inference_params[ | |
"stream" | |
] = True # cohere requires stream = True in inference params | |
data = json.dumps({"prompt": prompt, **inference_params}) | |
elif provider == "anthropic": | |
if model.startswith("anthropic.claude-3"): | |
# Separate system prompt from rest of message | |
system_prompt_idx: list[int] = [] | |
system_messages: list[str] = [] | |
for idx, message in enumerate(messages): | |
if message["role"] == "system": | |
system_messages.append(message["content"]) | |
system_prompt_idx.append(idx) | |
if len(system_prompt_idx) > 0: | |
inference_params["system"] = "\n".join(system_messages) | |
messages = [ | |
i for j, i in enumerate(messages) if j not in system_prompt_idx | |
] | |
# Format rest of message according to anthropic guidelines | |
messages = prompt_factory( | |
model=model, messages=messages, custom_llm_provider="anthropic_xml" | |
) # type: ignore | |
## LOAD CONFIG | |
config = litellm.AmazonAnthropicClaude3Config.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
## Handle Tool Calling | |
if "tools" in inference_params: | |
_is_function_call = True | |
for tool in inference_params["tools"]: | |
json_schemas[tool["function"]["name"]] = tool["function"].get( | |
"parameters", None | |
) | |
tool_calling_system_prompt = construct_tool_use_system_prompt( | |
tools=inference_params["tools"] | |
) | |
inference_params["system"] = ( | |
inference_params.get("system", "\n") | |
+ tool_calling_system_prompt | |
) # add the anthropic tool calling prompt to the system prompt | |
inference_params.pop("tools") | |
data = json.dumps({"messages": messages, **inference_params}) | |
else: | |
## LOAD CONFIG | |
config = litellm.AmazonAnthropicConfig.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
data = json.dumps({"prompt": prompt, **inference_params}) | |
elif provider == "ai21": | |
## LOAD CONFIG | |
config = litellm.AmazonAI21Config.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
data = json.dumps({"prompt": prompt, **inference_params}) | |
elif provider == "mistral": | |
## LOAD CONFIG | |
config = litellm.AmazonMistralConfig.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > amazon_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
data = json.dumps({"prompt": prompt, **inference_params}) | |
elif provider == "amazon": # amazon titan | |
## LOAD CONFIG | |
config = litellm.AmazonTitanConfig.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > amazon_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
data = json.dumps( | |
{ | |
"inputText": prompt, | |
"textGenerationConfig": inference_params, | |
} | |
) | |
elif provider == "meta" or provider == "llama": | |
## LOAD CONFIG | |
config = litellm.AmazonLlamaConfig.get_config() | |
for k, v in config.items(): | |
if ( | |
k not in inference_params | |
): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in | |
inference_params[k] = v | |
data = json.dumps({"prompt": prompt, **inference_params}) | |
else: | |
## LOGGING | |
logging_obj.pre_call( | |
input=messages, | |
api_key="", | |
additional_args={ | |
"complete_input_dict": inference_params, | |
}, | |
) | |
raise BedrockError( | |
status_code=404, | |
message="Bedrock Invoke HTTPX: Unknown provider={}, model={}. Try calling via converse route - `bedrock/converse/<model>`.".format( | |
provider, model | |
), | |
) | |
## COMPLETION CALL | |
headers = {"Content-Type": "application/json"} | |
if extra_headers is not None: | |
headers = {"Content-Type": "application/json", **extra_headers} | |
request = AWSRequest( | |
method="POST", url=endpoint_url, data=data, headers=headers | |
) | |
sigv4.add_auth(request) | |
if ( | |
extra_headers is not None and "Authorization" in extra_headers | |
): # prevent sigv4 from overwriting the auth header | |
request.headers["Authorization"] = extra_headers["Authorization"] | |
prepped = request.prepare() | |
## LOGGING | |
logging_obj.pre_call( | |
input=messages, | |
api_key="", | |
additional_args={ | |
"complete_input_dict": data, | |
"api_base": proxy_endpoint_url, | |
"headers": prepped.headers, | |
}, | |
) | |
### ROUTING (ASYNC, STREAMING, SYNC) | |
if acompletion: | |
if isinstance(client, HTTPHandler): | |
client = None | |
if stream is True and provider != "ai21": | |
return self.async_streaming( | |
model=model, | |
messages=messages, | |
data=data, | |
api_base=proxy_endpoint_url, | |
model_response=model_response, | |
print_verbose=print_verbose, | |
encoding=encoding, | |
logging_obj=logging_obj, | |
optional_params=optional_params, | |
stream=True, | |
litellm_params=litellm_params, | |
logger_fn=logger_fn, | |
headers=prepped.headers, | |
timeout=timeout, | |
client=client, | |
) # type: ignore | |
### ASYNC COMPLETION | |
return self.async_completion( | |
model=model, | |
messages=messages, | |
data=data, | |
api_base=proxy_endpoint_url, | |
model_response=model_response, | |
print_verbose=print_verbose, | |
encoding=encoding, | |
logging_obj=logging_obj, | |
optional_params=optional_params, | |
stream=stream, # type: ignore | |
litellm_params=litellm_params, | |
logger_fn=logger_fn, | |
headers=prepped.headers, | |
timeout=timeout, | |
client=client, | |
) # type: ignore | |
if client is None or isinstance(client, AsyncHTTPHandler): | |
_params = {} | |
if timeout is not None: | |
if isinstance(timeout, float) or isinstance(timeout, int): | |
timeout = httpx.Timeout(timeout) | |
_params["timeout"] = timeout | |
self.client = _get_httpx_client(_params) # type: ignore | |
else: | |
self.client = client | |
if (stream is not None and stream is True) and provider != "ai21": | |
response = self.client.post( | |
url=proxy_endpoint_url, | |
headers=prepped.headers, # type: ignore | |
data=data, | |
stream=stream, | |
logging_obj=logging_obj, | |
) | |
if response.status_code != 200: | |
raise BedrockError( | |
status_code=response.status_code, message=str(response.read()) | |
) | |
decoder = AWSEventStreamDecoder(model=model) | |
completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) | |
streaming_response = CustomStreamWrapper( | |
completion_stream=completion_stream, | |
model=model, | |
custom_llm_provider="bedrock", | |
logging_obj=logging_obj, | |
) | |
## LOGGING | |
logging_obj.post_call( | |
input=messages, | |
api_key="", | |
original_response=streaming_response, | |
additional_args={"complete_input_dict": data}, | |
) | |
return streaming_response | |
try: | |
response = self.client.post( | |
url=proxy_endpoint_url, | |
headers=dict(prepped.headers), | |
data=data, | |
logging_obj=logging_obj, | |
) | |
response.raise_for_status() | |
except httpx.HTTPStatusError as err: | |
error_code = err.response.status_code | |
raise BedrockError(status_code=error_code, message=err.response.text) | |
except httpx.TimeoutException: | |
raise BedrockError(status_code=408, message="Timeout error occurred.") | |
return self.process_response( | |
model=model, | |
response=response, | |
model_response=model_response, | |
stream=stream, | |
logging_obj=logging_obj, | |
optional_params=optional_params, | |
api_key="", | |
data=data, | |
messages=messages, | |
print_verbose=print_verbose, | |
encoding=encoding, | |
) | |
async def async_completion( | |
self, | |
model: str, | |
messages: list, | |
api_base: str, | |
model_response: ModelResponse, | |
print_verbose: Callable, | |
data: str, | |
timeout: Optional[Union[float, httpx.Timeout]], | |
encoding, | |
logging_obj: Logging, | |
stream, | |
optional_params: dict, | |
litellm_params=None, | |
logger_fn=None, | |
headers={}, | |
client: Optional[AsyncHTTPHandler] = None, | |
) -> Union[ModelResponse, CustomStreamWrapper]: | |
if client is None: | |
_params = {} | |
if timeout is not None: | |
if isinstance(timeout, float) or isinstance(timeout, int): | |
timeout = httpx.Timeout(timeout) | |
_params["timeout"] = timeout | |
client = get_async_httpx_client(params=_params, llm_provider=litellm.LlmProviders.BEDROCK) # type: ignore | |
else: | |
client = client # type: ignore | |
try: | |
response = await client.post( | |
api_base, | |
headers=headers, | |
data=data, | |
timeout=timeout, | |
logging_obj=logging_obj, | |
) | |
response.raise_for_status() | |
except httpx.HTTPStatusError as err: | |
error_code = err.response.status_code | |
raise BedrockError(status_code=error_code, message=err.response.text) | |
except httpx.TimeoutException: | |
raise BedrockError(status_code=408, message="Timeout error occurred.") | |
return self.process_response( | |
model=model, | |
response=response, | |
model_response=model_response, | |
stream=stream if isinstance(stream, bool) else False, | |
logging_obj=logging_obj, | |
api_key="", | |
data=data, | |
messages=messages, | |
print_verbose=print_verbose, | |
optional_params=optional_params, | |
encoding=encoding, | |
) | |
# for streaming, we need to instrument the function calling the wrapper | |
async def async_streaming( | |
self, | |
model: str, | |
messages: list, | |
api_base: str, | |
model_response: ModelResponse, | |
print_verbose: Callable, | |
data: str, | |
timeout: Optional[Union[float, httpx.Timeout]], | |
encoding, | |
logging_obj: Logging, | |
stream, | |
optional_params: dict, | |
litellm_params=None, | |
logger_fn=None, | |
headers={}, | |
client: Optional[AsyncHTTPHandler] = None, | |
) -> CustomStreamWrapper: | |
# The call is not made here; instead, we prepare the necessary objects for the stream. | |
streaming_response = CustomStreamWrapper( | |
completion_stream=None, | |
make_call=partial( | |
make_call, | |
client=client, | |
api_base=api_base, | |
headers=headers, | |
data=data, # type: ignore | |
model=model, | |
messages=messages, | |
logging_obj=logging_obj, | |
fake_stream=True if "ai21" in api_base else False, | |
), | |
model=model, | |
custom_llm_provider="bedrock", | |
logging_obj=logging_obj, | |
) | |
return streaming_response | |
def _get_provider_from_model_path( | |
model_path: str, | |
) -> Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL]: | |
""" | |
Helper function to get the provider from a model path with format: provider/model-name | |
Args: | |
model_path (str): The model path (e.g., 'llama/arn:aws:bedrock:us-east-1:086734376398:imported-model/r4c4kewx2s0n' or 'anthropic/model-name') | |
Returns: | |
Optional[str]: The provider name, or None if no valid provider found | |
""" | |
parts = model_path.split("/") | |
if len(parts) >= 1: | |
provider = parts[0] | |
if provider in get_args(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL): | |
return cast(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL, provider) | |
return None | |
def get_bedrock_model_id( | |
self, | |
optional_params: dict, | |
provider: Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL], | |
model: str, | |
) -> str: | |
modelId = optional_params.pop("model_id", None) | |
if modelId is not None: | |
modelId = self.encode_model_id(model_id=modelId) | |
else: | |
modelId = model | |
if provider == "llama" and "llama/" in modelId: | |
modelId = self._get_model_id_for_llama_like_model(modelId) | |
return modelId | |
def _get_model_id_for_llama_like_model( | |
self, | |
model: str, | |
) -> str: | |
""" | |
Remove `llama` from modelID since `llama` is simply a spec to follow for custom bedrock models | |
""" | |
model_id = model.replace("llama/", "") | |
return self.encode_model_id(model_id=model_id) | |
def get_response_stream_shape(): | |
global _response_stream_shape_cache | |
if _response_stream_shape_cache is None: | |
from botocore.loaders import Loader | |
from botocore.model import ServiceModel | |
loader = Loader() | |
bedrock_service_dict = loader.load_service_model("bedrock-runtime", "service-2") | |
bedrock_service_model = ServiceModel(bedrock_service_dict) | |
_response_stream_shape_cache = bedrock_service_model.shape_for("ResponseStream") | |
return _response_stream_shape_cache | |
class AWSEventStreamDecoder: | |
def __init__(self, model: str) -> None: | |
from botocore.parsers import EventStreamJSONParser | |
self.model = model | |
self.parser = EventStreamJSONParser() | |
self.content_blocks: List[ContentBlockDeltaEvent] = [] | |
def check_empty_tool_call_args(self) -> bool: | |
""" | |
Check if the tool call block so far has been an empty string | |
""" | |
args = "" | |
# if text content block -> skip | |
if len(self.content_blocks) == 0: | |
return False | |
if ( | |
"toolUse" not in self.content_blocks[0] | |
): # be explicit - only do this if tool use block, as this is to prevent json decoding errors | |
return False | |
for block in self.content_blocks: | |
if "toolUse" in block: | |
args += block["toolUse"]["input"] | |
if len(args) == 0: | |
return True | |
return False | |
def extract_reasoning_content_str( | |
self, reasoning_content_block: BedrockConverseReasoningContentBlockDelta | |
) -> Optional[str]: | |
if "text" in reasoning_content_block: | |
return reasoning_content_block["text"] | |
return None | |
def translate_thinking_blocks( | |
self, thinking_block: BedrockConverseReasoningContentBlockDelta | |
) -> Optional[ | |
List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]] | |
]: | |
""" | |
Translate the thinking blocks to a string | |
""" | |
thinking_blocks_list: List[ | |
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] | |
] = [] | |
_thinking_block: Optional[ | |
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] | |
] = None | |
if "text" in thinking_block: | |
_thinking_block = ChatCompletionThinkingBlock(type="thinking") | |
_thinking_block["thinking"] = thinking_block["text"] | |
elif "signature" in thinking_block: | |
_thinking_block = ChatCompletionThinkingBlock(type="thinking") | |
_thinking_block["signature"] = thinking_block["signature"] | |
_thinking_block["thinking"] = "" # consistent with anthropic response | |
elif "redactedContent" in thinking_block: | |
_thinking_block = ChatCompletionRedactedThinkingBlock( | |
type="redacted_thinking", data=thinking_block["redactedContent"] | |
) | |
if _thinking_block is not None: | |
thinking_blocks_list.append(_thinking_block) | |
return thinking_blocks_list | |
def converse_chunk_parser(self, chunk_data: dict) -> ModelResponseStream: | |
try: | |
verbose_logger.debug("\n\nRaw Chunk: {}\n\n".format(chunk_data)) | |
text = "" | |
tool_use: Optional[ChatCompletionToolCallChunk] = None | |
finish_reason = "" | |
usage: Optional[Usage] = None | |
provider_specific_fields: dict = {} | |
reasoning_content: Optional[str] = None | |
thinking_blocks: Optional[ | |
List[ | |
Union[ | |
ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock | |
] | |
] | |
] = None | |
index = int(chunk_data.get("contentBlockIndex", 0)) | |
if "start" in chunk_data: | |
start_obj = ContentBlockStartEvent(**chunk_data["start"]) | |
self.content_blocks = [] # reset | |
if start_obj is not None: | |
if "toolUse" in start_obj and start_obj["toolUse"] is not None: | |
## check tool name was formatted by litellm | |
_response_tool_name = start_obj["toolUse"]["name"] | |
response_tool_name = get_bedrock_tool_name( | |
response_tool_name=_response_tool_name | |
) | |
tool_use = { | |
"id": start_obj["toolUse"]["toolUseId"], | |
"type": "function", | |
"function": { | |
"name": response_tool_name, | |
"arguments": "", | |
}, | |
"index": index, | |
} | |
elif ( | |
"reasoningContent" in start_obj | |
and start_obj["reasoningContent"] is not None | |
): # redacted thinking can be in start object | |
thinking_blocks = self.translate_thinking_blocks( | |
start_obj["reasoningContent"] | |
) | |
provider_specific_fields = { | |
"reasoningContent": start_obj["reasoningContent"], | |
} | |
elif "delta" in chunk_data: | |
delta_obj = ContentBlockDeltaEvent(**chunk_data["delta"]) | |
self.content_blocks.append(delta_obj) | |
if "text" in delta_obj: | |
text = delta_obj["text"] | |
elif "toolUse" in delta_obj: | |
tool_use = { | |
"id": None, | |
"type": "function", | |
"function": { | |
"name": None, | |
"arguments": delta_obj["toolUse"]["input"], | |
}, | |
"index": index, | |
} | |
elif "reasoningContent" in delta_obj: | |
provider_specific_fields = { | |
"reasoningContent": delta_obj["reasoningContent"], | |
} | |
reasoning_content = self.extract_reasoning_content_str( | |
delta_obj["reasoningContent"] | |
) | |
thinking_blocks = self.translate_thinking_blocks( | |
delta_obj["reasoningContent"] | |
) | |
if ( | |
thinking_blocks | |
and len(thinking_blocks) > 0 | |
and reasoning_content is None | |
): | |
reasoning_content = "" # set to non-empty string to ensure consistency with Anthropic | |
elif ( | |
"contentBlockIndex" in chunk_data | |
): # stop block, no 'start' or 'delta' object | |
is_empty = self.check_empty_tool_call_args() | |
if is_empty: | |
tool_use = { | |
"id": None, | |
"type": "function", | |
"function": { | |
"name": None, | |
"arguments": "{}", | |
}, | |
"index": chunk_data["contentBlockIndex"], | |
} | |
elif "stopReason" in chunk_data: | |
finish_reason = map_finish_reason(chunk_data.get("stopReason", "stop")) | |
elif "usage" in chunk_data: | |
usage = converse_config._transform_usage(chunk_data.get("usage", {})) | |
model_response_provider_specific_fields = {} | |
if "trace" in chunk_data: | |
trace = chunk_data.get("trace") | |
model_response_provider_specific_fields["trace"] = trace | |
response = ModelResponseStream( | |
choices=[ | |
StreamingChoices( | |
finish_reason=finish_reason, | |
index=index, | |
delta=Delta( | |
content=text, | |
role="assistant", | |
tool_calls=[tool_use] if tool_use else None, | |
provider_specific_fields=( | |
provider_specific_fields | |
if provider_specific_fields | |
else None | |
), | |
thinking_blocks=thinking_blocks, | |
reasoning_content=reasoning_content, | |
), | |
) | |
], | |
usage=usage, | |
provider_specific_fields=model_response_provider_specific_fields, | |
) | |
return response | |
except Exception as e: | |
raise Exception("Received streaming error - {}".format(str(e))) | |
def _chunk_parser(self, chunk_data: dict) -> Union[GChunk, ModelResponseStream]: | |
text = "" | |
is_finished = False | |
finish_reason = "" | |
if "outputText" in chunk_data: | |
text = chunk_data["outputText"] | |
# ai21 mapping | |
elif "ai21" in self.model: # fake ai21 streaming | |
text = chunk_data.get("completions")[0].get("data").get("text") # type: ignore | |
is_finished = True | |
finish_reason = "stop" | |
######## /bedrock/converse mappings ############### | |
elif ( | |
"contentBlockIndex" in chunk_data | |
or "stopReason" in chunk_data | |
or "metrics" in chunk_data | |
or "trace" in chunk_data | |
): | |
return self.converse_chunk_parser(chunk_data=chunk_data) | |
######### /bedrock/invoke nova mappings ############### | |
elif "contentBlockDelta" in chunk_data: | |
# when using /bedrock/invoke/nova, the chunk_data is nested under "contentBlockDelta" | |
_chunk_data = chunk_data.get("contentBlockDelta", None) | |
return self.converse_chunk_parser(chunk_data=_chunk_data) | |
######## bedrock.mistral mappings ############### | |
elif "outputs" in chunk_data: | |
if ( | |
len(chunk_data["outputs"]) == 1 | |
and chunk_data["outputs"][0].get("text", None) is not None | |
): | |
text = chunk_data["outputs"][0]["text"] | |
stop_reason = chunk_data.get("stop_reason", None) | |
if stop_reason is not None: | |
is_finished = True | |
finish_reason = stop_reason | |
######## bedrock.cohere mappings ############### | |
# meta mapping | |
elif "generation" in chunk_data: | |
text = chunk_data["generation"] # bedrock.meta | |
# cohere mapping | |
elif "text" in chunk_data: | |
text = chunk_data["text"] # bedrock.cohere | |
# cohere mapping for finish reason | |
elif "finish_reason" in chunk_data: | |
finish_reason = chunk_data["finish_reason"] | |
is_finished = True | |
elif chunk_data.get("completionReason", None): | |
is_finished = True | |
finish_reason = chunk_data["completionReason"] | |
return GChunk( | |
text=text, | |
is_finished=is_finished, | |
finish_reason=finish_reason, | |
usage=None, | |
index=0, | |
tool_use=None, | |
) | |
def iter_bytes( | |
self, iterator: Iterator[bytes] | |
) -> Iterator[Union[GChunk, ModelResponseStream]]: | |
"""Given an iterator that yields lines, iterate over it & yield every event encountered""" | |
from botocore.eventstream import EventStreamBuffer | |
event_stream_buffer = EventStreamBuffer() | |
for chunk in iterator: | |
event_stream_buffer.add_data(chunk) | |
for event in event_stream_buffer: | |
message = self._parse_message_from_event(event) | |
if message: | |
# sse_event = ServerSentEvent(data=message, event="completion") | |
_data = json.loads(message) | |
yield self._chunk_parser(chunk_data=_data) | |
async def aiter_bytes( | |
self, iterator: AsyncIterator[bytes] | |
) -> AsyncIterator[Union[GChunk, ModelResponseStream]]: | |
"""Given an async iterator that yields lines, iterate over it & yield every event encountered""" | |
from botocore.eventstream import EventStreamBuffer | |
event_stream_buffer = EventStreamBuffer() | |
async for chunk in iterator: | |
event_stream_buffer.add_data(chunk) | |
for event in event_stream_buffer: | |
message = self._parse_message_from_event(event) | |
if message: | |
_data = json.loads(message) | |
yield self._chunk_parser(chunk_data=_data) | |
def _parse_message_from_event(self, event) -> Optional[str]: | |
response_dict = event.to_response_dict() | |
parsed_response = self.parser.parse(response_dict, get_response_stream_shape()) | |
if response_dict["status_code"] != 200: | |
decoded_body = response_dict["body"].decode() | |
if isinstance(decoded_body, dict): | |
error_message = decoded_body.get("message") | |
elif isinstance(decoded_body, str): | |
error_message = decoded_body | |
else: | |
error_message = "" | |
exception_status = response_dict["headers"].get(":exception-type") | |
error_message = exception_status + " " + error_message | |
raise BedrockError( | |
status_code=response_dict["status_code"], | |
message=( | |
json.dumps(error_message) | |
if isinstance(error_message, dict) | |
else error_message | |
), | |
) | |
if "chunk" in parsed_response: | |
chunk = parsed_response.get("chunk") | |
if not chunk: | |
return None | |
return chunk.get("bytes").decode() # type: ignore[no-any-return] | |
else: | |
chunk = response_dict.get("body") | |
if not chunk: | |
return None | |
return chunk.decode() # type: ignore[no-any-return] | |
class AmazonAnthropicClaudeStreamDecoder(AWSEventStreamDecoder): | |
def __init__( | |
self, | |
model: str, | |
sync_stream: bool, | |
json_mode: Optional[bool] = None, | |
) -> None: | |
""" | |
Child class of AWSEventStreamDecoder that handles the streaming response from the Anthropic family of models | |
The only difference between AWSEventStreamDecoder and AmazonAnthropicClaudeStreamDecoder is the `chunk_parser` method | |
""" | |
super().__init__(model=model) | |
self.anthropic_model_response_iterator = AnthropicModelResponseIterator( | |
streaming_response=None, | |
sync_stream=sync_stream, | |
json_mode=json_mode, | |
) | |
def _chunk_parser(self, chunk_data: dict) -> ModelResponseStream: | |
return self.anthropic_model_response_iterator.chunk_parser(chunk=chunk_data) | |
class AmazonDeepSeekR1StreamDecoder(AWSEventStreamDecoder): | |
def __init__( | |
self, | |
model: str, | |
sync_stream: bool, | |
) -> None: | |
super().__init__(model=model) | |
from litellm.llms.bedrock.chat.invoke_transformations.amazon_deepseek_transformation import ( | |
AmazonDeepseekR1ResponseIterator, | |
) | |
self.deepseek_model_response_iterator = AmazonDeepseekR1ResponseIterator( | |
streaming_response=None, | |
sync_stream=sync_stream, | |
) | |
def _chunk_parser(self, chunk_data: dict) -> Union[GChunk, ModelResponseStream]: | |
return self.deepseek_model_response_iterator.chunk_parser(chunk=chunk_data) | |
class MockResponseIterator: # for returning ai21 streaming responses | |
def __init__(self, model_response, json_mode: Optional[bool] = False): | |
self.model_response = model_response | |
self.json_mode = json_mode | |
self.is_done = False | |
# Sync iterator | |
def __iter__(self): | |
return self | |
def _handle_json_mode_chunk( | |
self, text: str, tool_calls: Optional[List[ChatCompletionToolCallChunk]] | |
) -> Tuple[str, Optional[ChatCompletionToolCallChunk]]: | |
""" | |
If JSON mode is enabled, convert the tool call to a message. | |
Bedrock returns the JSON schema as part of the tool call | |
OpenAI returns the JSON schema as part of the content, this handles placing it in the content | |
Args: | |
text: str | |
tool_use: Optional[ChatCompletionToolCallChunk] | |
Returns: | |
Tuple[str, Optional[ChatCompletionToolCallChunk]] | |
text: The text to use in the content | |
tool_use: The ChatCompletionToolCallChunk to use in the chunk response | |
""" | |
tool_use: Optional[ChatCompletionToolCallChunk] = None | |
if self.json_mode is True and tool_calls is not None: | |
message = litellm.AnthropicConfig()._convert_tool_response_to_message( | |
tool_calls=tool_calls | |
) | |
if message is not None: | |
text = message.content or "" | |
tool_use = None | |
elif tool_calls is not None and len(tool_calls) > 0: | |
tool_use = tool_calls[0] | |
return text, tool_use | |
def _chunk_parser(self, chunk_data: ModelResponse) -> GChunk: | |
try: | |
chunk_usage: Usage = getattr(chunk_data, "usage") | |
text = chunk_data.choices[0].message.content or "" # type: ignore | |
tool_use = None | |
_model_response_tool_call = cast( | |
Optional[List[ChatCompletionMessageToolCall]], | |
cast(Choices, chunk_data.choices[0]).message.tool_calls, | |
) | |
if self.json_mode is True: | |
text, tool_use = self._handle_json_mode_chunk( | |
text=text, | |
tool_calls=chunk_data.choices[0].message.tool_calls, # type: ignore | |
) | |
elif _model_response_tool_call is not None: | |
tool_use = ChatCompletionToolCallChunk( | |
id=_model_response_tool_call[0].id, | |
type="function", | |
function=ChatCompletionToolCallFunctionChunk( | |
name=_model_response_tool_call[0].function.name, | |
arguments=_model_response_tool_call[0].function.arguments, | |
), | |
index=0, | |
) | |
processed_chunk = GChunk( | |
text=text, | |
tool_use=tool_use, | |
is_finished=True, | |
finish_reason=map_finish_reason( | |
finish_reason=chunk_data.choices[0].finish_reason or "" | |
), | |
usage=ChatCompletionUsageBlock( | |
prompt_tokens=chunk_usage.prompt_tokens, | |
completion_tokens=chunk_usage.completion_tokens, | |
total_tokens=chunk_usage.total_tokens, | |
), | |
index=0, | |
) | |
return processed_chunk | |
except Exception as e: | |
raise ValueError(f"Failed to decode chunk: {chunk_data}. Error: {e}") | |
def __next__(self): | |
if self.is_done: | |
raise StopIteration | |
self.is_done = True | |
return self._chunk_parser(self.model_response) | |
# Async iterator | |
def __aiter__(self): | |
return self | |
async def __anext__(self): | |
if self.is_done: | |
raise StopAsyncIteration | |
self.is_done = True | |
return self._chunk_parser(self.model_response) | |