Skip to content
This repository was archived by the owner on Apr 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
*Issue #, if available:*
## Description

*Description of changes:*
<!-- Concise description of what this PR is tackling. -->

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
## Linked Issues

<!-- See https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue -->

## Checklist

- [ ] I have performed a self-review of my code
- [ ] I have added appropriate tests
- [ ] I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

11 changes: 11 additions & 0 deletions .github/workflows/aws-genai-cicd-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,14 @@ jobs:
echo "GitHub Token is not set"
fi
echo "AWS_ROLE_TO_ASSUME: ${{ vars.AWS_ROLE_TO_ASSUME_VAR }}"

# lint the code using ruff
- name: Run Ruff linter
uses: astral-sh/ruff-action@v1
with:
args: check ./src

- name: Run Ruff formatter check
uses: astral-sh/ruff-action@v1
with:
args: "format --check"
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,19 @@ login: ## Login to docker
.PHONY: tests
tests:
PYTHONPATH=src pytest

.PHONY: lint
lint: # Run pre-commit on staged/changed files
pre-commit run

.PHONY: check
check: # Run all pre-commit hooks on all files (useful for CI or full check)
pre-commit run --all-files

.PHONY: format
format: # Manually run ruff formatter on all files
ruff format .

.PHONY: pre-commit-install
pre-commit-install: # Install pre-commit hooks changes
pre-commit install
16 changes: 12 additions & 4 deletions src/api/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import logging
import os
import uvicorn

import uvicorn
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import PlainTextResponse
from mangum import Mangum

from api.setting import API_ROUTE_PREFIX, DESCRIPTION, SUMMARY, PROVIDER, TITLE, USE_MODEL_MAPPING, VERSION
from api.modelmapper import load_model_map
from api.setting import API_ROUTE_PREFIX, DESCRIPTION, PROVIDER, SUMMARY, TITLE, USE_MODEL_MAPPING, VERSION


def is_aws():
env = os.getenv("AWS_EXECUTION_ENV")
Expand All @@ -21,8 +22,9 @@ def is_aws():
return True
return False


provider = PROVIDER.lower() if PROVIDER else None
if provider == None:
if provider is None:
if is_aws():
provider = "aws"
else:
Expand Down Expand Up @@ -55,30 +57,36 @@ def is_aws():

if provider != "aws":
from api.routers.gcp import chat, embeddings
logging.info(f"Proxy target set to: GCP")

logging.info("Proxy target set to: GCP")
app.include_router(chat.router, prefix=API_ROUTE_PREFIX)
app.include_router(embeddings.router, prefix=API_ROUTE_PREFIX)
else:
from api.routers import chat, embeddings, model

logging.info("No proxy target set. Using internal routers.")
app.include_router(model.router, prefix=API_ROUTE_PREFIX)
app.include_router(chat.router, prefix=API_ROUTE_PREFIX)
app.include_router(embeddings.router, prefix=API_ROUTE_PREFIX)


@app.get("/", include_in_schema=False)
async def root():
"""Root endpoint for the API"""
return {"status": "OK"}


@app.get("/health")
async def health():
"""For health check if needed"""
return {"status": "OK"}


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)


handler = Mangum(app)

if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion src/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
raise RuntimeError("Unable to retrieve API KEY, please ensure the secret ARN is correct")
except KeyError:
raise RuntimeError('Please ensure the secret contains a "api_key" field')
elif api_key_env != None:
elif api_key_env is not None:
api_key = api_key_env
else:
# For local use only.
Expand Down
12 changes: 7 additions & 5 deletions src/api/gcp/credentials/metadata.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import logging
import requests

import requests
from google.auth import default
from google.auth.transport.requests import Request as AuthRequest

from api.setting import GOOGLE_CLOUD_PROJECT, GCP_REGION

from api.setting import GCP_REGION, GOOGLE_CLOUD_PROJECT

# GCP credentials and project details
credentials = None
project_id = None
location = None


def get_gcp_project_details():
from google.auth import default

Expand All @@ -29,17 +29,19 @@ def get_gcp_project_details():
zone = requests.get(
"http://metadata.google.internal/computeMetadata/v1/instance/zone",
headers={"Metadata-Flavor": "Google"},
timeout=1
timeout=1,
).text
location = zone.split("/")[-1].rsplit("-", 1)[0]

