import json from datetime import datetime from typing import Annotated, TypeVar from core_domain import ( ChatCompletionRequestContract, ChatCompletionResponseContract, EmbeddingRequestContract, EmbeddingResponseContract, ServiceHealth, ) from core_shared import error_detail from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse 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()) def json_dump(payload: dict[str, object]) -> str: return json.dumps(payload, ensure_ascii=False, default=str) @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=error_detail("error.validation", message=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=error_detail("error.validation", message=str(exc))) from exc if entity is None: raise HTTPException(status_code=404, detail=error_detail("error.model.not_found", id=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=error_detail("error.model.not_found", id=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=error_detail("error.model.not_found", id=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=error_detail("error.downstream.request_failed", service="model-gateway", error=str(exc))) from exc if result is None: raise HTTPException(status_code=404, detail=error_detail("error.model.not_found", id=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=error_detail("error.downstream.request_failed", service="model-gateway", error=str(exc))) from exc @router.post("/chat-completions/stream") def stream_chat_completion( payload: ChatCompletionRequestContract, service: ModelServiceDep) -> StreamingResponse: def events(): try: for delta in service.stream_chat_completion(payload): yield f"event: delta\ndata: {json_dump({'delta': delta})}\n\n" yield "event: done\ndata: {}\n\n" except ModelProviderClientError as exc: yield f"event: error\ndata: {json_dump({'message': str(exc)})}\n\n" return StreamingResponse( events(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no", }) @router.post("/embeddings", response_model=EmbeddingResponseContract) def create_embedding( payload: EmbeddingRequestContract, service: ModelServiceDep) -> EmbeddingResponseContract: try: return service.create_embedding(payload) except ModelProviderClientError as exc: raise HTTPException(status_code=502, detail=error_detail("error.downstream.request_failed", service="model-gateway", error=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=error_detail("error.validation", message=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=error_detail("error.validation", message=str(exc))) from exc if entity is None: raise HTTPException(status_code=404, detail=error_detail("error.model.not_found", id=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=error_detail("error.downstream.request_failed", service="model-gateway", error=str(exc))) from exc if result is None: raise HTTPException(status_code=404, detail=error_detail("error.model.not_found", id=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=error_detail("error.provider.not_found", id=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=error_detail("error.downstream.request_failed", service="model-gateway", error=str(exc))) from exc if result is None: raise HTTPException(status_code=404, detail=error_detail("error.provider.not_found", id=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=error_detail("error.downstream.request_failed", service="model-gateway", error=str(exc))) from exc