from django.db import IntegrityError from ninja import Router from ninja.errors import HttpError from apps.accounts.api import require_permission from apps.audit.models import OperationAuditLog from apps.modelhub.crypto import CredentialEncryptionError, encrypt_secret, mask_secret from apps.modelhub.models import ModelDeployment, ModelInvocation, ModelProvider from apps.modelhub.schemas import ( ModelDeploymentIn, ModelDeploymentOut, ModelDeploymentUpdateIn, ModelInvocationOut, ModelProviderIn, ModelProviderOut, ModelProviderUpdateIn, ModelTestIn, ) from apps.modelhub.services import create_model_test from apps.modelhub.tasks import run_model_test router = Router(tags=["modelhub"]) def serialize_provider(provider: ModelProvider) -> dict: return { "id": provider.id, "name": provider.name, "base_url": provider.base_url, "enabled": provider.enabled, "default_timeout_seconds": provider.default_timeout_seconds, "credential_set": bool(provider.api_key_encrypted), "credential_masked": mask_secret(provider.api_key_encrypted), "created_at": provider.created_at, "updated_at": provider.updated_at, } def serialize_deployment(deployment: ModelDeployment) -> dict: return { "id": deployment.id, "provider_id": deployment.provider_id, "provider_name": deployment.provider.name, "display_name": deployment.display_name, "model_name": deployment.model_name, "capability": deployment.capability, "default_temperature": deployment.default_temperature, "max_output_tokens": deployment.max_output_tokens, "enabled": deployment.enabled, } def audit(user, action: str, target_type: str, target_id: str, snapshot: dict) -> None: OperationAuditLog.objects.create( user=user if getattr(user, "is_authenticated", False) else None, module="modelhub", action=action, target_type=target_type, target_id=target_id, request_snapshot=snapshot, result="success", ) @router.get("/providers", response=list[ModelProviderOut]) def list_providers(request): require_permission(request, "model.manage") return [serialize_provider(provider) for provider in ModelProvider.objects.all()] @router.post("/providers", response=ModelProviderOut) def create_provider(request, payload: ModelProviderIn): require_permission(request, "model.manage") if not 1 <= payload.default_timeout_seconds <= 300: raise HttpError(400, "Timeout must be between 1 and 300 seconds") try: encrypted = encrypt_secret(payload.api_key) provider = ModelProvider.objects.create( name=payload.name, base_url=payload.base_url.rstrip("/"), api_key_encrypted=encrypted, enabled=payload.enabled, default_timeout_seconds=payload.default_timeout_seconds, created_by=request.user, ) except CredentialEncryptionError as error: raise HttpError(400, str(error)) from error except IntegrityError as error: raise HttpError(400, "Provider name already exists") from error audit(request.user, "create", "model_provider", str(provider.id), {"name": provider.name}) return serialize_provider(provider) @router.patch("/providers/{provider_id}", response=ModelProviderOut) def update_provider(request, provider_id: int, payload: ModelProviderUpdateIn): require_permission(request, "model.manage") provider = ModelProvider.objects.get(id=provider_id) values = payload.dict(exclude_unset=True) api_key = values.pop("api_key", None) if api_key: try: provider.api_key_encrypted = encrypt_secret(api_key) except CredentialEncryptionError as error: raise HttpError(400, str(error)) from error if "default_timeout_seconds" in values and not 1 <= values["default_timeout_seconds"] <= 300: raise HttpError(400, "Timeout must be between 1 and 300 seconds") for field, value in values.items(): setattr(provider, field, value.rstrip("/") if field == "base_url" else value) provider.save() audit( request.user, "update", "model_provider", str(provider.id), {"fields": sorted(values), "credential_replaced": bool(api_key)}, ) return serialize_provider(provider) @router.get("/deployments", response=list[ModelDeploymentOut]) def list_deployments(request, provider_id: int | None = None): require_permission(request, "model.manage") queryset = ModelDeployment.objects.select_related("provider") if provider_id: queryset = queryset.filter(provider_id=provider_id) return [serialize_deployment(item) for item in queryset] @router.post("/deployments", response=ModelDeploymentOut) def create_deployment(request, payload: ModelDeploymentIn): require_permission(request, "model.manage") if not 0 <= payload.default_temperature <= 2: raise HttpError(400, "Temperature must be between 0 and 2") if not 1 <= payload.max_output_tokens <= 32768: raise HttpError(400, "Max output tokens are out of range") deployment = ModelDeployment.objects.create(**payload.dict()) deployment = ModelDeployment.objects.select_related("provider").get(id=deployment.id) audit( request.user, "create", "model_deployment", str(deployment.id), {"model_name": deployment.model_name}, ) return serialize_deployment(deployment) @router.patch("/deployments/{deployment_id}", response=ModelDeploymentOut) def update_deployment(request, deployment_id: int, payload: ModelDeploymentUpdateIn): require_permission(request, "model.manage") deployment = ModelDeployment.objects.select_related("provider").get(id=deployment_id) values = payload.dict(exclude_unset=True) for field, value in values.items(): setattr(deployment, field, value) deployment.save() audit( request.user, "update", "model_deployment", str(deployment.id), {"fields": sorted(values)}, ) return serialize_deployment(deployment) @router.post("/deployments/{deployment_id}/test", response=ModelInvocationOut) def test_deployment(request, deployment_id: int, payload: ModelTestIn): require_permission(request, "model.manage") deployment = ModelDeployment.objects.select_related("provider").get(id=deployment_id) invocation = create_model_test(deployment, payload.prompt, request.user) run_model_test.delay(invocation.id) invocation.refresh_from_db() audit( request.user, "test", "model_deployment", str(deployment.id), {"invocation_id": str(invocation.public_id)}, ) return invocation @router.get("/model-tests/{public_id}", response=ModelInvocationOut) def get_model_test(request, public_id: str): require_permission(request, "model.manage") return ModelInvocation.objects.get(public_id=public_id, invocation_type="model_test")