from datetime import datetime from typing import Annotated, TypeVar from core_domain import ( ChatCompletionRequestContract, ChatCompletionResponseContract, ServiceHealth, ) from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import text from sqlalchemy.orm import Session from app.application.services import ModelGatewayApplicationService from app.bootstrap.settings import ModelGatewayServiceSettings from app.db.session import get_db from app.domain.repositories import ModelDefinitionRepository, ModelProviderDefinitionRepository from app.infrastructure.provider import ModelProviderClient, ModelProviderClientError from app.schemas.model import ( ApiResponse, DeleteData, DiscoverModelsData, DiscoverModelsRequestDto, ModelCreateRequest, ModelCreateRequestDto, ModelDeleteRequestDto, ModelDto, ModelProviderCreateRequestDto, ModelProviderDeleteRequestDto, ModelProviderDto, ModelProviderTestData, ModelProviderTestRequestDto, ModelProviderUpdateRequestDto, ModelResponse, ModelStatusUpdateRequest, ModelTestData, ModelTestRequest, ModelTestRequestDto, ModelTestResponse, ModelUpdateRequest, ModelUpdateRequestDto, PageRequest, PageResult, ) router = APIRouter() DbSession = Annotated[Session, Depends(get_db)] T = TypeVar("T") def get_model_gateway_settings() -> ModelGatewayServiceSettings: return ModelGatewayServiceSettings() def get_model_gateway_application_service( db: DbSession, settings: Annotated[ ModelGatewayServiceSettings, Depends(get_model_gateway_settings), ]) -> ModelGatewayApplicationService: return ModelGatewayApplicationService( model_repository=ModelDefinitionRepository(db), provider_repository=ModelProviderDefinitionRepository(db), provider_client=ModelProviderClient(settings=settings), settings=settings) ModelServiceDep = Annotated[ ModelGatewayApplicationService, Depends(get_model_gateway_application_service), ] def ok(data: T) -> ApiResponse[T]: return ApiResponse[T]( data=data, requestId="", serverTime=datetime.utcnow()) @router.get("/health", response_model=ServiceHealth) def health_check( db: DbSession, settings: Annotated[ ModelGatewayServiceSettings, Depends(get_model_gateway_settings), ]) -> ServiceHealth: db.execute(text("SELECT 1")) provider_status = "configured" if settings.provider_base_url else "missing" return ServiceHealth(service="model-gateway-service", status="ok", database=provider_status) @router.post("", response_model=ModelResponse) def create_model( payload: ModelCreateRequest, service: ModelServiceDep, ) -> ModelResponse: try: entity = service.create_model(payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return ModelResponse.from_entity(entity) @router.get("", response_model=list[ModelResponse]) def list_models( service: ModelServiceDep, ) -> list[ModelResponse]: return [ModelResponse.from_entity(item) for item in service.list_models()] @router.patch("/{model_id}", response_model=ModelResponse) def update_model( model_id: str, payload: ModelUpdateRequest, service: ModelServiceDep, ) -> ModelResponse: try: entity = service.update_model(model_id=model_id, payload=payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc if entity is None: raise HTTPException(status_code=404, detail=f"model not found: {model_id}") return ModelResponse.from_entity(entity) @router.patch("/{model_id}/status", response_model=ModelResponse) def update_model_status( model_id: str, payload: ModelStatusUpdateRequest, service: ModelServiceDep, ) -> ModelResponse: entity = service.update_model_status(model_id=model_id, payload=payload) if entity is None: raise HTTPException(status_code=404, detail=f"model not found: {model_id}") return ModelResponse.from_entity(entity) @router.delete("/{model_id}", status_code=204) def delete_model( model_id: str, service: ModelServiceDep, ) -> None: if not service.delete_model(model_id): raise HTTPException(status_code=404, detail=f"model not found: {model_id}") @router.post("/{model_id}/test", response_model=ModelTestResponse) def test_model( model_id: str, payload: ModelTestRequest, service: ModelServiceDep, ) -> ModelTestResponse: try: result = service.test_model(model_id=model_id, payload=payload) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc if result is None: raise HTTPException(status_code=404, detail=f"model not found: {model_id}") return result @router.post("/chat-completions", response_model=ChatCompletionResponseContract) def create_chat_completion( payload: ChatCompletionRequestContract, service: ModelServiceDep) -> ChatCompletionResponseContract: try: return service.create_chat_completion(payload) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc @router.post("/list", response_model=ApiResponse[PageResult[ModelDto]]) def list_models_contract( payload: PageRequest, service: ModelServiceDep) -> ApiResponse[PageResult[ModelDto]]: keyword = (payload.keyword or "").lower().strip() items = [ item for item in service.list_models() if not keyword or keyword in item.name.lower() or keyword in item.model_name.lower() or keyword in item.provider_type.lower() ] page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ModelDto].from_items( items=[ModelDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/create", response_model=ApiResponse[ModelDto]) def create_model_contract( payload: ModelCreateRequestDto, service: ModelServiceDep) -> ApiResponse[ModelDto]: try: entity = service.create_model_from_contract(payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return ok(ModelDto.from_entity(entity)) @router.post("/update", response_model=ApiResponse[ModelDto]) def update_model_contract( payload: ModelUpdateRequestDto, service: ModelServiceDep) -> ApiResponse[ModelDto]: try: entity = service.update_model_from_contract(payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc if entity is None: raise HTTPException(status_code=404, detail=f"model not found: {payload.modelId}") return ok(ModelDto.from_entity(entity)) @router.post("/delete", response_model=ApiResponse[DeleteData]) def delete_model_contract( payload: ModelDeleteRequestDto, service: ModelServiceDep) -> ApiResponse[DeleteData]: deleted = service.delete_model_from_contract(payload) return ok(DeleteData(deleted=deleted, modelId=payload.modelId)) @router.post("/test", response_model=ApiResponse[ModelTestData]) def test_model_contract( payload: ModelTestRequestDto, service: ModelServiceDep) -> ApiResponse[ModelTestData]: try: result = service.test_model_from_contract(payload) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc if result is None: raise HTTPException(status_code=404, detail=f"model not found: {payload.modelId}") return ok(result) @router.post("/providers/list", response_model=ApiResponse[PageResult[ModelProviderDto]]) def list_model_providers_contract( payload: PageRequest, service: ModelServiceDep) -> ApiResponse[PageResult[ModelProviderDto]]: keyword = (payload.keyword or "").lower().strip() items = [ item for item in service.list_providers() if not keyword or keyword in item.name.lower() or keyword in item.provider_type.lower() or keyword in item.base_url.lower() ] page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ModelProviderDto].from_items( items=[ModelProviderDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/providers/create", response_model=ApiResponse[ModelProviderDto]) def create_model_provider_contract( payload: ModelProviderCreateRequestDto, service: ModelServiceDep) -> ApiResponse[ModelProviderDto]: entity = service.create_provider(payload) return ok(ModelProviderDto.from_entity(entity)) @router.post("/providers/update", response_model=ApiResponse[ModelProviderDto]) def update_model_provider_contract( payload: ModelProviderUpdateRequestDto, service: ModelServiceDep) -> ApiResponse[ModelProviderDto]: entity = service.update_provider(payload) if entity is None: raise HTTPException(status_code=404, detail=f"provider not found: {payload.providerId}") return ok(ModelProviderDto.from_entity(entity)) @router.post("/providers/delete", response_model=ApiResponse[DeleteData]) def delete_model_provider_contract( payload: ModelProviderDeleteRequestDto, service: ModelServiceDep) -> ApiResponse[DeleteData]: deleted = service.delete_provider(payload) return ok(DeleteData(deleted=deleted, providerId=payload.providerId)) @router.post("/providers/test", response_model=ApiResponse[ModelProviderTestData]) def test_model_provider_contract( payload: ModelProviderTestRequestDto, service: ModelServiceDep) -> ApiResponse[ModelProviderTestData]: try: result = service.test_provider(payload) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc if result is None: raise HTTPException(status_code=404, detail=f"provider not found: {payload.providerId}") return ok(result) @router.post("/providers/discover", response_model=ApiResponse[DiscoverModelsData]) def discover_models_contract( payload: DiscoverModelsRequestDto, service: ModelServiceDep) -> ApiResponse[DiscoverModelsData]: try: return ok(service.discover_models(payload)) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc