feat: add trusted ticket intake backend
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from django.db import IntegrityError
|
||||
from django.utils import timezone
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
|
||||
@@ -31,6 +32,14 @@ def serialize_provider(provider: ModelProvider) -> dict:
|
||||
"default_timeout_seconds": provider.default_timeout_seconds,
|
||||
"credential_set": bool(provider.api_key_encrypted),
|
||||
"credential_masked": mask_secret(provider.api_key_encrypted),
|
||||
"image_egress_approved": provider.image_egress_approved,
|
||||
"image_egress_approved_by": (
|
||||
provider.image_egress_approved_by.get_full_name()
|
||||
or provider.image_egress_approved_by.username
|
||||
if provider.image_egress_approved_by_id
|
||||
else None
|
||||
),
|
||||
"image_egress_approved_at": provider.image_egress_approved_at,
|
||||
"created_at": provider.created_at,
|
||||
"updated_at": provider.updated_at,
|
||||
}
|
||||
@@ -65,7 +74,10 @@ def audit(user, action: str, target_type: str, target_id: str, snapshot: dict) -
|
||||
@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()]
|
||||
return [
|
||||
serialize_provider(provider)
|
||||
for provider in ModelProvider.objects.select_related("image_egress_approved_by")
|
||||
]
|
||||
|
||||
|
||||
@router.post("/providers", response=ModelProviderOut)
|
||||
@@ -81,6 +93,9 @@ def create_provider(request, payload: ModelProviderIn):
|
||||
api_key_encrypted=encrypted,
|
||||
enabled=payload.enabled,
|
||||
default_timeout_seconds=payload.default_timeout_seconds,
|
||||
image_egress_approved=payload.image_egress_approved,
|
||||
image_egress_approved_by=request.user if payload.image_egress_approved else None,
|
||||
image_egress_approved_at=timezone.now() if payload.image_egress_approved else None,
|
||||
created_by=request.user,
|
||||
)
|
||||
except CredentialEncryptionError as error:
|
||||
@@ -97,6 +112,7 @@ def update_provider(request, provider_id: int, payload: ModelProviderUpdateIn):
|
||||
provider = ModelProvider.objects.get(id=provider_id)
|
||||
values = payload.dict(exclude_unset=True)
|
||||
api_key = values.pop("api_key", None)
|
||||
image_egress_approved = values.pop("image_egress_approved", None)
|
||||
if api_key:
|
||||
try:
|
||||
provider.api_key_encrypted = encrypt_secret(api_key)
|
||||
@@ -106,13 +122,22 @@ def update_provider(request, provider_id: int, payload: ModelProviderUpdateIn):
|
||||
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)
|
||||
if image_egress_approved is not None:
|
||||
provider.image_egress_approved = image_egress_approved
|
||||
provider.image_egress_approved_by = request.user if image_egress_approved else None
|
||||
provider.image_egress_approved_at = timezone.now() if image_egress_approved else None
|
||||
provider.save()
|
||||
audit(
|
||||
request.user,
|
||||
"update",
|
||||
"model_provider",
|
||||
str(provider.id),
|
||||
{"fields": sorted(values), "credential_replaced": bool(api_key)},
|
||||
{
|
||||
"fields": sorted(
|
||||
[*values, *(["image_egress_approved"] if image_egress_approved is not None else [])]
|
||||
),
|
||||
"credential_replaced": bool(api_key),
|
||||
},
|
||||
)
|
||||
return serialize_provider(provider)
|
||||
|
||||
@@ -133,6 +158,8 @@ def create_deployment(request, payload: ModelDeploymentIn):
|
||||
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")
|
||||
if payload.capability not in {"text_chat", "vision_chat"}:
|
||||
raise HttpError(400, "Unknown model capability")
|
||||
deployment = ModelDeployment.objects.create(**payload.dict())
|
||||
deployment = ModelDeployment.objects.select_related("provider").get(id=deployment.id)
|
||||
audit(
|
||||
@@ -150,6 +177,8 @@ def update_deployment(request, deployment_id: int, payload: ModelDeploymentUpdat
|
||||
require_permission(request, "model.manage")
|
||||
deployment = ModelDeployment.objects.select_related("provider").get(id=deployment_id)
|
||||
values = payload.dict(exclude_unset=True)
|
||||
if "capability" in values and values["capability"] not in {"text_chat", "vision_chat"}:
|
||||
raise HttpError(400, "Unknown model capability")
|
||||
for field, value in values.items():
|
||||
setattr(deployment, field, value)
|
||||
deployment.save()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-16 11:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("modelhub", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="modelprovider",
|
||||
name="image_egress_approved",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="modelprovider",
|
||||
name="image_egress_approved_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="modelprovider",
|
||||
name="image_egress_approved_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="approved_image_model_providers",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -10,6 +10,15 @@ class ModelProvider(models.Model):
|
||||
api_key_encrypted = models.TextField(blank=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
default_timeout_seconds = models.PositiveIntegerField(default=60)
|
||||
image_egress_approved = models.BooleanField(default=False)
|
||||
image_egress_approved_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="approved_image_model_providers",
|
||||
)
|
||||
image_egress_approved_at = models.DateTimeField(null=True, blank=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
|
||||
@@ -11,6 +11,7 @@ class ModelProviderIn(Schema):
|
||||
api_key: str
|
||||
enabled: bool = True
|
||||
default_timeout_seconds: int = 60
|
||||
image_egress_approved: bool = False
|
||||
|
||||
|
||||
class ModelProviderUpdateIn(Schema):
|
||||
@@ -19,6 +20,7 @@ class ModelProviderUpdateIn(Schema):
|
||||
api_key: str | None = None
|
||||
enabled: bool | None = None
|
||||
default_timeout_seconds: int | None = None
|
||||
image_egress_approved: bool | None = None
|
||||
|
||||
|
||||
class ModelProviderOut(Schema):
|
||||
@@ -29,6 +31,9 @@ class ModelProviderOut(Schema):
|
||||
default_timeout_seconds: int
|
||||
credential_set: bool
|
||||
credential_masked: str
|
||||
image_egress_approved: bool
|
||||
image_egress_approved_by: str | None
|
||||
image_egress_approved_at: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -37,6 +42,7 @@ class ModelDeploymentIn(Schema):
|
||||
provider_id: int
|
||||
display_name: str
|
||||
model_name: str
|
||||
capability: str = "text_chat"
|
||||
default_temperature: float = 0.2
|
||||
max_output_tokens: int = 2048
|
||||
enabled: bool = True
|
||||
@@ -45,6 +51,7 @@ class ModelDeploymentIn(Schema):
|
||||
class ModelDeploymentUpdateIn(Schema):
|
||||
display_name: str | None = None
|
||||
model_name: str | None = None
|
||||
capability: str | None = None
|
||||
default_temperature: float | None = None
|
||||
max_output_tokens: int | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from time import monotonic, sleep
|
||||
from typing import Any
|
||||
|
||||
from django.utils import timezone
|
||||
from openai import (
|
||||
@@ -33,7 +34,7 @@ class ModelGatewayError(RuntimeError):
|
||||
|
||||
def _request_completion(
|
||||
deployment: ModelDeployment,
|
||||
messages: list[dict[str, str]],
|
||||
messages: list[dict[str, Any]],
|
||||
*,
|
||||
temperature: float,
|
||||
max_output_tokens: int,
|
||||
@@ -79,7 +80,7 @@ def _error_details(error: Exception) -> tuple[str, str, bool]:
|
||||
|
||||
def execute_invocation(
|
||||
invocation: ModelInvocation,
|
||||
messages: list[dict[str, str]],
|
||||
messages: list[dict[str, Any]],
|
||||
*,
|
||||
temperature: float | None = None,
|
||||
max_output_tokens: int | None = None,
|
||||
@@ -101,6 +102,7 @@ def execute_invocation(
|
||||
"base_url": deployment.provider.base_url,
|
||||
"deployment_id": deployment.id,
|
||||
"model_name": deployment.model_name,
|
||||
"capability": deployment.capability,
|
||||
"temperature": temperature if temperature is not None else deployment.default_temperature,
|
||||
"max_output_tokens": max_output_tokens or deployment.max_output_tokens,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user