except Exception:
logging.warning(f"Error: Failed to get project and location from metadata server. Using local settings.")
logging.warning("Error: Failed to get project and location from metadata server. Using local settings.")

return credentials, project_id, location


credentials, project_id, location = get_gcp_project_details()


# Utility: get service account access token
def get_access_token():
credentials, _ = default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
Expand Down
5 changes: 3 additions & 2 deletions src/api/modelmapper.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import os
import json
import os
from pathlib import Path

_model_map = None


def load_model_map():
global _model_map
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
modelmap_path = os.path.join(BASE_DIR, "../data/modelmap.json")
with open(modelmap_path, "r") as f:
_model_map = json.load(f)


def get_model(provider, model, fallback_model):
provider = provider.lower()
if model is None or model == "":
Expand All @@ -19,4 +21,3 @@ def get_model(provider, model, fallback_model):

available_models = _model_map.get(provider, {})
return available_models.get(model, model)

11 changes: 5 additions & 6 deletions src/api/models/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from fastapi import HTTPException
from starlette.concurrency import run_in_threadpool

from api.modelmapper import get_model
from api.models.base import BaseChatModel, BaseEmbeddingsModel
from api.schema import (
AssistantMessage,
Expand All @@ -39,7 +40,6 @@
UserMessage,
)
from api.setting import AWS_REGION, DEBUG, DEFAULT_MODEL, ENABLE_CROSS_REGION_INFERENCE
from api.modelmapper import get_model

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -145,16 +145,15 @@ def validate(self, chat_request: ChatRequest):
if DEBUG:
logger.debug("Bedrock validate " + chat_request.model + " list: " + json.dumps(bedrock_model_list))
logger.debug(f"Checking model: {repr(chat_request.model)}")
logger.debug(f"Available keys include: {repr('anthropic.claude-3-5-sonnet-20241022-v2:0') in bedrock_model_list}")
logger.debug(
f"Available keys include: {repr('anthropic.claude-3-5-sonnet-20241022-v2:0') in bedrock_model_list}"
)

# check if model is supported
if chat_request.model not in bedrock_model_list.keys():
if DEBUG:
logger.debug(f"Bedrock list: {list(bedrock_model_list.keys())}")
error = (
f"Unsupported model '{chat_request.model}'. "
f"list of known models: {bedrock_model_list.keys()}"
)
error = f"Unsupported model '{chat_request.model}'. list of known models: {bedrock_model_list.keys()}"
logger.error(error)

if error:
Expand Down
7 changes: 3 additions & 4 deletions src/api/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
from fastapi.responses import StreamingResponse

from api.auth import api_key_auth
from api.modelmapper import get_model
from api.models.bedrock import BedrockModel
from api.schema import ChatRequest, ChatResponse, ChatStreamResponse, Error
from api.modelmapper import get_model

from api.setting import DEFAULT_MODEL, USE_MODEL_MAPPING

router = APIRouter(
Expand Down Expand Up @@ -36,10 +35,10 @@ async def chat_completions(
),
],
):
if chat_request.model != None and chat_request.model.lower().startswith("gpt-"):
if chat_request.model is not None and chat_request.model.lower().startswith("gpt-"):
chat_request.model = DEFAULT_MODEL

# replace with mapped model name
# replace with mapped model name
if USE_MODEL_MAPPING:
req_model = chat_request.model
req_model = get_model("aws", req_model, "chat-default")
Expand Down
4 changes: 2 additions & 2 deletions src/api/routers/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from fastapi import APIRouter, Body, Depends

from api.auth import api_key_auth
from api.modelmapper import get_model
from api.models.bedrock import get_embeddings_model
from api.schema import EmbeddingsRequest, EmbeddingsResponse
from api.setting import DEFAULT_EMBEDDING_MODEL
from api.modelmapper import get_model

router = APIRouter(
prefix="/embeddings",
Expand All @@ -28,7 +28,7 @@ async def embeddings(
),
],
):
if embeddings_request.model != None and embeddings_request.model.lower().startswith("text-embedding-"):
if embeddings_request.model is not None and embeddings_request.model.lower().startswith("text-embedding-"):
embeddings_request.model = DEFAULT_EMBEDDING_MODEL
# Exception will be raised if model not supported.
embeddings_request.model = get_model("aws", embeddings_request.model, "embedding-default")
Expand Down
Loading