Files
ai-ticket-platform/backend/apps/modelhub/api.py
2026-07-14 13:44:55 +08:00

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")