686 lines
25 KiB
Python
686 lines
25 KiB
Python
from datetime import date, timedelta
|
|
from math import ceil
|
|
|
|
from django.conf import settings
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone
|
|
from ninja import Router
|
|
from ninja.errors import HttpError
|
|
|
|
from apps.accounts.api import require_login, require_permission
|
|
from apps.audit.models import OperationAuditLog
|
|
from apps.modelhub.crypto import mask_secret
|
|
from apps.modelhub.governance import (
|
|
ModelGovernanceError,
|
|
create_deployment_config,
|
|
create_provider_config,
|
|
deployment_impact,
|
|
provider_impact,
|
|
remove_deployment,
|
|
remove_provider,
|
|
restore_deployment,
|
|
restore_provider,
|
|
update_deployment_config,
|
|
update_provider_config,
|
|
)
|
|
from apps.modelhub.models import ModelDeployment, ModelInvocation, ModelProvider
|
|
from apps.modelhub.schemas import (
|
|
DeletionImpactOut,
|
|
LifecycleResultOut,
|
|
ModelDeploymentIn,
|
|
ModelDeploymentOptionOut,
|
|
ModelDeploymentOut,
|
|
ModelDeploymentPageOut,
|
|
ModelDeploymentUpdateIn,
|
|
ModelInvocationDiagnosticsOut,
|
|
ModelInvocationOut,
|
|
ModelInvocationPageOut,
|
|
ModelProviderIn,
|
|
ModelProviderOut,
|
|
ModelProviderPageOut,
|
|
ModelProviderUpdateIn,
|
|
ModelTestIn,
|
|
)
|
|
from apps.modelhub.services import create_model_test, create_structured_output_test
|
|
from apps.modelhub.structured_output import (
|
|
CHAT_CAPABILITIES,
|
|
StructuredOutputError,
|
|
validate_mode_for_capability,
|
|
)
|
|
from apps.modelhub.tasks import run_model_test
|
|
|
|
router = Router(tags=["modelhub"])
|
|
|
|
|
|
def governance_http_error(error: ModelGovernanceError) -> HttpError:
|
|
return HttpError(error.status, f"{error.code}: {error}")
|
|
|
|
|
|
def structured_http_error(error: StructuredOutputError) -> HttpError:
|
|
return HttpError(400, f"{error.code}: {error}")
|
|
|
|
|
|
def require_superuser(request):
|
|
user = require_login(request)
|
|
if not user.is_superuser:
|
|
raise HttpError(403, "superuser_required: 仅超级管理员可执行生命周期操作")
|
|
return user
|
|
|
|
|
|
def require_any_permission(request, *codes: str):
|
|
user = require_login(request)
|
|
if user.is_superuser or any(user.has_perm(f"system.{code}") for code in codes):
|
|
return user
|
|
raise HttpError(403, "Permission denied")
|
|
|
|
|
|
def _user_label(user) -> str | None:
|
|
if not user:
|
|
return None
|
|
return user.get_full_name() or user.username
|
|
|
|
|
|
def _provider_status(provider: ModelProvider) -> str:
|
|
if provider.archived_at:
|
|
return "archived"
|
|
if not provider.enabled:
|
|
return "disabled"
|
|
if not provider.api_key_encrypted:
|
|
return "credential_missing"
|
|
return "ready"
|
|
|
|
|
|
def _deployment_status(deployment: ModelDeployment) -> str:
|
|
if deployment.archived_at:
|
|
return "archived"
|
|
if deployment.provider.archived_at:
|
|
return "provider_archived"
|
|
if not deployment.enabled:
|
|
return "disabled"
|
|
provider_status = _provider_status(deployment.provider)
|
|
if provider_status != "ready":
|
|
return f"provider_{provider_status}"
|
|
if (
|
|
deployment.capability in CHAT_CAPABILITIES
|
|
and deployment.structured_output_test_status != "passed"
|
|
):
|
|
return "unverified"
|
|
return "ready"
|
|
|
|
|
|
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),
|
|
"image_egress_approved": provider.image_egress_approved,
|
|
"image_egress_approved_by": _user_label(provider.image_egress_approved_by),
|
|
"image_egress_approved_at": provider.image_egress_approved_at,
|
|
"credential_updated_at": provider.credential_updated_at,
|
|
"credential_updated_by": _user_label(provider.credential_updated_by),
|
|
"archived_at": provider.archived_at,
|
|
"archived_by": _user_label(provider.archived_by),
|
|
"deployment_count": (
|
|
provider.deployment_count
|
|
if hasattr(provider, "deployment_count")
|
|
else provider.deployments.count()
|
|
),
|
|
"active_deployment_count": (
|
|
provider.active_deployment_count
|
|
if hasattr(provider, "active_deployment_count")
|
|
else provider.deployments.filter(archived_at__isnull=True).count()
|
|
),
|
|
"effective_status": _provider_status(provider),
|
|
"created_at": provider.created_at,
|
|
"updated_at": provider.updated_at,
|
|
}
|
|
|
|
|
|
def _reference_summary(deployment: ModelDeployment) -> dict[str, int]:
|
|
return {
|
|
"agent_version_count": (
|
|
deployment.agent_version_count
|
|
if hasattr(deployment, "agent_version_count")
|
|
else deployment.agent_versions.count()
|
|
),
|
|
"step_binding_count": (
|
|
deployment.step_binding_count
|
|
if hasattr(deployment, "step_binding_count")
|
|
else deployment.agent_step_bindings.count()
|
|
),
|
|
"run_step_count": (
|
|
deployment.run_step_count
|
|
if hasattr(deployment, "run_step_count")
|
|
else deployment.agent_run_steps.count()
|
|
),
|
|
"invocation_count": (
|
|
deployment.invocation_count
|
|
if hasattr(deployment, "invocation_count")
|
|
else deployment.invocations.count()
|
|
),
|
|
}
|
|
|
|
|
|
def serialize_deployment(deployment: ModelDeployment, metrics: dict | None = None) -> dict:
|
|
if metrics is None:
|
|
from apps.agents.structured_metrics import build_structured_output_metrics
|
|
|
|
metrics = build_structured_output_metrics(deployment_id=deployment.id)
|
|
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,
|
|
"structured_output_mode": deployment.structured_output_mode,
|
|
"structured_output_test_status": deployment.structured_output_test_status,
|
|
"structured_output_verified_at": deployment.structured_output_verified_at,
|
|
"structured_output_test_message": deployment.structured_output_test_message,
|
|
"structured_output_metrics": metrics,
|
|
"enabled": deployment.enabled,
|
|
"effective_status": _deployment_status(deployment),
|
|
"provider_effective_status": _provider_status(deployment.provider),
|
|
"reference_summary": _reference_summary(deployment),
|
|
"archived_at": deployment.archived_at,
|
|
"archived_by": _user_label(deployment.archived_by),
|
|
"archived_via_provider": deployment.archived_via_provider,
|
|
}
|
|
|
|
|
|
def _diagnostics_expires_at(invocation: ModelInvocation):
|
|
return invocation.created_at + timedelta(
|
|
days=settings.MODEL_INVOCATION_DIAGNOSTIC_RETENTION_DAYS
|
|
)
|
|
|
|
|
|
def serialize_invocation(
|
|
invocation: ModelInvocation,
|
|
*,
|
|
include_diagnostics: bool = False,
|
|
) -> dict:
|
|
snapshot = invocation.model_snapshot or {}
|
|
deployment = invocation.deployment
|
|
expires_at = _diagnostics_expires_at(invocation)
|
|
available = (
|
|
invocation.diagnostics_scrubbed_at is None and expires_at > timezone.now()
|
|
)
|
|
summary = {
|
|
key: snapshot.get(key)
|
|
for key in (
|
|
"provider_id",
|
|
"provider_name",
|
|
"deployment_id",
|
|
"model_name",
|
|
"capability",
|
|
"structured_output",
|
|
)
|
|
if key in snapshot
|
|
}
|
|
return {
|
|
"public_id": invocation.public_id,
|
|
"deployment_id": invocation.deployment_id,
|
|
"deployment_display_name": deployment.display_name if deployment else None,
|
|
"provider_name": (
|
|
deployment.provider.name if deployment else snapshot.get("provider_name")
|
|
),
|
|
"invocation_type": invocation.invocation_type,
|
|
"status": invocation.status,
|
|
"model_snapshot": snapshot if include_diagnostics else {},
|
|
"model_snapshot_summary": summary,
|
|
"output_text": invocation.output_text if include_diagnostics and available else "",
|
|
"input_tokens": invocation.input_tokens,
|
|
"output_tokens": invocation.output_tokens,
|
|
"duration_ms": invocation.duration_ms,
|
|
"retry_count": invocation.retry_count,
|
|
"error_code": invocation.error_code,
|
|
"error_message": invocation.error_message[:500],
|
|
"created_at": invocation.created_at,
|
|
"started_at": invocation.started_at,
|
|
"finished_at": invocation.finished_at,
|
|
"diagnostics_scrubbed_at": invocation.diagnostics_scrubbed_at,
|
|
"diagnostics_expires_at": expires_at,
|
|
"diagnostics_available": available,
|
|
}
|
|
|
|
|
|
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",
|
|
)
|
|
|
|
|
|
def _page(queryset, page: int, page_size: int):
|
|
page = max(page, 1)
|
|
page_size = min(max(page_size, 1), 100)
|
|
total = queryset.count()
|
|
start = (page - 1) * page_size
|
|
return list(queryset[start : start + page_size]), {
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"total": total,
|
|
"total_pages": ceil(total / page_size) if total else 0,
|
|
}
|
|
|
|
|
|
@router.get("/providers", response=ModelProviderPageOut)
|
|
def list_providers(
|
|
request,
|
|
q: str = "",
|
|
enabled: str = "all",
|
|
archived: str = "active",
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
):
|
|
require_permission(request, "model.manage")
|
|
queryset = ModelProvider.objects.select_related(
|
|
"image_egress_approved_by", "credential_updated_by", "archived_by"
|
|
).annotate(
|
|
deployment_count=Count("deployments", distinct=True),
|
|
active_deployment_count=Count(
|
|
"deployments",
|
|
filter=Q(deployments__archived_at__isnull=True),
|
|
distinct=True,
|
|
),
|
|
)
|
|
if q:
|
|
queryset = queryset.filter(Q(name__icontains=q) | Q(base_url__icontains=q))
|
|
if enabled in {"true", "false"}:
|
|
queryset = queryset.filter(enabled=enabled == "true")
|
|
if archived == "active":
|
|
queryset = queryset.filter(archived_at__isnull=True)
|
|
elif archived == "archived":
|
|
queryset = queryset.filter(archived_at__isnull=False)
|
|
rows, pagination = _page(queryset, page, page_size)
|
|
return {"items": [serialize_provider(item) for item in rows], **pagination}
|
|
|
|
|
|
@router.post("/providers", response=ModelProviderOut)
|
|
def create_provider(request, payload: ModelProviderIn):
|
|
require_permission(request, "model.manage")
|
|
try:
|
|
provider = create_provider_config(payload.dict(), request.user)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) 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.select_related(
|
|
"image_egress_approved_by", "credential_updated_by", "archived_by"
|
|
).get(id=provider_id)
|
|
try:
|
|
provider, snapshot = update_provider_config(
|
|
provider, payload.dict(exclude_unset=True), request.user
|
|
)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(request.user, "update", "model_provider", str(provider.id), snapshot)
|
|
return serialize_provider(provider)
|
|
|
|
|
|
@router.get("/providers/{provider_id}/deletion-impact", response=DeletionImpactOut)
|
|
def get_provider_deletion_impact(request, provider_id: int):
|
|
require_superuser(request)
|
|
return provider_impact(ModelProvider.objects.get(id=provider_id))
|
|
|
|
|
|
@router.delete("/providers/{provider_id}", response=LifecycleResultOut)
|
|
def delete_provider(request, provider_id: int):
|
|
user = require_superuser(request)
|
|
try:
|
|
result, impact = remove_provider(provider_id, user)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(user, result, "model_provider", str(provider_id), {"impact": impact})
|
|
return {"result": result, "impact": impact}
|
|
|
|
|
|
@router.post("/providers/{provider_id}/restore", response=ModelProviderOut)
|
|
def restore_provider_api(request, provider_id: int):
|
|
user = require_superuser(request)
|
|
try:
|
|
provider = restore_provider(provider_id)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(user, "restore", "model_provider", str(provider_id), {"enabled": False})
|
|
return serialize_provider(provider)
|
|
|
|
|
|
@router.get("/deployments", response=ModelDeploymentPageOut)
|
|
def list_deployments(
|
|
request,
|
|
provider_id: int | None = None,
|
|
capability: str = "",
|
|
effective_status: str = "",
|
|
structured_test_status: str = "",
|
|
archived: str = "active",
|
|
q: str = "",
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
):
|
|
require_permission(request, "model.manage")
|
|
queryset = ModelDeployment.objects.select_related(
|
|
"provider", "archived_by"
|
|
).annotate(
|
|
agent_version_count=Count("agent_versions", distinct=True),
|
|
step_binding_count=Count("agent_step_bindings", distinct=True),
|
|
run_step_count=Count("agent_run_steps", distinct=True),
|
|
invocation_count=Count("invocations", distinct=True),
|
|
)
|
|
if provider_id:
|
|
queryset = queryset.filter(provider_id=provider_id)
|
|
if capability:
|
|
queryset = queryset.filter(capability=capability)
|
|
if structured_test_status:
|
|
queryset = queryset.filter(
|
|
structured_output_test_status=structured_test_status
|
|
)
|
|
if q:
|
|
queryset = queryset.filter(
|
|
Q(display_name__icontains=q)
|
|
| Q(model_name__icontains=q)
|
|
| Q(provider__name__icontains=q)
|
|
)
|
|
if archived == "active":
|
|
queryset = queryset.filter(archived_at__isnull=True)
|
|
elif archived == "archived":
|
|
queryset = queryset.filter(archived_at__isnull=False)
|
|
if effective_status:
|
|
if effective_status == "archived":
|
|
queryset = queryset.filter(archived_at__isnull=False)
|
|
elif effective_status == "provider_archived":
|
|
queryset = queryset.filter(
|
|
archived_at__isnull=True,
|
|
provider__archived_at__isnull=False,
|
|
)
|
|
elif effective_status == "disabled":
|
|
queryset = queryset.filter(archived_at__isnull=True, enabled=False)
|
|
elif effective_status == "provider_disabled":
|
|
queryset = queryset.filter(
|
|
archived_at__isnull=True,
|
|
enabled=True,
|
|
provider__archived_at__isnull=True,
|
|
provider__enabled=False,
|
|
)
|
|
elif effective_status == "unverified":
|
|
queryset = queryset.filter(
|
|
archived_at__isnull=True,
|
|
provider__archived_at__isnull=True,
|
|
enabled=True,
|
|
provider__enabled=True,
|
|
capability__in=CHAT_CAPABILITIES,
|
|
).exclude(structured_output_test_status="passed")
|
|
elif effective_status == "ready":
|
|
queryset = queryset.filter(
|
|
archived_at__isnull=True,
|
|
provider__archived_at__isnull=True,
|
|
enabled=True,
|
|
provider__enabled=True,
|
|
).exclude(provider__api_key_encrypted="").filter(
|
|
Q(capability__in={"embedding", "rerank"})
|
|
| Q(structured_output_test_status="passed")
|
|
)
|
|
rows, pagination = _page(queryset, page, page_size)
|
|
from apps.agents.structured_metrics import build_deployment_structured_output_metrics
|
|
|
|
metrics = build_deployment_structured_output_metrics([item.id for item in rows])
|
|
return {
|
|
"items": [serialize_deployment(item, metrics[item.id]) for item in rows],
|
|
**pagination,
|
|
}
|
|
|
|
|
|
@router.get("/deployment-options", response=list[ModelDeploymentOptionOut])
|
|
def list_deployment_options(request):
|
|
require_any_permission(request, "agent.manage", "model.manage")
|
|
queryset = ModelDeployment.objects.select_related("provider").filter(
|
|
archived_at__isnull=True,
|
|
provider__archived_at__isnull=True,
|
|
enabled=True,
|
|
provider__enabled=True,
|
|
).exclude(provider__api_key_encrypted="")
|
|
items = [item for item in queryset if _deployment_status(item) == "ready"]
|
|
return [
|
|
{
|
|
"id": item.id,
|
|
"provider_id": item.provider_id,
|
|
"provider_name": item.provider.name,
|
|
"display_name": item.display_name,
|
|
"model_name": item.model_name,
|
|
"capability": item.capability,
|
|
"default_temperature": item.default_temperature,
|
|
"max_output_tokens": item.max_output_tokens,
|
|
"structured_output_mode": item.structured_output_mode,
|
|
"structured_output_test_status": item.structured_output_test_status,
|
|
"structured_output_verified_at": item.structured_output_verified_at,
|
|
"structured_output_test_message": item.structured_output_test_message,
|
|
"enabled": item.enabled,
|
|
}
|
|
for item in items
|
|
]
|
|
|
|
|
|
@router.post("/deployments", response=ModelDeploymentOut)
|
|
def create_deployment(request, payload: ModelDeploymentIn):
|
|
require_permission(request, "model.manage")
|
|
try:
|
|
deployment = create_deployment_config(payload.dict())
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
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", "archived_by").get(
|
|
id=deployment_id
|
|
)
|
|
try:
|
|
deployment, snapshot = update_deployment_config(
|
|
deployment, payload.dict(exclude_unset=True)
|
|
)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(request.user, "update", "model_deployment", str(deployment.id), snapshot)
|
|
return serialize_deployment(deployment)
|
|
|
|
|
|
def _ensure_testable(deployment: ModelDeployment):
|
|
if deployment.archived_at or deployment.provider.archived_at:
|
|
raise HttpError(
|
|
400, "model_config_archived: 归档模型不允许编辑、测试或启用"
|
|
)
|
|
|
|
|
|
@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)
|
|
_ensure_testable(deployment)
|
|
if deployment.capability not in CHAT_CAPABILITIES:
|
|
raise HttpError(400, "Embedding 或重排模型请通过对应能力入口验证")
|
|
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 serialize_invocation(invocation, include_diagnostics=True)
|
|
|
|
|
|
@router.post(
|
|
"/deployments/{deployment_id}/structured-output-test",
|
|
response=ModelInvocationOut,
|
|
)
|
|
def test_structured_output(request, deployment_id: int):
|
|
require_permission(request, "model.manage")
|
|
deployment = ModelDeployment.objects.select_related("provider").get(id=deployment_id)
|
|
_ensure_testable(deployment)
|
|
if deployment.capability not in CHAT_CAPABILITIES:
|
|
raise HttpError(400, "该模型能力不适用结构化输出测试")
|
|
try:
|
|
validate_mode_for_capability(
|
|
deployment.capability, deployment.structured_output_mode
|
|
)
|
|
except StructuredOutputError as error:
|
|
raise structured_http_error(error) from error
|
|
deployment.structured_output_test_status = "untested"
|
|
deployment.structured_output_verified_at = None
|
|
deployment.structured_output_test_message = "结构化输出测试执行中"
|
|
deployment.save(
|
|
update_fields=[
|
|
"structured_output_test_status",
|
|
"structured_output_verified_at",
|
|
"structured_output_test_message",
|
|
"updated_at",
|
|
]
|
|
)
|
|
invocation = create_structured_output_test(deployment, request.user)
|
|
run_model_test.delay(invocation.id)
|
|
invocation.refresh_from_db()
|
|
audit(
|
|
request.user,
|
|
"structured_output_test",
|
|
"model_deployment",
|
|
str(deployment.id),
|
|
{
|
|
"invocation_id": str(invocation.public_id),
|
|
"mode": deployment.structured_output_mode,
|
|
},
|
|
)
|
|
return serialize_invocation(invocation, include_diagnostics=True)
|
|
|
|
|
|
@router.get("/deployments/{deployment_id}/deletion-impact", response=DeletionImpactOut)
|
|
def get_deployment_deletion_impact(request, deployment_id: int):
|
|
require_superuser(request)
|
|
return deployment_impact(ModelDeployment.objects.get(id=deployment_id))
|
|
|
|
|
|
@router.delete("/deployments/{deployment_id}", response=LifecycleResultOut)
|
|
def delete_deployment(request, deployment_id: int):
|
|
user = require_superuser(request)
|
|
try:
|
|
result, impact = remove_deployment(deployment_id, user)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(user, result, "model_deployment", str(deployment_id), {"impact": impact})
|
|
return {"result": result, "impact": impact}
|
|
|
|
|
|
@router.post("/deployments/{deployment_id}/restore", response=ModelDeploymentOut)
|
|
def restore_deployment_api(request, deployment_id: int):
|
|
user = require_superuser(request)
|
|
try:
|
|
deployment = restore_deployment(deployment_id)
|
|
except ModelGovernanceError as error:
|
|
raise governance_http_error(error) from error
|
|
audit(user, "restore", "model_deployment", str(deployment_id), {"enabled": False})
|
|
return serialize_deployment(deployment)
|
|
|
|
|
|
@router.get("/invocations", response=ModelInvocationPageOut)
|
|
def list_invocations(
|
|
request,
|
|
deployment_id: int | None = None,
|
|
provider_id: int | None = None,
|
|
invocation_type: str = "",
|
|
status: str = "",
|
|
date_from: date | None = None,
|
|
date_to: date | None = None,
|
|
error_code: str = "",
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
):
|
|
require_permission(request, "model.manage")
|
|
queryset = ModelInvocation.objects.select_related("deployment__provider")
|
|
if deployment_id:
|
|
queryset = queryset.filter(deployment_id=deployment_id)
|
|
if provider_id:
|
|
queryset = queryset.filter(deployment__provider_id=provider_id)
|
|
if invocation_type:
|
|
queryset = queryset.filter(invocation_type=invocation_type)
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__date__gte=date_from)
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__date__lte=date_to)
|
|
if error_code:
|
|
queryset = queryset.filter(error_code=error_code)
|
|
rows, pagination = _page(queryset, page, page_size)
|
|
return {"items": [serialize_invocation(item) for item in rows], **pagination}
|
|
|
|
|
|
@router.get("/invocations/{public_id}", response=ModelInvocationOut)
|
|
def get_invocation(request, public_id: str):
|
|
require_permission(request, "model.manage")
|
|
invocation = ModelInvocation.objects.select_related("deployment__provider").get(
|
|
public_id=public_id
|
|
)
|
|
return serialize_invocation(invocation)
|
|
|
|
|
|
@router.get(
|
|
"/invocations/{public_id}/diagnostics",
|
|
response=ModelInvocationDiagnosticsOut,
|
|
)
|
|
def get_invocation_diagnostics(request, public_id: str):
|
|
require_permission(request, "model.invocation.view")
|
|
invocation = ModelInvocation.objects.get(public_id=public_id)
|
|
expires_at = _diagnostics_expires_at(invocation)
|
|
available = (
|
|
invocation.diagnostics_scrubbed_at is None and expires_at > timezone.now()
|
|
)
|
|
if not available:
|
|
raise HttpError(
|
|
410, "diagnostics_expired: 模型调用诊断内容已超过保留期"
|
|
)
|
|
return {
|
|
"public_id": invocation.public_id,
|
|
"input_snapshot": invocation.input_snapshot,
|
|
"output_text": invocation.output_text,
|
|
"diagnostics_scrubbed_at": invocation.diagnostics_scrubbed_at,
|
|
"diagnostics_expires_at": expires_at,
|
|
"diagnostics_available": True,
|
|
}
|
|
|
|
|
|
@router.get("/model-tests/{public_id}", response=ModelInvocationOut)
|
|
def get_model_test(request, public_id: str):
|
|
require_permission(request, "model.manage")
|
|
invocation = ModelInvocation.objects.select_related("deployment__provider").get(
|
|
public_id=public_id,
|
|
invocation_type__in={"model_test", "structured_output_test"},
|
|
)
|
|
return serialize_invocation(invocation, include_diagnostics=True)
|