feat: add openai-compatible model gateway
This commit is contained in:
186
backend/apps/modelhub/api.py
Normal file
186
backend/apps/modelhub/api.py
Normal file
@@ -0,0 +1,186 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user