chore: polish code with little update (#182)

- Run Docker container as non-root user (appuser) to minimize security risks
- Add Docker HEALTHCHECK for better container orchestration
- Make CORS configurable via ALLOWED_ORIGINS env var with security warning
- Replace assertions with proper error handling (TypeError/ValueError)
- Add 30s timeout to HTTP requests to prevent hanging connections
- Disable auto-reload in production uvicorn settings
This commit is contained in:
Li Yi
2025-10-11 14:49:18 +08:00
committed by GitHub
parent 8177876e5e
commit 9cea7f9314
8 changed files with 46 additions and 21 deletions

View File

@@ -133,7 +133,7 @@ If you encounter any issues, please check the [Troubleshooting Guide](./docs/Tro
### SDK/API Usage ### SDK/API Usage
All you need is the API Key and the API Base URL. If you didn't set up your own key, then the default API Key (`bedrock`) will be used. All you need is the API Key and the API Base URL. If you didn't set up your own key following Step 1, the application will fail to start with an error message indicating that the API Key is not configured.
Now, you can try out the proxy APIs. Let's say you want to test Claude 3 Sonnet model (model ID: `anthropic.claude-3-sonnet-20240229-v1:0`)... Now, you can try out the proxy APIs. Let's say you want to test Claude 3 Sonnet model (model ID: `anthropic.claude-3-sonnet-20240229-v1:0`)...

8
THIRD_PARTY Normal file
View File

@@ -0,0 +1,8 @@
certifi
SPDX-License-Identifier: MPL-2.0
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
https://github.com/certifi/python-certifi

View File

@@ -256,8 +256,8 @@ Resources:
Ref: ContainerImageUri Ref: ContainerImageUri
Name: proxy-api Name: proxy-api
PortMappings: PortMappings:
- ContainerPort: 80 - ContainerPort: 8080
HostPort: 80 HostPort: 8080
Protocol: tcp Protocol: tcp
Secrets: Secrets:
- Name: API_KEY - Name: API_KEY
@@ -303,7 +303,7 @@ Resources:
HealthCheckGracePeriodSeconds: 60 HealthCheckGracePeriodSeconds: 60
LoadBalancers: LoadBalancers:
- ContainerName: proxy-api - ContainerName: proxy-api
ContainerPort: 80 ContainerPort: 8080
TargetGroupArn: TargetGroupArn:
Ref: ProxyALBListenerTargetsGroup187739FA Ref: ProxyALBListenerTargetsGroup187739FA
NetworkConfiguration: NetworkConfiguration:
@@ -340,7 +340,7 @@ Resources:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
Description: Load balancer to target Description: Load balancer to target
FromPort: 80 FromPort: 8080
GroupId: GroupId:
Fn::GetAtt: Fn::GetAtt:
- ProxyApiServiceSecurityGroup51EBD9B8 - ProxyApiServiceSecurityGroup51EBD9B8
@@ -350,7 +350,7 @@ Resources:
Fn::GetAtt: Fn::GetAtt:
- ProxyALBSecurityGroup0D6CA3DA - ProxyALBSecurityGroup0D6CA3DA
- GroupId - GroupId
ToPort: 80 ToPort: 8080
DependsOn: DependsOn:
- ProxyTaskRoleDefaultPolicy933321B8 - ProxyTaskRoleDefaultPolicy933321B8
- ProxyTaskRole5DB6A540 - ProxyTaskRole5DB6A540
@@ -396,13 +396,13 @@ Resources:
Fn::GetAtt: Fn::GetAtt:
- ProxyApiServiceSecurityGroup51EBD9B8 - ProxyApiServiceSecurityGroup51EBD9B8
- GroupId - GroupId
FromPort: 80 FromPort: 8080
GroupId: GroupId:
Fn::GetAtt: Fn::GetAtt:
- ProxyALBSecurityGroup0D6CA3DA - ProxyALBSecurityGroup0D6CA3DA
- GroupId - GroupId
IpProtocol: tcp IpProtocol: tcp
ToPort: 80 ToPort: 8080
ProxyALBListener933E9515: ProxyALBListener933E9515:
Type: AWS::ElasticLoadBalancingV2::Listener Type: AWS::ElasticLoadBalancingV2::Listener
Properties: Properties:
@@ -421,7 +421,7 @@ Resources:
HealthCheckIntervalSeconds: 60 HealthCheckIntervalSeconds: 60
HealthCheckPath: /health HealthCheckPath: /health
HealthCheckTimeoutSeconds: 30 HealthCheckTimeoutSeconds: 30
Port: 80 Port: 8080
Protocol: HTTP Protocol: HTTP
TargetGroupAttributes: TargetGroupAttributes:
- Key: stickiness.enabled - Key: stickiness.enabled

View File

@@ -8,6 +8,15 @@ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./api /app/api COPY ./api /app/api
ENV PORT=80 # Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser && \
chown -R appuser:appuser /app
USER appuser
ENV PORT=8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/models').read()"
CMD ["sh", "-c", "uvicorn api.app:app --host 0.0.0.0 --port ${PORT}"] CMD ["sh", "-c", "uvicorn api.app:app --host 0.0.0.0 --port ${PORT}"]

View File

@@ -1,4 +1,5 @@
import logging import logging
import os
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
@@ -23,9 +24,16 @@ logging.basicConfig(
) )
app = FastAPI(**config) app = FastAPI(**config)
allowed_origins = os.environ.get("ALLOWED_ORIGINS", "*")
origins_list = [origin.strip() for origin in allowed_origins.split(",")] if allowed_origins != "*" else ["*"]
# Warn if CORS allows all origins
if origins_list == ["*"]:
logging.warning("CORS is configured to allow all origins (*). Set ALLOWED_ORIGINS environment variable to restrict access.")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=origins_list, # nosec - configurable via ALLOWED_ORIGINS env var
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -61,4 +69,5 @@ async def validation_exception_handler(request, exc):
handler = Mangum(app) handler = Mangum(app)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) # Bind to 0.0.0.0 for container environments, network is handled by network policies and load balancers
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False) # nosec B104

