187 lines
6.9 KiB
Python
187 lines
6.9 KiB
Python
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")
|