apply ruff linter

This commit is contained in:
Aiden Dai
2025-03-13 13:50:57 +08:00
parent 33e8fcfd3b
commit f21b9a2e84
10 changed files with 128 additions and 181 deletions
-19
View File
@@ -1,19 +0,0 @@
[flake8]
max-line-length = 120
ignore =
E203,W191,W503
exclude =
build
.git
__pycache__
.tox
venv
.venv
.venv-test
tmp*
deployment
cdk.out
node_modules
max-complexity = 10
require-code = True
+8
View File
@@ -0,0 +1,8 @@
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.10
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
+23
View File
@@ -0,0 +1,23 @@
line-length = 120
indent-width = 4
target-version = "py312"
exclude = [
".venv",
".vscode",
"test/*"
]
[lint]
select = ["E", "F"]
ignore = [
"E501",
"B008",
"C901",
"F401",
"W191",
]
[format]
# use double quotes for strings.
quote-style = "double"
+5 -11
View File
@@ -16,9 +16,7 @@ if api_key_param:
# For backward compatibility. # For backward compatibility.
# Please now use secrets manager instead. # Please now use secrets manager instead.
ssm = boto3.client("ssm") ssm = boto3.client("ssm")
api_key = ssm.get_parameter(Name=api_key_param, WithDecryption=True)["Parameter"][ api_key = ssm.get_parameter(Name=api_key_param, WithDecryption=True)["Parameter"]["Value"]
"Value"
]
elif api_key_secret_arn: elif api_key_secret_arn:
sm = boto3.client("secretsmanager") sm = boto3.client("secretsmanager")
try: try:
@@ -26,11 +24,9 @@ elif api_key_secret_arn:
if "SecretString" in response: if "SecretString" in response:
secret = json.loads(response["SecretString"]) secret = json.loads(response["SecretString"])
api_key = secret["api_key"] api_key = secret["api_key"]
except ClientError as e: except ClientError:
raise RuntimeError( raise RuntimeError("Unable to retrieve API KEY, please ensure the secret ARN is correct")
"Unable to retrieve API KEY, please ensure the secret ARN is correct" except KeyError:
)
except KeyError as e:
raise RuntimeError('Please ensure the secret contains a "api_key" field') raise RuntimeError('Please ensure the secret contains a "api_key" field')
elif api_key_env: elif api_key_env:
api_key = api_key_env api_key = api_key_env
@@ -45,6 +41,4 @@ def api_key_auth(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)], credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
): ):
if credentials.credentials != api_key: if credentials.credentials != api_key:
raise HTTPException( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key")
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key"
)
+1 -3
View File
@@ -43,9 +43,7 @@ class BaseChatModel(ABC):
return "chatcmpl-" + str(uuid.uuid4())[:8] return "chatcmpl-" + str(uuid.uuid4())[:8]
@staticmethod @staticmethod
def stream_response_to_bytes( def stream_response_to_bytes(response: ChatStreamResponse | None = None) -> bytes:
response: ChatStreamResponse | None = None
) -> bytes:
if response: if response:
# to populate other fields when using exclude_unset=True # to populate other fields when using exclude_unset=True
response.system_fingerprint = "fp" response.system_fingerprint = "fp"
+41 -88
View File
@@ -36,7 +36,6 @@ from api.schema import (
EmbeddingsResponse, EmbeddingsResponse,
EmbeddingsUsage, EmbeddingsUsage,
Embedding, Embedding,
) )
from api.setting import DEBUG, AWS_REGION, ENABLE_CROSS_REGION_INFERENCE, DEFAULT_MODEL from api.setting import DEBUG, AWS_REGION, ENABLE_CROSS_REGION_INFERENCE, DEFAULT_MODEL
@@ -50,15 +49,15 @@ bedrock_runtime = boto3.client(
config=config, config=config,
) )
bedrock_client = boto3.client( bedrock_client = boto3.client(
service_name='bedrock', service_name="bedrock",
region_name=AWS_REGION, region_name=AWS_REGION,
config=config, config=config,
) )
def get_inference_region_prefix(): def get_inference_region_prefix():
if AWS_REGION.startswith('ap-'): if AWS_REGION.startswith("ap-"):
return 'apac' return "apac"
return AWS_REGION[:2] return AWS_REGION[:2]
@@ -88,49 +87,38 @@ def list_bedrock_models() -> dict:
profile_list = [] profile_list = []
if ENABLE_CROSS_REGION_INFERENCE: if ENABLE_CROSS_REGION_INFERENCE:
# List system defined inference profile IDs # List system defined inference profile IDs
response = bedrock_client.list_inference_profiles( response = bedrock_client.list_inference_profiles(maxResults=1000, typeEquals="SYSTEM_DEFINED")
maxResults=1000, profile_list = [p["inferenceProfileId"] for p in response["inferenceProfileSummaries"]]
typeEquals='SYSTEM_DEFINED'
)
profile_list = [p['inferenceProfileId'] for p in response['inferenceProfileSummaries']]
# List foundation models, only cares about text outputs here. # List foundation models, only cares about text outputs here.
response = bedrock_client.list_foundation_models( response = bedrock_client.list_foundation_models(byOutputModality="TEXT")
byOutputModality='TEXT'
)
for model in response['modelSummaries']: for model in response["modelSummaries"]:
model_id = model.get('modelId', 'N/A') model_id = model.get("modelId", "N/A")
stream_supported = model.get('responseStreamingSupported', True) stream_supported = model.get("responseStreamingSupported", True)
status = model['modelLifecycle'].get('status', 'ACTIVE') status = model["modelLifecycle"].get("status", "ACTIVE")
# currently, use this to filter out rerank models and legacy models # currently, use this to filter out rerank models and legacy models
if not stream_supported or status not in ["ACTIVE", "LEGACY"]: if not stream_supported or status not in ["ACTIVE", "LEGACY"]:
continue continue
inference_types = model.get('inferenceTypesSupported', []) inference_types = model.get("inferenceTypesSupported", [])
input_modalities = model['inputModalities'] input_modalities = model["inputModalities"]
# Add on-demand model list # Add on-demand model list
if 'ON_DEMAND' in inference_types: if "ON_DEMAND" in inference_types:
model_list[model_id] = { model_list[model_id] = {"modalities": input_modalities}
'modalities': input_modalities
}
# Add cross-region inference model list. # Add cross-region inference model list.
profile_id = cr_inference_prefix + '.' + model_id profile_id = cr_inference_prefix + "." + model_id
if profile_id in profile_list: if profile_id in profile_list:
model_list[profile_id] = { model_list[profile_id] = {"modalities": input_modalities}
'modalities': input_modalities
}
except Exception as e: except Exception as e:
logger.error(f"Unable to list models: {str(e)}") logger.error(f"Unable to list models: {str(e)}")
if not model_list: if not model_list:
# In case stack not updated. # In case stack not updated.
model_list[DEFAULT_MODEL] = { model_list[DEFAULT_MODEL] = {"modalities": ["TEXT", "IMAGE"]}
'modalities': ["TEXT", "IMAGE"]
}
return model_list return model_list
@@ -140,7 +128,6 @@ bedrock_model_list = list_bedrock_models()
class BedrockModel(BaseChatModel): class BedrockModel(BaseChatModel):
def list_models(self) -> list[str]: def list_models(self) -> list[str]:
"""Always refresh the latest model list""" """Always refresh the latest model list"""
global bedrock_model_list global bedrock_model_list
@@ -224,10 +211,7 @@ class BedrockModel(BaseChatModel):
logger.info("Proxy response :" + stream_response.model_dump_json()) logger.info("Proxy response :" + stream_response.model_dump_json())
if stream_response.choices: if stream_response.choices:
yield self.stream_response_to_bytes(stream_response) yield self.stream_response_to_bytes(stream_response)
elif ( elif chat_request.stream_options and chat_request.stream_options.include_usage:
chat_request.stream_options
and chat_request.stream_options.include_usage
):
# An empty choices for Usage as per OpenAI doc below: # An empty choices for Usage as per OpenAI doc below:
# if you set stream_options: {"include_usage": true}. # if you set stream_options: {"include_usage": true}.
# an additional chunk will be streamed before the data: [DONE] message. # an additional chunk will be streamed before the data: [DONE] message.
@@ -277,9 +261,7 @@ class BedrockModel(BaseChatModel):
messages.append( messages.append(
{ {
"role": message.role, "role": message.role,
"content": self._parse_content_parts( "content": self._parse_content_parts(message, chat_request.model),
message, chat_request.model
),
} }
) )
elif isinstance(message, AssistantMessage): elif isinstance(message, AssistantMessage):
@@ -288,9 +270,7 @@ class BedrockModel(BaseChatModel):
messages.append( messages.append(
{ {
"role": message.role, "role": message.role,
"content": self._parse_content_parts( "content": self._parse_content_parts(message, chat_request.model),
message, chat_request.model
),
} }
) )
if message.tool_calls: if message.tool_calls:
@@ -305,7 +285,7 @@ class BedrockModel(BaseChatModel):
"toolUse": { "toolUse": {
"toolUseId": tool_call.id, "toolUseId": tool_call.id,
"name": tool_call.function.name, "name": tool_call.function.name,
"input": tool_input "input": tool_input,
} }
} }
], ],
@@ -364,16 +344,13 @@ class BedrockModel(BaseChatModel):
# Search through the list of messages and combine messages from the same role into one list # Search through the list of messages and combine messages from the same role into one list
for message in messages: for message in messages:
next_role = message['role'] next_role = message["role"]
next_content = message['content'] next_content = message["content"]
# If the next role is different from the previous message, add the previous role's messages to the list # If the next role is different from the previous message, add the previous role's messages to the list
if next_role != current_role: if next_role != current_role:
if current_content: if current_content:
reformatted_messages.append({ reformatted_messages.append({"role": current_role, "content": current_content})
"role": current_role,
"content": current_content
})
# Switch to the new role # Switch to the new role
current_role = next_role current_role = next_role
current_content = [] current_content = []
@@ -386,10 +363,7 @@ class BedrockModel(BaseChatModel):
# Add the last role's messages to the list # Add the last role's messages to the list
if current_content: if current_content:
reformatted_messages.append({ reformatted_messages.append({"role": current_role, "content": current_content})
"role": current_role,
"content": current_content
})
return reformatted_messages return reformatted_messages
@@ -426,25 +400,20 @@ class BedrockModel(BaseChatModel):
# From OpenAI api, the max_token is not supported in reasoning mode # From OpenAI api, the max_token is not supported in reasoning mode
# Use max_completion_tokens if provided. # Use max_completion_tokens if provided.
max_tokens = chat_request.max_completion_tokens if chat_request.max_completion_tokens else chat_request.max_tokens max_tokens = (
chat_request.max_completion_tokens if chat_request.max_completion_tokens else chat_request.max_tokens
)
budget_tokens = self._calc_budget_tokens(max_tokens, chat_request.reasoning_effort) budget_tokens = self._calc_budget_tokens(max_tokens, chat_request.reasoning_effort)
inference_config["maxTokens"] = max_tokens inference_config["maxTokens"] = max_tokens
# unset topP - Not supported # unset topP - Not supported
inference_config.pop("topP") inference_config.pop("topP")
args["additionalModelRequestFields"] = { args["additionalModelRequestFields"] = {
"reasoning_config": { "reasoning_config": {"type": "enabled", "budget_tokens": budget_tokens}
"type": "enabled",
"budget_tokens": budget_tokens
}
} }
# add tool config # add tool config
if chat_request.tools: if chat_request.tools:
args["toolConfig"] = { args["toolConfig"] = {"tools": [self._convert_tool_spec(t.function) for t in chat_request.tools]}
"tools": [
self._convert_tool_spec(t.function) for t in chat_request.tools
]
}
if chat_request.tool_choice and not chat_request.model.startswith("meta.llama3-1-"): if chat_request.tool_choice and not chat_request.model.startswith("meta.llama3-1-"):
if isinstance(chat_request.tool_choice, str): if isinstance(chat_request.tool_choice, str):
@@ -458,7 +427,8 @@ class BedrockModel(BaseChatModel):
# Specific tool to use # Specific tool to use
assert "function" in chat_request.tool_choice assert "function" in chat_request.tool_choice
args["toolConfig"]["toolChoice"] = { args["toolConfig"]["toolChoice"] = {
"tool": {"name": chat_request.tool_choice["function"].get("name", "")}} "tool": {"name": chat_request.tool_choice["function"].get("name", "")}
}
return args return args
def _create_response( def _create_response(
@@ -470,7 +440,6 @@ class BedrockModel(BaseChatModel):
input_tokens: int = 0, input_tokens: int = 0,
output_tokens: int = 0, output_tokens: int = 0,
) -> ChatResponse: ) -> ChatResponse:
message = ChatResponseMessage( message = ChatResponseMessage(
role="assistant", role="assistant",
) )
@@ -524,9 +493,7 @@ class BedrockModel(BaseChatModel):
response.created = int(time.time()) response.created = int(time.time())
return response return response
def _create_response_stream( def _create_response_stream(self, model_id: str, message_id: str, chunk: dict) -> ChatStreamResponse | None:
self, model_id: str, message_id: str, chunk: dict
) -> ChatStreamResponse | None:
"""Parsing the Bedrock stream response chunk. """Parsing the Bedrock stream response chunk.
Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html#message-inference-examples Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html#message-inference-examples
@@ -583,7 +550,7 @@ class BedrockModel(BaseChatModel):
index=index, index=index,
function=ResponseFunction( function=ResponseFunction(
arguments=delta["toolUse"]["input"], arguments=delta["toolUse"]["input"],
) ),
) )
] ]
) )
@@ -641,7 +608,6 @@ class BedrockModel(BaseChatModel):
response = requests.get(image_url) response = requests.get(image_url)
# Check if the request was successful # Check if the request was successful
if response.status_code == 200: if response.status_code == 200:
content_type = response.headers.get("Content-Type") content_type = response.headers.get("Content-Type")
if not content_type.startswith("image"): if not content_type.startswith("image"):
content_type = "image/jpeg" content_type = "image/jpeg"
@@ -649,9 +615,7 @@ class BedrockModel(BaseChatModel):
image_content = response.content image_content = response.content
return image_content, content_type return image_content, content_type
else: else:
raise HTTPException( raise HTTPException(status_code=500, detail="Unable to access the image url")
status_code=500, detail="Unable to access the image url"
)
def _parse_content_parts( def _parse_content_parts(
self, self,
@@ -695,7 +659,7 @@ class BedrockModel(BaseChatModel):
@staticmethod @staticmethod
def is_supported_modality(model_id: str, modality: str = "IMAGE") -> bool: def is_supported_modality(model_id: str, modality: str = "IMAGE") -> bool:
model = bedrock_model_list.get(model_id) model = bedrock_model_list.get(model_id)
modalities = model.get('modalities', []) modalities = model.get("modalities", [])
if modality in modalities: if modality in modalities:
return True return True
return False return False
@@ -740,7 +704,7 @@ class BedrockModel(BaseChatModel):
"max_tokens": "length", "max_tokens": "length",
"stop_sequence": "stop", "stop_sequence": "stop",
"complete": "stop", "complete": "stop",
"content_filtered": "content_filter" "content_filtered": "content_filter",
} }
return finish_reason_mapping.get(finish_reason.lower(), finish_reason.lower()) return finish_reason_mapping.get(finish_reason.lower(), finish_reason.lower())
return None return None
@@ -803,7 +767,6 @@ class BedrockEmbeddingsModel(BaseEmbeddingsModel, ABC):
class CohereEmbeddingsModel(BedrockEmbeddingsModel): class CohereEmbeddingsModel(BedrockEmbeddingsModel):
def _parse_args(self, embeddings_request: EmbeddingsRequest) -> dict: def _parse_args(self, embeddings_request: EmbeddingsRequest) -> dict:
texts = [] texts = []
if isinstance(embeddings_request.input, str): if isinstance(embeddings_request.input, str):
@@ -834,9 +797,7 @@ class CohereEmbeddingsModel(BedrockEmbeddingsModel):
return args return args
def embed(self, embeddings_request: EmbeddingsRequest) -> EmbeddingsResponse: def embed(self, embeddings_request: EmbeddingsRequest) -> EmbeddingsResponse:
response = self._invoke_model( response = self._invoke_model(args=self._parse_args(embeddings_request), model_id=embeddings_request.model)
args=self._parse_args(embeddings_request), model_id=embeddings_request.model
)
response_body = json.loads(response.get("body").read()) response_body = json.loads(response.get("body").read())
if DEBUG: if DEBUG:
logger.info("Bedrock response body: " + str(response_body)) logger.info("Bedrock response body: " + str(response_body))
@@ -849,19 +810,13 @@ class CohereEmbeddingsModel(BedrockEmbeddingsModel):
class TitanEmbeddingsModel(BedrockEmbeddingsModel): class TitanEmbeddingsModel(BedrockEmbeddingsModel):
def _parse_args(self, embeddings_request: EmbeddingsRequest) -> dict: def _parse_args(self, embeddings_request: EmbeddingsRequest) -> dict:
if isinstance(embeddings_request.input, str): if isinstance(embeddings_request.input, str):
input_text = embeddings_request.input input_text = embeddings_request.input
elif ( elif isinstance(embeddings_request.input, list) and len(embeddings_request.input) == 1:
isinstance(embeddings_request.input, list)
and len(embeddings_request.input) == 1
):
input_text = embeddings_request.input[0] input_text = embeddings_request.input[0]
else: else:
raise ValueError( raise ValueError("Amazon Titan Embeddings models support only single strings as input.")
"Amazon Titan Embeddings models support only single strings as input."
)
args = { args = {
"inputText": input_text, "inputText": input_text,
# Note: inputImage is not supported! # Note: inputImage is not supported!
@@ -875,9 +830,7 @@ class TitanEmbeddingsModel(BedrockEmbeddingsModel):
return args return args
def embed(self, embeddings_request: EmbeddingsRequest) -> EmbeddingsResponse: def embed(self, embeddings_request: EmbeddingsRequest) -> EmbeddingsResponse:
response = self._invoke_model( response = self._invoke_model(args=self._parse_args(embeddings_request), model_id=embeddings_request.model)
args=self._parse_args(embeddings_request), model_id=embeddings_request.model
)
response_body = json.loads(response.get("body").read()) response_body = json.loads(response.get("body").read())
if DEBUG: if DEBUG:
logger.info("Bedrock response body: " + str(response_body)) logger.info("Bedrock response body: " + str(response_body))
+2 -4
View File
@@ -30,7 +30,7 @@ async def chat_completions(
} }
], ],
), ),
] ],
): ):
if chat_request.model.lower().startswith("gpt-"): if chat_request.model.lower().startswith("gpt-"):
chat_request.model = DEFAULT_MODEL chat_request.model = DEFAULT_MODEL
@@ -39,7 +39,5 @@ async def chat_completions(
model = BedrockModel() model = BedrockModel()
model.validate(chat_request) model.validate(chat_request)
if chat_request.stream: if chat_request.stream:
return StreamingResponse( return StreamingResponse(content=model.chat_stream(chat_request), media_type="text/event-stream")
content=model.chat_stream(chat_request), media_type="text/event-stream"
)
return model.chat(chat_request) return model.chat(chat_request)
+2 -4
View File
@@ -21,13 +21,11 @@ async def embeddings(
examples=[ examples=[
{ {
"model": "cohere.embed-multilingual-v3", "model": "cohere.embed-multilingual-v3",
"input": [ "input": ["Your text string goes here"],
"Your text string goes here"
],
} }
], ],
), ),
] ],
): ):
if embeddings_request.model.lower().startswith("text-embedding-"): if embeddings_request.model.lower().startswith("text-embedding-"):
embeddings_request.model = DEFAULT_EMBEDDING_MODEL embeddings_request.model = DEFAULT_EMBEDDING_MODEL
+2 -4
View File
@@ -22,9 +22,7 @@ async def validate_model_id(model_id: str):
@router.get("", response_model=Models) @router.get("", response_model=Models)
async def list_models(): async def list_models():
model_list = [ model_list = [Model(id=model_id) for model_id in chat_model.list_models()]
Model(id=model_id) for model_id in chat_model.list_models()
]
return Models(data=model_list) return Models(data=model_list)
@@ -36,7 +34,7 @@ async def get_model(
model_id: Annotated[ model_id: Annotated[
str, str,
Path(description="Model ID", example="anthropic.claude-3-sonnet-20240229-v1:0"), Path(description="Model ID", example="anthropic.claude-3-sonnet-20240229-v1:0"),
] ],
): ):
await validate_model_id(model_id) await validate_model_id(model_id)
return Model(id=model_id) return Model(id=model_id)
+2 -6
View File
@@ -13,10 +13,6 @@ Use OpenAI-Compatible RESTful APIs for Amazon Bedrock models.
DEBUG = os.environ.get("DEBUG", "false").lower() != "false" DEBUG = os.environ.get("DEBUG", "false").lower() != "false"
AWS_REGION = os.environ.get("AWS_REGION", "us-west-2") AWS_REGION = os.environ.get("AWS_REGION", "us-west-2")
DEFAULT_MODEL = os.environ.get( DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "anthropic.claude-3-sonnet-20240229-v1:0")
"DEFAULT_MODEL", "anthropic.claude-3-sonnet-20240229-v1:0" DEFAULT_EMBEDDING_MODEL = os.environ.get("DEFAULT_EMBEDDING_MODEL", "cohere.embed-multilingual-v3")
)
DEFAULT_EMBEDDING_MODEL = os.environ.get(
"DEFAULT_EMBEDDING_MODEL", "cohere.embed-multilingual-v3"
)
ENABLE_CROSS_REGION_INFERENCE = os.environ.get("ENABLE_CROSS_REGION_INFERENCE", "true").lower() != "false" ENABLE_CROSS_REGION_INFERENCE = os.environ.get("ENABLE_CROSS_REGION_INFERENCE", "true").lower() != "false"