View File

@@ -7,8 +7,6 @@ from botocore.exceptions import ClientError
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from api.setting import DEFAULT_API_KEYS
api_key_param = os.environ.get("API_KEY_PARAM_NAME") api_key_param = os.environ.get("API_KEY_PARAM_NAME")
api_key_secret_arn = os.environ.get("API_KEY_SECRET_ARN") api_key_secret_arn = os.environ.get("API_KEY_SECRET_ARN")
api_key_env = os.environ.get("API_KEY") api_key_env = os.environ.get("API_KEY")
@@ -31,8 +29,9 @@ elif api_key_secret_arn:
elif api_key_env: elif api_key_env:
api_key = api_key_env api_key = api_key_env
else: else:
# For local use only. raise RuntimeError(
api_key = DEFAULT_API_KEYS "API Key is not configured. Please set up your API Key."
)
security = HTTPBearer() security = HTTPBearer()

View File

@@ -310,7 +310,8 @@ class BedrockModel(BaseChatModel):
if message.role != "system": if message.role != "system":
# ignore system messages here # ignore system messages here
continue continue
assert isinstance(message.content, str) if not isinstance(message.content, str):
raise TypeError(f"System message content must be a string, got {type(message.content).__name__}")
system_prompts.append({"text": message.content}) system_prompts.append({"text": message.content})
return system_prompts return system_prompts
@@ -580,7 +581,8 @@ class BedrockModel(BaseChatModel):
tool_config["toolChoice"] = {"auto": {}} tool_config["toolChoice"] = {"auto": {}}
else: else:
# Specific tool to use # Specific tool to use
assert "function" in chat_request.tool_choice if "function" not in chat_request.tool_choice:
raise ValueError("tool_choice must contain 'function' key when specifying a specific tool")
tool_config["toolChoice"] = {"tool": {"name": chat_request.tool_choice["function"].get("name", "")}} tool_config["toolChoice"] = {"tool": {"name": chat_request.tool_choice["function"].get("name", "")}}
args["toolConfig"] = tool_config args["toolConfig"] = tool_config
# add Additional fields to enable extend thinking # add Additional fields to enable extend thinking
@@ -784,7 +786,7 @@ class BedrockModel(BaseChatModel):
return base64.b64decode(image_data), content_type.group(1) return base64.b64decode(image_data), content_type.group(1)
# Send a request to the image URL # Send a request to the image URL
response = requests.get(image_url) response = requests.get(image_url, timeout=30)
# 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")

View File

@@ -1,7 +1,5 @@
import os import os
DEFAULT_API_KEYS = "bedrock"
API_ROUTE_PREFIX = os.environ.get("API_ROUTE_PREFIX", "/api/v1") API_ROUTE_PREFIX = os.environ.get("API_ROUTE_PREFIX", "/api/v1")
TITLE = "Amazon Bedrock Proxy APIs" TITLE = "Amazon Bedrock Proxy APIs"