Initial public ModelForge release

This commit is contained in:
Jens
2026-09-01 21:30:16 +02:00
commit 7082ab955a
490 changed files with 104252 additions and 0 deletions
@@ -0,0 +1,134 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.acquisition import (
ArtifactJobResponse,
ArtifactSetResponse,
DiscoveryCandidate,
DiscoverySearchRequest,
DownloadPlanCreate,
DownloadPlanResponse,
UpstreamRefreshRequest,
UpstreamSnapshotResponse,
)
from modelforge_api.providers.huggingface import OfficialHuggingFaceProvider
from modelforge_api.services.acquisition import AcquisitionService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1",
tags=["artifact-acquisition"],
dependencies=[Depends(require_viewer)],
)
def get_acquisition_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> AcquisitionService:
token = settings.hf_token.get_secret_value() if settings.hf_token else None
provider = OfficialHuggingFaceProvider(token=token, timeout=settings.hf_timeout_seconds)
return AcquisitionService(session, settings, provider)
Service = Annotated[AcquisitionService, Depends(get_acquisition_service)]
@router.post(
"/discovery/search",
response_model=list[DiscoveryCandidate],
dependencies=[Depends(require_operator)],
)
def search(request: DiscoverySearchRequest, service: Service) -> list[DiscoveryCandidate]:
return service.search(request)
@router.post(
"/models/{model_id}/refresh-upstream",
response_model=UpstreamSnapshotResponse,
dependencies=[Depends(require_operator)],
)
def refresh_upstream(
model_id: uuid.UUID, request: UpstreamRefreshRequest, service: Service
) -> UpstreamSnapshotResponse:
return service.refresh_model(model_id, request.revision)
@router.get("/models/{model_id}/upstream", response_model=UpstreamSnapshotResponse)
def model_upstream(model_id: uuid.UUID, service: Service) -> UpstreamSnapshotResponse:
return service.latest_snapshot(model_id)
@router.get("/revisions/{revision_id}/artifact-sets", response_model=list[ArtifactSetResponse])
def artifact_sets(revision_id: uuid.UUID, service: Service) -> list[ArtifactSetResponse]:
return service.artifact_sets(revision_id)
@router.post(
"/download-plans",
response_model=DownloadPlanResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_download_plan(request: DownloadPlanCreate, service: Service) -> DownloadPlanResponse:
return service.create_plan(request)
@router.get("/download-plans/{plan_id}", response_model=DownloadPlanResponse)
def download_plan(plan_id: uuid.UUID, service: Service) -> DownloadPlanResponse:
return service.plan_response(plan_id)
@router.post(
"/download-plans/{plan_id}/approve",
response_model=DownloadPlanResponse,
dependencies=[Depends(require_admin)],
)
def approve_download_plan(plan_id: uuid.UUID, service: Service) -> DownloadPlanResponse:
return service.approve_plan(plan_id)
@router.post(
"/download-plans/{plan_id}/execute",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def execute_download_plan(plan_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.execute_plan(plan_id)
@router.get("/artifact-jobs", response_model=list[ArtifactJobResponse])
def artifact_jobs(
service: Service, limit: Annotated[int, Query(ge=1, le=100)] = 100
) -> list[ArtifactJobResponse]:
return service.jobs()[:limit]
@router.get("/artifact-jobs/{job_id}", response_model=ArtifactJobResponse)
def artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.job(job_id)
@router.post(
"/artifact-jobs/{job_id}/cancel",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def cancel_artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.cancel(job_id)
@router.post(
"/artifact-jobs/{job_id}/retry",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def retry_artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.retry(job_id)
@@ -0,0 +1,371 @@
from __future__ import annotations
import time
import uuid
from collections import defaultdict, deque
from typing import Annotated
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin
from modelforge_api.db import get_session
from modelforge_api.domain.acquisition import (
AgentArtifactJobLease,
AgentJobComplete,
AgentJobControl,
AgentJobFailure,
AgentJobProgress,
ArtifactJobResponse,
)
from modelforge_api.domain.agent_protocol import (
EnrollmentRequest,
EnrollmentResponse,
EnrollmentTokenCreate,
EnrollmentTokenCreated,
EnrollmentTokenSummary,
HeartbeatRequest,
InventoryReport,
NodeCredentialCreated,
NodeManagementUpdate,
ObservationAck,
TelemetryReport,
)
from modelforge_api.domain.node_decommission import (
NodeDecommissionExecute,
NodeDecommissionPreview,
NodeDecommissionResult,
)
from modelforge_api.domain.runtime import (
AgentRuntimeProbeComplete,
AgentRuntimeProbeControl,
AgentRuntimeProbeFailure,
AgentRuntimeProbeLease,
AgentRuntimeProbeProgress,
RuntimeProbeResponse,
)
from modelforge_api.persistence.models import ComputeNode, NodeCredential
from modelforge_api.providers.huggingface import OfficialHuggingFaceProvider
from modelforge_api.services.acquisition import AcquisitionService
from modelforge_api.services.node_agent import NodeAgentService, NodeAuthenticationEvidence
from modelforge_api.services.node_decommission import NodeDecommissionService
from modelforge_api.services.runtime import RuntimeService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(tags=["node-agent"])
class ActionResponse(BaseModel):
status: str = "ok"
class AttemptLimiter:
def __init__(self, limit: int = 10, window_seconds: int = 60) -> None:
self.limit = limit
self.window_seconds = window_seconds
self.attempts: dict[str, deque[float]] = defaultdict(deque)
def check(self, key: str) -> None:
now = time.monotonic()
bucket = self.attempts[key]
while bucket and bucket[0] <= now - self.window_seconds:
bucket.popleft()
if len(bucket) >= self.limit:
raise HTTPException(status_code=429, detail="enrollment rate limit exceeded")
bucket.append(now)
enrollment_limiter = AttemptLimiter()
def get_agent_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NodeAgentService:
return NodeAgentService(session, settings)
Service = Annotated[NodeAgentService, Depends(get_agent_service)]
def get_decommission_service(
session: Annotated[Session, Depends(get_session)],
) -> NodeDecommissionService:
return NodeDecommissionService(session)
DecommissionService = Annotated[NodeDecommissionService, Depends(get_decommission_service)]
@router.post(
"/api/v1/admin/node-enrollments",
response_model=EnrollmentTokenCreated,
status_code=status.HTTP_201_CREATED,
)
def create_enrollment(
request: EnrollmentTokenCreate, service: Service, _admin: Admin, response: Response
) -> EnrollmentTokenCreated:
response.headers["Cache-Control"] = "no-store"
return service.create_enrollment(request)
@router.get("/api/v1/admin/node-enrollments", response_model=list[EnrollmentTokenSummary])
def list_enrollments(service: Service, _admin: Admin) -> list[EnrollmentTokenSummary]:
return [
EnrollmentTokenSummary(
id=str(item.id),
created_at=item.created_at,
expires_at=item.expires_at,
used_at=item.used_at,
revoked_at=item.revoked_at,
)
for item in service.repository.enrollments()
]
@router.delete("/api/v1/admin/node-enrollments/{enrollment_id}", response_model=ActionResponse)
def revoke_enrollment(enrollment_id: uuid.UUID, service: Service, _admin: Admin) -> ActionResponse:
service.revoke_enrollment(enrollment_id)
return ActionResponse()
@router.patch("/api/v1/admin/hardware/nodes/{node_id}", response_model=ActionResponse)
def update_node(
node_id: uuid.UUID,
request: NodeManagementUpdate,
service: Service,
_admin: Admin,
) -> ActionResponse:
service.update_node(node_id, request)
return ActionResponse()
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/decommission/preview",
response_model=NodeDecommissionPreview,
)
def preview_node_decommission(
node_id: uuid.UUID, service: DecommissionService, _admin: Admin
) -> NodeDecommissionPreview:
return service.preview(node_id)
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/decommission",
response_model=NodeDecommissionResult,
)
def execute_node_decommission(
node_id: uuid.UUID,
request: NodeDecommissionExecute,
service: DecommissionService,
_admin: Admin,
) -> NodeDecommissionResult:
return service.execute(node_id, request)
@router.delete("/api/v1/admin/hardware/nodes/{node_id}/credential", response_model=ActionResponse)
def revoke_credential(node_id: uuid.UUID, service: Service, _admin: Admin) -> ActionResponse:
service.revoke_credential(node_id)
return ActionResponse()
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/credential/rotate",
response_model=NodeCredentialCreated,
)
def rotate_credential(
node_id: uuid.UUID, service: Service, _admin: Admin, response: Response
) -> NodeCredentialCreated:
response.headers["Cache-Control"] = "no-store"
return service.rotate_credential(node_id)
@router.post(
"/api/v1/agent/enroll",
response_model=EnrollmentResponse,
status_code=status.HTTP_201_CREATED,
)
def enroll(
request: EnrollmentRequest, http_request: Request, service: Service, response: Response
) -> EnrollmentResponse:
enrollment_limiter.check(http_request.client.host if http_request.client else "unknown")
response.headers["Cache-Control"] = "no-store"
return service.enroll(request)
def authenticated_node(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> tuple[NodeCredential, ComputeNode]:
evidence = getattr(request.state, "node_authentication", None)
if isinstance(evidence, NodeAuthenticationEvidence):
return service.reuse_authentication(evidence, authorization)
return service.authenticate(authorization)
NodeIdentity = tuple[NodeCredential, ComputeNode]
AgentIdentity = Annotated[NodeIdentity, Depends(authenticated_node)]
def get_agent_acquisition_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> AcquisitionService:
token = settings.hf_token.get_secret_value() if settings.hf_token else None
return AcquisitionService(
session,
settings,
OfficialHuggingFaceProvider(token=token, timeout=settings.hf_timeout_seconds),
actor_type="node_agent",
actor_id="authenticated-node",
)
AgentAcquisition = Annotated[AcquisitionService, Depends(get_agent_acquisition_service)]
def get_agent_runtime_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RuntimeService:
return RuntimeService(
session,
settings,
actor_type="runtime_worker",
actor_id="authenticated-node",
)
AgentRuntime = Annotated[RuntimeService, Depends(get_agent_runtime_service)]
@router.post("/api/v1/agent/heartbeat", response_model=ObservationAck)
def heartbeat(
request: HeartbeatRequest, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.heartbeat(node, request)
@router.put("/api/v1/agent/inventory", response_model=ObservationAck)
def publish_inventory(
request: InventoryReport, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.publish_inventory(node, request)
@router.put("/api/v1/agent/telemetry", response_model=ObservationAck)
def publish_telemetry(
request: TelemetryReport, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.publish_telemetry(node, request)
@router.get(
"/api/v1/agent/artifact-jobs/next",
response_model=AgentArtifactJobLease | None,
)
def claim_artifact_job(
acquisition: AgentAcquisition, identity: AgentIdentity
) -> AgentArtifactJobLease | None:
_credential, node = identity
return acquisition.claim_next(node)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/progress",
response_model=AgentJobControl,
)
def artifact_job_progress(
job_id: uuid.UUID,
request: AgentJobProgress,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> AgentJobControl:
_credential, node = identity
return acquisition.progress(job_id, node, request)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/complete",
response_model=ArtifactJobResponse,
)
def artifact_job_complete(
job_id: uuid.UUID,
request: AgentJobComplete,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> ArtifactJobResponse:
_credential, node = identity
return acquisition.complete(job_id, node, request)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/fail",
response_model=ArtifactJobResponse,
)
def artifact_job_fail(
job_id: uuid.UUID,
request: AgentJobFailure,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> ArtifactJobResponse:
_credential, node = identity
return acquisition.fail(job_id, node, request)
@router.get(
"/api/v1/agent/runtime-probes/next",
response_model=AgentRuntimeProbeLease | None,
)
def claim_runtime_probe(
runtime: AgentRuntime, identity: AgentIdentity
) -> AgentRuntimeProbeLease | None:
_credential, node = identity
return runtime.claim_next(node)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/progress",
response_model=AgentRuntimeProbeControl,
)
def runtime_probe_progress(
probe_id: uuid.UUID,
request: AgentRuntimeProbeProgress,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> AgentRuntimeProbeControl:
_credential, node = identity
return runtime.progress(probe_id, node, request)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/complete",
response_model=RuntimeProbeResponse,
)
def runtime_probe_complete(
probe_id: uuid.UUID,
request: AgentRuntimeProbeComplete,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> RuntimeProbeResponse:
_credential, node = identity
return runtime.complete(probe_id, node, request)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/fail",
response_model=RuntimeProbeResponse,
)
def runtime_probe_fail(
probe_id: uuid.UUID,
request: AgentRuntimeProbeFailure,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> RuntimeProbeResponse:
_credential, node = identity
return runtime.fail(probe_id, node, request)
@@ -0,0 +1,273 @@
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.schemas import (
CapabilityContractResponse,
CapabilityEstateResponse,
InstallationDependencyResponse,
ModelInstallationRationaleResponse,
ProjectBindingResponse,
ProjectResponse,
)
from modelforge_api.persistence.models import (
ArtifactSet,
Capability,
CapabilityContract,
CapabilityDeployment,
CapabilityEvaluationRun,
CapabilityResourceEnvelope,
Model,
ModelRevision,
Project,
ProjectBinding,
ProjectFitEvidence,
ResidencyAllocation,
RuntimeProfile,
ServiceClient,
)
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
router = APIRouter(
prefix="/api/v1", tags=["registry"], dependencies=[Depends(require_viewer)]
)
@router.get("/capabilities", response_model=list[CapabilityContractResponse])
def list_capabilities(
registry: ManifestRegistry = Depends(get_manifest_registry),
session: Session = Depends(get_session),
) -> list[CapabilityContractResponse]:
responses = []
for contract in registry.capabilities():
stable = session.execute(
select(CapabilityDeployment)
.join(
CapabilityContract,
CapabilityContract.id == CapabilityDeployment.capability_contract_id,
)
.join(Capability, Capability.id == CapabilityContract.capability_id)
.where(
Capability.key == contract.capability,
CapabilityContract.version == contract.version,
CapabilityDeployment.status == "stable",
CapabilityDeployment.production.is_(True),
)
).scalar_one_or_none()
responses.append(
CapabilityContractResponse(
key=contract.capability,
version=contract.version,
description=contract.description,
contract=contract,
stable_deployment=(
{
"id": str(stable.id),
"status": stable.status,
"health": stable.health_status,
"production": stable.production,
}
if stable
else None
),
)
)
return responses
@router.get("/capability-estate", response_model=list[CapabilityEstateResponse])
def capability_estate(
registry: ManifestRegistry = Depends(get_manifest_registry),
session: Session = Depends(get_session),
) -> list[CapabilityEstateResponse]:
responses: list[CapabilityEstateResponse] = []
for manifest in registry.capabilities():
contract = session.scalar(
select(CapabilityContract)
.join(Capability, Capability.id == CapabilityContract.capability_id)
.where(Capability.key == manifest.capability, CapabilityContract.version == manifest.version)
)
deployment = None
model = None
revision = None
profile = None
envelope = None
latest_evaluation = None
if contract:
deployment = session.scalar(
select(CapabilityDeployment)
.where(CapabilityDeployment.capability_contract_id == contract.id)
.order_by(
CapabilityDeployment.production.desc(),
CapabilityDeployment.created_at.desc(),
)
.limit(1)
)
if deployment:
artifact_set = session.get(ArtifactSet, deployment.artifact_set_id)
revision = session.get(ModelRevision, artifact_set.revision_id) if artifact_set else None
model = session.get(Model, revision.model_id) if revision else None
profile = session.get(RuntimeProfile, deployment.runtime_profile_id)
envelope = session.scalar(
select(CapabilityResourceEnvelope).where(
CapabilityResourceEnvelope.capability_deployment_id == deployment.id
)
)
latest_evaluation = session.scalar(
select(CapabilityEvaluationRun)
.where(CapabilityEvaluationRun.capability_deployment_id == deployment.id)
.order_by(CapabilityEvaluationRun.created_at.desc())
.limit(1)
)
operational_state = (
deployment.status
if deployment
else ("blocked" if manifest.estate.stability == "blocked" else "not_deployed")
)
responses.append(
CapabilityEstateResponse(
capability=manifest.capability,
version=manifest.version,
category=manifest.estate.category,
purpose=manifest.estate.purpose,
declared_stability=manifest.estate.stability,
operational_state=operational_state,
current_deployment_id=deployment.id if deployment else None,
model=model.display_name if model else None,
revision=revision.resolved_commit_sha if revision else None,
runtime=profile.runtime_type if profile else None,
node=str(deployment.compute_node_id) if deployment else None,
resource_class=manifest.estate.resource_class,
measured_required_vram_bytes=envelope.required_vram_bytes if envelope else None,
consumers=manifest.estate.consumers,
privacy_class=manifest.privacy.classification,
evaluation_type=manifest.estate.evaluation_type,
evaluation_state=latest_evaluation.status if latest_evaluation else "not_evaluated",
)
)
return responses
@router.get(
"/models/installation-rationale",
response_model=list[ModelInstallationRationaleResponse],
)
def model_installation_rationale(
session: Session = Depends(get_session),
) -> list[ModelInstallationRationaleResponse]:
responses: list[ModelInstallationRationaleResponse] = []
for model in session.scalars(select(Model).order_by(Model.display_name)).all():
revisions = session.scalars(
select(ModelRevision).where(ModelRevision.model_id == model.id)
).all()
revision_ids = [item.id for item in revisions]
sets = (
session.scalars(select(ArtifactSet).where(ArtifactSet.revision_id.in_(revision_ids))).all()
if revision_ids
else []
)
installed_sets = [item for item in sets if item.availability == "local"]
dependencies: list[InstallationDependencyResponse] = []
for artifact_set in installed_sets:
deployments = session.scalars(
select(CapabilityDeployment).where(
CapabilityDeployment.artifact_set_id == artifact_set.id
)
).all()
for deployment in deployments:
contract = session.get(CapabilityContract, deployment.capability_contract_id)
capability = session.get(Capability, contract.capability_id) if contract else None
if not contract or not capability:
continue
projects = session.scalars(
select(Project)
.join(ProjectBinding, ProjectBinding.project_id == Project.id)
.where(
ProjectBinding.capability_contract_id == contract.id,
ProjectBinding.deprecated_at.is_(None),
)
.distinct()
).all()
active_projects = session.scalars(
select(Project)
.join(ProjectBinding, ProjectBinding.project_id == Project.id)
.join(ServiceClient, ServiceClient.project_binding_id == ProjectBinding.id)
.where(
ProjectBinding.capability_contract_id == contract.id,
ServiceClient.status == "active",
)
.distinct()
).all()
project_fit_ids = session.scalars(
select(ProjectFitEvidence.id)
.join(
ProjectBinding,
ProjectBinding.id == ProjectFitEvidence.project_binding_id,
)
.where(ProjectBinding.capability_contract_id == contract.id)
).all()
evaluations = session.scalars(
select(CapabilityEvaluationRun).where(
CapabilityEvaluationRun.capability_deployment_id == deployment.id
)
).all()
residency = session.scalar(
select(ResidencyAllocation).where(
ResidencyAllocation.capability_deployment_id == deployment.id
)
)
dependencies.append(
InstallationDependencyResponse(
capability=capability.key,
version=contract.version,
deployment_id=deployment.id,
channel=deployment.channel,
production=deployment.production,
project_consumers=[item.key for item in projects],
active_project_consumers=[item.key for item in active_projects],
project_fit_evidence_ids=list(project_fit_ids),
evaluation_run_ids=[item.id for item in evaluations],
last_used_at=residency.last_used_at if residency else None,
)
)
blockers = []
if dependencies:
blockers.append("capability_deployment_dependency")
if any(item.production for item in dependencies):
blockers.append("production_dependency")
responses.append(
ModelInstallationRationaleResponse(
model_id=model.id,
display_name=model.display_name,
upstream_source=model.upstream_source,
installed=bool(installed_sets),
installed_bytes=sum(item.total_size_bytes for item in installed_sets),
dependencies=dependencies,
can_delete=bool(installed_sets) and not blockers,
deletion_blockers=blockers,
)
)
return responses
@router.get("/projects", response_model=list[ProjectResponse])
def list_projects(
registry: ManifestRegistry = Depends(get_manifest_registry),
) -> list[ProjectResponse]:
return [
ProjectResponse(
**project.project.model_dump(),
notes=project.notes,
bindings=[
ProjectBindingResponse(
capability=capability,
contract_version=binding.contract_version,
binding=binding,
)
for capability, binding in project.bindings.items()
],
)
for project in registry.projects()
]
@@ -0,0 +1,387 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.capability_evaluation import (
CapabilityAdvisorResponse,
CapabilityEvaluationRunCreate,
CapabilityEvaluationRunResponse,
CapabilityEvaluationSuiteCreate,
CapabilityEvaluationSuiteResponse,
)
from modelforge_api.domain.evaluation import (
AdvisorPolicyResponse,
AdvisorPolicyUpdate,
AdvisorRecommendationCreate,
AdvisorRecommendationDismiss,
AdvisorRecommendationResponse,
DiscoveryCandidateAssessmentCreate,
DiscoveryCandidateAssessmentResponse,
EmbeddingMigrationCreate,
EmbeddingMigrationResponse,
EvaluationCaseDefinitionResponse,
EvaluationCaseResultResponse,
EvaluationComparisonCreate,
EvaluationComparisonResponse,
EvaluationRunComplete,
EvaluationRunCreate,
EvaluationRunResponse,
EvaluationSuiteCreate,
EvaluationSuiteResponse,
MigrationUpdate,
ModelComparisonCreate,
ModelComparisonResponse,
RerankingCaseResultResponse,
RerankingRunComplete,
RerankingRunCreate,
RerankingRunResponse,
RetrievalCandidatePoolCreate,
RetrievalCandidatePoolResponse,
RetrievalPipelineIdentityCreate,
RetrievalPipelineIdentityResponse,
)
from modelforge_api.services.capability_evaluation import CapabilityEvaluationService
from modelforge_api.services.evaluation import EvaluationService
router = APIRouter(
prefix="/api/v1",
tags=["project-evaluation"],
dependencies=[Depends(require_viewer)],
)
def service(session: Annotated[Session, Depends(get_session)]) -> EvaluationService:
return EvaluationService(session)
Service = Annotated[EvaluationService, Depends(service)]
def capability_service(
session: Annotated[Session, Depends(get_session)],
) -> CapabilityEvaluationService:
return CapabilityEvaluationService(session)
CapabilityService = Annotated[CapabilityEvaluationService, Depends(capability_service)]
@router.get("/capability-advisor", response_model=list[CapabilityAdvisorResponse])
def capability_advisor(svc: CapabilityService) -> list[CapabilityAdvisorResponse]:
return svc.advisor()
@router.get(
"/capability-evaluation-suites",
response_model=list[CapabilityEvaluationSuiteResponse],
)
def capability_suites(svc: CapabilityService) -> list[CapabilityEvaluationSuiteResponse]:
return svc.suites()
@router.post(
"/capability-evaluation-suites",
response_model=CapabilityEvaluationSuiteResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_suite(
request: CapabilityEvaluationSuiteCreate, svc: CapabilityService, _admin: Admin
) -> CapabilityEvaluationSuiteResponse:
return svc.create_suite(request)
@router.get(
"/capability-evaluation-runs",
response_model=list[CapabilityEvaluationRunResponse],
)
def capability_runs(svc: CapabilityService) -> list[CapabilityEvaluationRunResponse]:
return svc.runs()
@router.post(
"/capability-evaluation-runs",
response_model=CapabilityEvaluationRunResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_run(
request: CapabilityEvaluationRunCreate, svc: CapabilityService, _admin: Admin
) -> CapabilityEvaluationRunResponse:
return svc.create_run(request)
@router.get("/evaluation-suites", response_model=list[EvaluationSuiteResponse])
def suites(svc: Service) -> list[EvaluationSuiteResponse]:
return svc.suites()
@router.post(
"/evaluation-suites",
response_model=EvaluationSuiteResponse,
status_code=status.HTTP_201_CREATED,
)
def create_suite(
request: EvaluationSuiteCreate, svc: Service, _admin: Admin
) -> EvaluationSuiteResponse:
return svc.create_suite(request)
@router.get(
"/evaluation-suites/{suite_id}/revisions/{revision_id}/cases",
response_model=list[EvaluationCaseDefinitionResponse],
)
def suite_cases(
suite_id: uuid.UUID, revision_id: uuid.UUID, svc: Service
) -> list[EvaluationCaseDefinitionResponse]:
return svc.suite_cases(suite_id, revision_id)
@router.get("/evaluation-runs", response_model=list[EvaluationRunResponse])
def runs(svc: Service) -> list[EvaluationRunResponse]:
return svc.runs()
@router.post(
"/evaluation-runs", response_model=EvaluationRunResponse, status_code=status.HTTP_201_CREATED
)
def create_run(request: EvaluationRunCreate, svc: Service, _admin: Admin) -> EvaluationRunResponse:
return svc.create_run(request)
@router.get("/evaluation-runs/{run_id}", response_model=EvaluationRunResponse)
def run(run_id: uuid.UUID, svc: Service) -> EvaluationRunResponse:
return svc.run(run_id)
@router.post("/evaluation-runs/{run_id}/complete", response_model=EvaluationRunResponse)
def complete_run(
run_id: uuid.UUID, request: EvaluationRunComplete, svc: Service, _admin: Admin
) -> EvaluationRunResponse:
return svc.complete_run(run_id, request)
@router.get("/evaluation-runs/{run_id}/cases", response_model=list[EvaluationCaseResultResponse])
def case_results(run_id: uuid.UUID, svc: Service) -> list[EvaluationCaseResultResponse]:
return svc.case_results(run_id)
@router.post(
"/retrieval-candidate-pools",
response_model=RetrievalCandidatePoolResponse,
status_code=status.HTTP_201_CREATED,
)
def create_candidate_pool(
request: RetrievalCandidatePoolCreate, svc: Service, _admin: Admin
) -> RetrievalCandidatePoolResponse:
return svc.create_candidate_pool(request)
@router.get("/retrieval-candidate-pools", response_model=list[RetrievalCandidatePoolResponse])
def candidate_pools(svc: Service) -> list[RetrievalCandidatePoolResponse]:
return svc.candidate_pools()
@router.post(
"/retrieval-pipeline-identities",
response_model=RetrievalPipelineIdentityResponse,
status_code=status.HTTP_201_CREATED,
)
def create_pipeline_identity(
request: RetrievalPipelineIdentityCreate, svc: Service, _admin: Admin
) -> RetrievalPipelineIdentityResponse:
return svc.create_pipeline_identity(request)
@router.get(
"/retrieval-pipeline-identities", response_model=list[RetrievalPipelineIdentityResponse]
)
def pipeline_identities(svc: Service) -> list[RetrievalPipelineIdentityResponse]:
return svc.pipeline_identities()
@router.post(
"/reranking-runs",
response_model=RerankingRunResponse,
status_code=status.HTTP_201_CREATED,
)
def create_reranking_run(
request: RerankingRunCreate, svc: Service, _admin: Admin
) -> RerankingRunResponse:
return svc.create_reranking_run(request)
@router.get("/reranking-runs", response_model=list[RerankingRunResponse])
def reranking_runs(svc: Service) -> list[RerankingRunResponse]:
return svc.reranking_runs()
@router.post("/reranking-runs/{run_id}/complete", response_model=RerankingRunResponse)
def complete_reranking_run(
run_id: uuid.UUID, request: RerankingRunComplete, svc: Service, _admin: Admin
) -> RerankingRunResponse:
return svc.complete_reranking_run(run_id, request)
@router.get("/reranking-runs/{run_id}/cases", response_model=list[RerankingCaseResultResponse])
def reranking_case_results(run_id: uuid.UUID, svc: Service) -> list[RerankingCaseResultResponse]:
return svc.reranking_case_results(run_id)
@router.post(
"/discovery-assessments",
response_model=DiscoveryCandidateAssessmentResponse,
status_code=status.HTTP_201_CREATED,
)
def create_discovery_assessment(
request: DiscoveryCandidateAssessmentCreate, svc: Service, _admin: Admin
) -> DiscoveryCandidateAssessmentResponse:
return svc.create_discovery_assessment(request)
@router.get("/discovery-assessments", response_model=list[DiscoveryCandidateAssessmentResponse])
def discovery_assessments(svc: Service) -> list[DiscoveryCandidateAssessmentResponse]:
return svc.discovery_assessments()
@router.post(
"/evaluation-comparisons",
response_model=EvaluationComparisonResponse,
status_code=status.HTTP_201_CREATED,
)
def compare(
request: EvaluationComparisonCreate, svc: Service, _admin: Admin
) -> EvaluationComparisonResponse:
return svc.compare(request)
@router.get("/evaluation-comparisons", response_model=list[EvaluationComparisonResponse])
def comparisons(svc: Service) -> list[EvaluationComparisonResponse]:
return svc.comparisons()
@router.get("/evaluation-comparisons/{comparison_id}", response_model=EvaluationComparisonResponse)
def comparison(comparison_id: uuid.UUID, svc: Service) -> EvaluationComparisonResponse:
return svc.comparison(comparison_id)
@router.post(
"/model-comparisons",
response_model=ModelComparisonResponse,
status_code=status.HTTP_201_CREATED,
)
def create_model_comparison(
request: ModelComparisonCreate, svc: Service, _admin: Admin
) -> ModelComparisonResponse:
return svc.create_model_comparison(request)
@router.get("/model-comparisons", response_model=list[ModelComparisonResponse])
def model_comparisons(svc: Service) -> list[ModelComparisonResponse]:
return svc.model_comparisons()
@router.get("/model-comparisons/{comparison_id}", response_model=ModelComparisonResponse)
def model_comparison(comparison_id: uuid.UUID, svc: Service) -> ModelComparisonResponse:
return svc.model_comparison(comparison_id)
@router.post(
"/model-comparisons/{comparison_id}/recommendations",
response_model=AdvisorRecommendationResponse,
status_code=status.HTTP_201_CREATED,
)
def create_recommendation(
comparison_id: uuid.UUID,
request: AdvisorRecommendationCreate,
svc: Service,
_admin: Admin,
) -> AdvisorRecommendationResponse:
return svc.recommend(comparison_id, request)
@router.get("/recommendations", response_model=list[AdvisorRecommendationResponse])
def recommendations(svc: Service) -> list[AdvisorRecommendationResponse]:
return svc.recommendations()
@router.get("/recommendations/{recommendation_id}", response_model=AdvisorRecommendationResponse)
def recommendation(recommendation_id: uuid.UUID, svc: Service) -> AdvisorRecommendationResponse:
return svc.recommendation(recommendation_id)
@router.post(
"/recommendations/{recommendation_id}/dismiss",
response_model=AdvisorRecommendationResponse,
)
def dismiss_recommendation(
recommendation_id: uuid.UUID,
request: AdvisorRecommendationDismiss,
svc: Service,
_admin: Admin,
) -> AdvisorRecommendationResponse:
return svc.dismiss_recommendation(recommendation_id, request)
@router.get("/admin/advisor-policies/current", response_model=AdvisorPolicyResponse)
def advisor_policy(svc: Service, _admin: Admin) -> AdvisorPolicyResponse:
return svc.advisor_policy()
@router.patch("/admin/advisor-policies/current", response_model=AdvisorPolicyResponse)
def update_advisor_policy(
request: AdvisorPolicyUpdate, svc: Service, _admin: Admin
) -> AdvisorPolicyResponse:
return svc.update_advisor_policy(request)
@router.get("/embedding-migrations", response_model=list[EmbeddingMigrationResponse])
def migrations(svc: Service) -> list[EmbeddingMigrationResponse]:
return svc.migrations()
@router.post(
"/projects/{project_id}/embedding-migrations",
response_model=EmbeddingMigrationResponse,
status_code=status.HTTP_201_CREATED,
)
def create_migration(
project_id: uuid.UUID, request: EmbeddingMigrationCreate, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.create_migration(project_id, request)
@router.get("/embedding-migrations/{migration_id}", response_model=EmbeddingMigrationResponse)
def migration(migration_id: uuid.UUID, svc: Service) -> EmbeddingMigrationResponse:
return svc.migration(migration_id)
@router.post(
"/embedding-migrations/{migration_id}/state", response_model=EmbeddingMigrationResponse
)
def update_migration(
migration_id: uuid.UUID, request: MigrationUpdate, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.update_migration(migration_id, request)
@router.post(
"/embedding-migrations/{migration_id}/start", response_model=EmbeddingMigrationResponse
)
def start_migration(
migration_id: uuid.UUID, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.start_migration(migration_id)
@router.post(
"/embedding-migrations/{migration_id}/cancel", response_model=EmbeddingMigrationResponse
)
def cancel_migration(
migration_id: uuid.UUID, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.cancel_migration(migration_id)
@@ -0,0 +1,82 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.hardware import AcceleratorState, HardwareState, NodeState
from modelforge_api.services.hardware_factory import build_hardware_service
from modelforge_api.services.hardware_inventory import HardwareInventoryService, HardwareRefreshBusy
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1/hardware", tags=["hardware"], dependencies=[Depends(require_viewer)]
)
def get_hardware_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> HardwareInventoryService:
return build_hardware_service(session, settings)
Service = Annotated[HardwareInventoryService, Depends(get_hardware_service)]
@router.get("", response_model=HardwareState)
def hardware_overview(service: Service) -> HardwareState:
return service.state()
@router.get("/nodes", response_model=list[NodeState])
def list_nodes(service: Service) -> list[NodeState]:
return service.state().nodes
@router.get("/nodes/{node_id}", response_model=NodeState)
def get_node(node_id: uuid.UUID, service: Service) -> NodeState:
node = next((item for item in service.state().nodes if item.id == str(node_id)), None)
if node is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="compute node not found")
return node
@router.get("/accelerators", response_model=list[AcceleratorState])
def list_accelerators(service: Service) -> list[AcceleratorState]:
return [accelerator for node in service.state().nodes for accelerator in node.accelerators]
@router.get("/accelerators/{accelerator_id}", response_model=AcceleratorState)
def get_accelerator(accelerator_id: uuid.UUID, service: Service) -> AcceleratorState:
accelerator = next(
(
item
for node in service.state().nodes
for item in node.accelerators
if item.id == str(accelerator_id)
),
None,
)
if accelerator is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="accelerator not found")
return accelerator
@router.post(
"/refresh",
response_model=HardwareState,
dependencies=[Depends(require_operator)],
)
def refresh_hardware(service: Service) -> HardwareState:
try:
return service.refresh()
except HardwareRefreshBusy as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"hardware inventory failed: {type(exc).__name__}",
) from exc
@@ -0,0 +1,109 @@
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from modelforge_api import __version__
from modelforge_api.api.authorization import require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.enums import HealthStatus
from modelforge_api.domain.release import (
CURRENT_AGENT_PROTOCOL_VERSION,
MINIMUM_POSTGRES_MAJOR,
MINIMUM_UPGRADE_SOURCE,
PRODUCT_NAME,
RELEASE_CHANNEL,
SUPPORTED_AGENT_PROTOCOL_VERSIONS,
SUPPORTED_SCHEMA_REVISIONS,
TARGET_SCHEMA_REVISION,
build_identity,
)
from modelforge_api.domain.schemas import (
HealthResponse,
ReadinessResponse,
ReleaseCompatibility,
ReleaseInfo,
SystemMetadata,
)
from modelforge_api.persistence.models import CapabilityDeployment
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
from modelforge_api.settings import Settings, get_settings
router = APIRouter(prefix="/api/v1", tags=["system"])
@router.get("/health/live", response_model=HealthResponse)
def liveness() -> HealthResponse:
return HealthResponse(version=__version__)
@router.get("/health/ready", response_model=ReadinessResponse)
def readiness(registry: ManifestRegistry = Depends(get_manifest_registry)) -> ReadinessResponse:
registry.capabilities()
registry.projects()
registry.candidates()
registry.benchmarks()
registry.policies()
return ReadinessResponse(
status=HealthStatus.HEALTHY,
checks={"manifests": HealthStatus.HEALTHY},
version=__version__,
)
@router.get(
"/system",
response_model=SystemMetadata,
dependencies=[Depends(require_viewer)],
)
def system_metadata(
settings: Settings = Depends(get_settings),
session: Session = Depends(get_session),
) -> SystemMetadata:
production_count = int(
session.scalar(
select(func.count(CapabilityDeployment.id)).where(
CapabilityDeployment.status == "stable",
CapabilityDeployment.production.is_(True),
)
)
or 0
)
return SystemMetadata(
name=PRODUCT_NAME,
version=__version__,
environment=settings.env,
release_channel=RELEASE_CHANNEL,
production_inference_available=production_count > 0,
)
@router.get("/version", response_model=ReleaseInfo)
def release_info(settings: Settings = Depends(get_settings)) -> ReleaseInfo:
"""Build identity and compatibility for the running process.
Unauthenticated on purpose: an operator diagnosing a deployment needs to know which build is
answering before they have credentials for it, and everything here is already implied by the
image they are running. Nothing configuration-derived or sensitive is exposed.
"""
identity = build_identity(
source_commit=settings.build_commit,
built_at=settings.build_timestamp,
image_digest=settings.build_image_digest,
)
return ReleaseInfo(
name=PRODUCT_NAME,
version=identity.version,
release_channel=identity.channel,
source_commit=identity.source_commit,
built_at=identity.built_at,
image_digest=identity.image_digest,
compatibility=ReleaseCompatibility(
schema_revision=TARGET_SCHEMA_REVISION,
supported_schema_revisions=list(SUPPORTED_SCHEMA_REVISIONS),
agent_protocol_version=CURRENT_AGENT_PROTOCOL_VERSION,
supported_agent_protocol_versions=list(SUPPORTED_AGENT_PROTOCOL_VERSIONS),
minimum_upgrade_source=MINIMUM_UPGRADE_SOURCE,
minimum_postgres_major=MINIMUM_POSTGRES_MAJOR,
),
)
@@ -0,0 +1,248 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.lifecycle_contracts import (
ApprovalDecision,
ApprovalPolicyCreate,
ApprovalPolicyResponse,
ApprovalRequestCreate,
ApprovalRequestResponse,
CanaryObservation,
CleanupExecutionCreate,
CleanupPlanCreate,
CleanupPlanResponse,
LifecycleEventResponse,
LifecycleOperationResponse,
LifecycleSubjectCreate,
LifecycleSubjectResponse,
PlanExecutionCreate,
PromotionPlanCreate,
PromotionPlanResponse,
RetentionPolicyCreate,
RetentionPolicyResponse,
)
from modelforge_api.services.lifecycle import LifecycleService
router = APIRouter(
prefix="/api/v1/admin/lifecycle",
tags=["lifecycle"],
dependencies=[Depends(require_viewer)],
)
def get_lifecycle_service(
session: Annotated[Session, Depends(get_session)],
) -> LifecycleService:
return LifecycleService(session)
Service = Annotated[LifecycleService, Depends(get_lifecycle_service)]
@router.get("/approval-policies", response_model=list[ApprovalPolicyResponse])
def approval_policies(service: Service, _admin: Admin) -> list[ApprovalPolicyResponse]:
return service.policies()
@router.post(
"/approval-policies",
response_model=ApprovalPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_approval_policy(
request: ApprovalPolicyCreate, service: Service, _admin: Admin
) -> ApprovalPolicyResponse:
return service.create_policy(request)
@router.get("/subjects", response_model=list[LifecycleSubjectResponse])
def subjects(service: Service, _admin: Admin) -> list[LifecycleSubjectResponse]:
return service.subjects()
@router.post(
"/subjects",
response_model=LifecycleSubjectResponse,
status_code=status.HTTP_201_CREATED,
)
def create_subject(
request: LifecycleSubjectCreate, service: Service, _admin: Admin
) -> LifecycleSubjectResponse:
return service.create_subject(request)
@router.get("/approval-requests", response_model=list[ApprovalRequestResponse])
def approval_requests(service: Service, _admin: Admin) -> list[ApprovalRequestResponse]:
return service.approval_requests()
@router.post(
"/approval-requests",
response_model=ApprovalRequestResponse,
status_code=status.HTTP_201_CREATED,
)
def request_approval(
request: ApprovalRequestCreate, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.request_approval(request)
@router.get("/approval-requests/{approval_id}", response_model=ApprovalRequestResponse)
def approval_request(
approval_id: uuid.UUID, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.approval(approval_id)
@router.post("/approval-requests/{approval_id}/approve", response_model=ApprovalRequestResponse)
def approve_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "approve")
@router.post("/approval-requests/{approval_id}/reject", response_model=ApprovalRequestResponse)
def reject_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "reject")
@router.post("/approval-requests/{approval_id}/revoke", response_model=ApprovalRequestResponse)
def revoke_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "revoke")
@router.get("/promotion-plans", response_model=list[PromotionPlanResponse])
def promotion_plans(service: Service, _admin: Admin) -> list[PromotionPlanResponse]:
return service.plans()
@router.post(
"/promotion-plans",
response_model=PromotionPlanResponse,
status_code=status.HTTP_201_CREATED,
)
def create_promotion_plan(
request: PromotionPlanCreate, service: Service, _admin: Admin
) -> PromotionPlanResponse:
return service.create_plan(request)
@router.get("/promotion-plans/{plan_id}", response_model=PromotionPlanResponse)
def promotion_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> PromotionPlanResponse:
return service.plan(plan_id)
@router.post("/promotion-plans/{plan_id}/approve", response_model=PromotionPlanResponse)
def approve_promotion_plan(
plan_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> PromotionPlanResponse:
return service.approve_plan(plan_id, request)
@router.post("/promotion-plans/{plan_id}/execute", response_model=LifecycleOperationResponse)
def execute_promotion_plan(
plan_id: uuid.UUID, request: PlanExecutionCreate, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.execute_plan(plan_id, request)
@router.get("/operations", response_model=list[LifecycleOperationResponse])
def operations(service: Service, _admin: Admin) -> list[LifecycleOperationResponse]:
return service.operations()
@router.get("/operations/{operation_id}", response_model=LifecycleOperationResponse)
def operation(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.operation(operation_id)
@router.post("/operations/{operation_id}/canary", response_model=LifecycleOperationResponse)
def observe_canary(
operation_id: uuid.UUID, request: CanaryObservation, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.observe_canary(operation_id, request)
@router.post("/operations/{operation_id}/commit", response_model=LifecycleOperationResponse)
def commit_operation(
operation_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.commit_operation(operation_id, request)
@router.post("/operations/{operation_id}/rollback", response_model=LifecycleOperationResponse)
def rollback_operation(
operation_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.rollback_operation(operation_id, request)
@router.post("/reconcile", response_model=dict[str, int])
def reconcile(service: Service, _admin: Admin) -> dict[str, int]:
return {"reconciled_operations": service.reconcile_incomplete()}
@router.get("/retention-policies", response_model=list[RetentionPolicyResponse])
def retention_policies(service: Service, _admin: Admin) -> list[RetentionPolicyResponse]:
return service.retention_policies()
@router.post(
"/retention-policies",
response_model=RetentionPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_retention_policy(
request: RetentionPolicyCreate, service: Service, _admin: Admin
) -> RetentionPolicyResponse:
return service.create_retention_policy(request)
@router.get("/cleanup-plans", response_model=list[CleanupPlanResponse])
def cleanup_plans(service: Service, _admin: Admin) -> list[CleanupPlanResponse]:
return service.cleanup_plans()
@router.post(
"/cleanup-plans",
response_model=CleanupPlanResponse,
status_code=status.HTTP_201_CREATED,
)
def create_cleanup_plan(
request: CleanupPlanCreate, service: Service, _admin: Admin
) -> CleanupPlanResponse:
return service.create_cleanup_plan(request)
@router.get("/cleanup-plans/{plan_id}", response_model=CleanupPlanResponse)
def cleanup_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> CleanupPlanResponse:
return service.cleanup_plan(plan_id)
@router.post("/cleanup-plans/{plan_id}/execute", response_model=CleanupPlanResponse)
def execute_cleanup_plan(
plan_id: uuid.UUID, request: CleanupExecutionCreate, service: Service, _admin: Admin
) -> CleanupPlanResponse:
return service.execute_cleanup(plan_id, request)
@router.get("/events", response_model=list[LifecycleEventResponse])
def lifecycle_events(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=1000)] = 200,
) -> list[LifecycleEventResponse]:
return service.events(limit)
@@ -0,0 +1,191 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.migration_contracts import (
BatchReport,
CutoverOperationResponse,
CutoverPrepare,
CutoverReport,
MigrationBatchResponse,
MigrationEventResponse,
MigrationPlanCreate,
MigrationPlanResponse,
MigrationValidationPolicyCreate,
MigrationValidationPolicyResponse,
PreflightReport,
ReconciliationReport,
RollbackReport,
ShadowReport,
StateAction,
ValidationReport,
ValidationSnapshotResponse,
)
from modelforge_api.services.migration_engine import MigrationEngineService
router = APIRouter(
prefix="/api/v1/admin/migrations",
tags=["migrations"],
dependencies=[Depends(require_viewer)],
)
def get_service(session: Annotated[Session, Depends(get_session)]) -> MigrationEngineService:
return MigrationEngineService(session)
Service = Annotated[MigrationEngineService, Depends(get_service)]
@router.get("/validation-policies", response_model=list[MigrationValidationPolicyResponse])
def validation_policies(service: Service, _admin: Admin) -> list[MigrationValidationPolicyResponse]:
return service.validation_policies()
@router.post(
"/validation-policies",
response_model=MigrationValidationPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_validation_policy(
request: MigrationValidationPolicyCreate, service: Service, _admin: Admin
) -> MigrationValidationPolicyResponse:
return service.create_validation_policy(request)
@router.get("/plans", response_model=list[MigrationPlanResponse])
def plans(service: Service, _admin: Admin) -> list[MigrationPlanResponse]:
return service.plans()
@router.post("/plans", response_model=MigrationPlanResponse, status_code=status.HTTP_201_CREATED)
def create_plan(
request: MigrationPlanCreate, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.create_plan(request)
@router.get("/plans/{plan_id}", response_model=MigrationPlanResponse)
def plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> MigrationPlanResponse:
return service.plan(plan_id)
@router.post("/plans/{plan_id}/preflight", response_model=MigrationPlanResponse)
def preflight(
plan_id: uuid.UUID, request: PreflightReport, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.preflight(plan_id, request)
@router.post("/plans/{plan_id}/backfill/start", response_model=MigrationPlanResponse)
def start_backfill(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.start_backfill(plan_id, request)
@router.post("/plans/{plan_id}/backfill/pause", response_model=MigrationPlanResponse)
def pause_backfill(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.pause_backfill(plan_id, request)
@router.post("/plans/{plan_id}/batches", response_model=MigrationBatchResponse)
def record_batch(
plan_id: uuid.UUID, request: BatchReport, service: Service, _admin: Admin
) -> MigrationBatchResponse:
return service.record_batch(plan_id, request)
@router.get("/plans/{plan_id}/batches", response_model=list[MigrationBatchResponse])
def batches(plan_id: uuid.UUID, service: Service, _admin: Admin) -> list[MigrationBatchResponse]:
return service.batches(plan_id)
@router.post("/plans/{plan_id}/validation", response_model=ValidationSnapshotResponse)
def validate_target(
plan_id: uuid.UUID, request: ValidationReport, service: Service, _admin: Admin
) -> ValidationSnapshotResponse:
return service.validate(plan_id, request)
@router.get("/plans/{plan_id}/validation", response_model=list[ValidationSnapshotResponse])
def validation_snapshots(
plan_id: uuid.UUID, service: Service, _admin: Admin
) -> list[ValidationSnapshotResponse]:
return service.validation_snapshots(plan_id)
@router.post("/plans/{plan_id}/shadow/start", response_model=MigrationPlanResponse)
def start_shadow(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.start_shadow(plan_id, request)
@router.post("/plans/{plan_id}/shadow/complete", response_model=MigrationPlanResponse)
def complete_shadow(
plan_id: uuid.UUID, request: ShadowReport, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.complete_shadow(plan_id, request)
@router.post("/plans/{plan_id}/cutover/prepare", response_model=CutoverOperationResponse)
def prepare_cutover(
plan_id: uuid.UUID, request: CutoverPrepare, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.prepare_cutover(plan_id, request)
@router.post("/plans/{plan_id}/cutover/report", response_model=CutoverOperationResponse)
def report_cutover(
plan_id: uuid.UUID, request: CutoverReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.report_cutover(plan_id, request)
@router.post("/plans/{plan_id}/rollback", response_model=CutoverOperationResponse)
def rollback(
plan_id: uuid.UUID, request: RollbackReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.rollback(plan_id, request)
@router.post("/reconcile", response_model=CutoverOperationResponse)
def reconcile(
request: ReconciliationReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.reconcile(request)
@router.post("/plans/{plan_id}/cancel", response_model=MigrationPlanResponse)
def cancel(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.cancel(plan_id, request)
@router.get("/cutovers", response_model=list[CutoverOperationResponse])
def cutovers(
service: Service,
_admin: Admin,
plan_id: Annotated[uuid.UUID | None, Query()] = None,
) -> list[CutoverOperationResponse]:
return service.operations(plan_id)
@router.get("/events", response_model=list[MigrationEventResponse])
def events(
service: Service,
_admin: Admin,
plan_id: Annotated[uuid.UUID | None, Query()] = None,
limit: Annotated[int, Query(ge=1, le=1000)] = 200,
) -> list[MigrationEventResponse]:
return service.events(plan_id, limit)
@@ -0,0 +1,237 @@
from __future__ import annotations
import uuid
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Query, Response, status
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.observability import (
AlertAction,
AlertHistoryResponse,
AlertResponse,
AlertRuleCreate,
AlertRuleResponse,
CapacitySnapshotResponse,
IncidentResponse,
MaintenanceWindowCreate,
MaintenanceWindowResponse,
OperationsOverview,
SLIDefinitionResponse,
SLOEvaluationResponse,
SLOPolicyCreate,
SLOPolicyResponse,
TrendResponse,
metrics,
)
from modelforge_api.persistence.models import (
AlertHistoryEvent,
IncidentTimelineEvent,
OperationalIncident,
)
from modelforge_api.services.observability import ObservabilityService
router = APIRouter(tags=["operations"], dependencies=[Depends(require_viewer)])
def get_service(session: Annotated[Session, Depends(get_session)]) -> ObservabilityService:
return ObservabilityService(session)
Service = Annotated[ObservabilityService, Depends(get_service)]
@router.get("/metrics", response_class=Response)
def prometheus_metrics(service: Service, _admin: Admin) -> Response:
"""Admin-isolated Prometheus exposition; process metrics survive DB failure."""
try:
content = service.prometheus()
metrics.gauge("modelforge_observability_degraded", {}, 0)
except SQLAlchemyError:
service.session.rollback()
metrics.gauge("modelforge_observability_degraded", {}, 1)
content = metrics.render()
return Response(content=content, media_type="text/plain; version=0.0.4; charset=utf-8")
@router.get("/api/v1/admin/operations/overview", response_model=OperationsOverview)
def overview(service: Service, _admin: Admin) -> OperationsOverview:
return service.overview()
@router.get(
"/api/v1/admin/operations/slis", response_model=list[SLIDefinitionResponse]
)
def sli_definitions(service: Service, _admin: Admin) -> list[SLIDefinitionResponse]:
return service.definitions()
@router.get(
"/api/v1/admin/operations/slo-policies", response_model=list[SLOPolicyResponse]
)
def slo_policies(service: Service, _admin: Admin) -> list[SLOPolicyResponse]:
return service.policies()
@router.post(
"/api/v1/admin/operations/slo-policies",
response_model=SLOPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_slo_policy(
request: SLOPolicyCreate, service: Service, _admin: Admin
) -> SLOPolicyResponse:
return service.create_policy(request)
@router.get(
"/api/v1/admin/operations/slo-evaluations", response_model=list[SLOEvaluationResponse]
)
def slo_evaluations(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[SLOEvaluationResponse]:
return service.evaluations(limit)
@router.post(
"/api/v1/admin/operations/slo-evaluations/run",
response_model=list[SLOEvaluationResponse],
)
def evaluate_slos(service: Service, _admin: Admin) -> list[SLOEvaluationResponse]:
return service.evaluate_slos()
@router.get(
"/api/v1/admin/operations/alert-rules", response_model=list[AlertRuleResponse]
)
def alert_rules(service: Service, _admin: Admin) -> list[AlertRuleResponse]:
return service.rules()
@router.post(
"/api/v1/admin/operations/alert-rules",
response_model=AlertRuleResponse,
status_code=status.HTTP_201_CREATED,
)
def create_alert_rule(
request: AlertRuleCreate, service: Service, _admin: Admin
) -> AlertRuleResponse:
return service.create_rule(request)
@router.get("/api/v1/admin/operations/alerts", response_model=list[AlertResponse])
def alerts(
service: Service, _admin: Admin, limit: int = Query(default=200, ge=1, le=1000)
) -> list[AlertResponse]:
return service.alerts(limit)
@router.post("/api/v1/admin/operations/alerts/evaluate", response_model=list[AlertResponse])
def evaluate_alerts(service: Service, _admin: Admin) -> list[AlertResponse]:
return service.evaluate_alerts()
@router.post(
"/api/v1/admin/operations/alerts/{alert_id}/acknowledge", response_model=AlertResponse
)
def acknowledge_alert(
alert_id: uuid.UUID, request: AlertAction, service: Service, _admin: Admin
) -> AlertResponse:
return service.acknowledge(alert_id, request)
@router.get(
"/api/v1/admin/operations/alerts/{alert_id}/history",
response_model=list[AlertHistoryResponse],
)
def alert_history(
alert_id: uuid.UUID, service: Service, _admin: Admin
) -> list[AlertHistoryResponse]:
rows = service.session.scalars(
select(AlertHistoryEvent)
.where(AlertHistoryEvent.alert_id == alert_id)
.order_by(AlertHistoryEvent.occurred_at)
)
return [AlertHistoryResponse.model_validate(item) for item in rows]
@router.get(
"/api/v1/admin/operations/maintenance-windows",
response_model=list[MaintenanceWindowResponse],
)
def maintenance_windows(service: Service, _admin: Admin) -> list[MaintenanceWindowResponse]:
return service.maintenance_windows()
@router.post(
"/api/v1/admin/operations/maintenance-windows",
response_model=MaintenanceWindowResponse,
status_code=status.HTTP_201_CREATED,
)
def create_maintenance_window(
request: MaintenanceWindowCreate, service: Service, _admin: Admin
) -> MaintenanceWindowResponse:
return service.create_maintenance_window(request)
@router.get("/api/v1/admin/operations/capacity", response_model=list[CapacitySnapshotResponse])
def capacity(
service: Service, _admin: Admin, limit: int = Query(default=500, ge=1, le=5000)
) -> list[CapacitySnapshotResponse]:
return service.capacity(limit)
@router.post(
"/api/v1/admin/operations/capacity/collect", response_model=list[CapacitySnapshotResponse]
)
def collect_capacity(service: Service, _admin: Admin) -> list[CapacitySnapshotResponse]:
return service.collect_capacity()
@router.get("/api/v1/admin/operations/capacity/{node_id}/trend", response_model=TrendResponse)
def capacity_trend(
node_id: uuid.UUID,
service: Service,
_admin: Admin,
hours: int = Query(default=24, ge=1, le=2160),
) -> TrendResponse:
return service.capacity_trend(node_id, hours)
@router.post("/api/v1/admin/operations/retention/run", response_model=dict[str, int])
def apply_retention(service: Service, _admin: Admin) -> dict[str, int]:
return service.apply_retention()
@router.get("/api/v1/admin/operations/incidents", response_model=list[IncidentResponse])
def incidents(service: Service, _admin: Admin) -> list[IncidentResponse]:
rows = service.session.scalars(
select(OperationalIncident).order_by(OperationalIncident.last_seen_at.desc())
)
return [IncidentResponse.model_validate(item) for item in rows]
@router.get("/api/v1/admin/operations/incidents/{incident_id}/timeline")
def incident_timeline(
incident_id: uuid.UUID, service: Service, _admin: Admin
) -> list[dict[str, Any]]:
rows = service.session.scalars(
select(IncidentTimelineEvent)
.where(IncidentTimelineEvent.incident_id == incident_id)
.order_by(IncidentTimelineEvent.occurred_at)
)
return [
{
"id": str(item.id),
"alert_id": str(item.alert_id) if item.alert_id else None,
"event_type": item.event_type,
"relation": item.relation,
"summary": item.summary,
"occurred_at": item.occurred_at,
}
for item in rows
]
@@ -0,0 +1,215 @@
"""Operator-only recovery API.
Backup creation, verification and restore *planning* are safe control-plane operations. Restore
*execution* is deliberately restricted: it can only run against an isolated destination that is
not this control plane's own database, and replacing a production database stays an operator
runbook/CLI action rather than a remote call.
"""
from __future__ import annotations
import uuid
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.recovery import (
ArtifactRecoveryCreate,
ArtifactRecoveryResponse,
BackupCapacityEstimate,
BackupSetCreate,
BackupSetResponse,
RecoveryAssetResponse,
RecoveryDashboard,
RecoveryPolicyCreate,
RecoveryPolicyResponse,
RestoreAdvanceRequest,
RestoreOperationEventResponse,
RestoreOperationResponse,
RestorePlanCreate,
RestorePlanResponse,
)
from modelforge_api.services.recovery import RecoveryService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1/admin/recovery",
tags=["recovery"],
dependencies=[Depends(require_viewer)],
)
def get_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RecoveryService:
return RecoveryService(session, settings)
Service = Annotated[RecoveryService, Depends(get_service)]
@router.get("/dashboard", response_model=RecoveryDashboard)
def dashboard(service: Service, _admin: Admin) -> RecoveryDashboard:
return service.dashboard()
@router.get("/policies", response_model=list[RecoveryPolicyResponse])
def policies(service: Service, _admin: Admin) -> list[RecoveryPolicyResponse]:
return service.policies()
@router.post(
"/policies",
response_model=RecoveryPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_policy(
request: RecoveryPolicyCreate, service: Service, _admin: Admin
) -> RecoveryPolicyResponse:
return service.create_policy(request)
@router.get("/assets", response_model=list[RecoveryAssetResponse])
def assets(service: Service, _admin: Admin) -> list[RecoveryAssetResponse]:
return service.assets()
@router.get("/capacity", response_model=BackupCapacityEstimate)
def capacity(service: Service, _admin: Admin) -> BackupCapacityEstimate:
return service.estimate_capacity()
@router.get("/backups", response_model=list[BackupSetResponse])
def backups(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[BackupSetResponse]:
return service.backups(limit)
@router.post(
"/backups", response_model=BackupSetResponse, status_code=status.HTTP_201_CREATED
)
def create_backup(
request: BackupSetCreate, service: Service, _admin: Admin
) -> BackupSetResponse:
return service.create_backup(request)
@router.get("/backups/{backup_set_id}", response_model=BackupSetResponse)
def backup(backup_set_id: uuid.UUID, service: Service, _admin: Admin) -> BackupSetResponse:
return service.backup(backup_set_id)
@router.post("/backups/{backup_set_id}/verify", response_model=BackupSetResponse)
def verify_backup(
backup_set_id: uuid.UUID, service: Service, _admin: Admin
) -> BackupSetResponse:
return service.verify_backup(backup_set_id)
@router.post("/retention/run", response_model=dict[str, int])
def apply_retention(service: Service, _admin: Admin) -> dict[str, int]:
return service.apply_retention()
@router.get("/restore-plans", response_model=list[RestorePlanResponse])
def restore_plans(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[RestorePlanResponse]:
return service.restore_plans(limit)
@router.post(
"/restore-plans", response_model=RestorePlanResponse, status_code=status.HTTP_201_CREATED
)
def create_restore_plan(
request: RestorePlanCreate, service: Service, _admin: Admin
) -> RestorePlanResponse:
return service.create_restore_plan(request)
@router.get("/restore-plans/{plan_id}", response_model=RestorePlanResponse)
def restore_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> RestorePlanResponse:
return service.restore_plan(plan_id)
@router.post("/restore-plans/{plan_id}/preflight", response_model=RestorePlanResponse)
def preflight(plan_id: uuid.UUID, service: Service, _admin: Admin) -> RestorePlanResponse:
return service.preflight(plan_id)
@router.post(
"/restore-plans/{plan_id}/start",
response_model=RestoreOperationResponse,
status_code=status.HTTP_201_CREATED,
)
def start_restore(
plan_id: uuid.UUID,
request: RestoreAdvanceRequest,
service: Service,
_admin: Admin,
) -> RestoreOperationResponse:
return service.start_restore(plan_id, request)
@router.get("/restore-operations", response_model=list[RestoreOperationResponse])
def restore_operations(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[RestoreOperationResponse]:
return service.restore_operations(limit)
@router.get("/restore-operations/{operation_id}", response_model=RestoreOperationResponse)
def restore_operation(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> RestoreOperationResponse:
return service.restore_operation(operation_id)
@router.post("/restore-operations/{operation_id}/advance", response_model=RestoreOperationResponse)
def advance_restore(
operation_id: uuid.UUID,
request: RestoreAdvanceRequest,
service: Service,
_admin: Admin,
) -> RestoreOperationResponse:
return service.advance_restore(operation_id, request)
@router.get(
"/restore-operations/{operation_id}/events",
response_model=list[RestoreOperationEventResponse],
)
def restore_events(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> list[RestoreOperationEventResponse]:
return service.restore_events(operation_id)
@router.get("/artifact-recoveries", response_model=list[ArtifactRecoveryResponse])
def artifact_recoveries(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[ArtifactRecoveryResponse]:
return service.artifact_recoveries(limit)
@router.post(
"/artifact-recoveries",
response_model=ArtifactRecoveryResponse,
status_code=status.HTTP_201_CREATED,
)
def plan_artifact_recovery(
request: ArtifactRecoveryCreate, service: Service, _admin: Admin
) -> ArtifactRecoveryResponse:
return service.plan_artifact_recovery(request)
@router.get("/fingerprint", response_model=dict[str, Any])
def fingerprint(service: Service, _admin: Admin) -> dict[str, Any]:
"""Bounded semantic fingerprint of the live control plane; contains no secret material."""
return service.fingerprint()
@@ -0,0 +1,291 @@
from __future__ import annotations
import math
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, Response, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.registry import (
ArtifactCreate,
ArtifactResponse,
CapacityDecision,
DerivedArtifactCreate,
DerivedArtifactResponse,
ModelCreate,
ModelResponse,
ModelUpdate,
Page,
RevisionCreate,
RevisionResponse,
StorageRootCreate,
StorageRootObservation,
StorageRootResponse,
StorageRootUpdate,
VerifyResponse,
)
from modelforge_api.services.registry import RegistryService
router = APIRouter(
prefix="/api/v1", tags=["model-registry"], dependencies=[Depends(require_viewer)]
)
def get_registry_service(session: Annotated[Session, Depends(get_session)]) -> RegistryService:
return RegistryService(session)
Service = Annotated[RegistryService, Depends(get_registry_service)]
def page_values[T: BaseModel](items: list[T], page: int, page_size: int) -> Page[T]:
total = len(items)
start = (page - 1) * page_size
return Page(
items=items[start : start + page_size],
page=page,
page_size=page_size,
total=total,
pages=math.ceil(total / page_size) if total else 0,
)
@router.get("/models", response_model=Page[ModelResponse])
def list_models(
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
search: str | None = None,
lifecycle: str | None = None,
source_type: str | None = None,
) -> Page[ModelResponse]:
return service.list_models(
page=page,
page_size=page_size,
search=search,
lifecycle=lifecycle,
source_type=source_type,
)
@router.post(
"/models",
response_model=ModelResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_model(request: ModelCreate, service: Service) -> ModelResponse:
return service.create_model(request)
@router.get("/models/{model_id}", response_model=ModelResponse)
def get_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.get_model(model_id)
@router.patch(
"/models/{model_id}",
response_model=ModelResponse,
dependencies=[Depends(require_operator)],
)
def update_model(model_id: uuid.UUID, request: ModelUpdate, service: Service) -> ModelResponse:
return service.update_model(model_id, request)
@router.post(
"/models/{model_id}/deprecate",
response_model=ModelResponse,
dependencies=[Depends(require_admin)],
)
def deprecate_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.deprecate_model(model_id)
@router.post(
"/models/{model_id}/archive",
response_model=ModelResponse,
dependencies=[Depends(require_admin)],
)
def archive_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.archive_model(model_id)
@router.delete(
"/models/{model_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_model(model_id: uuid.UUID, service: Service) -> Response:
service.delete("model", model_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/models/{model_id}/revisions", response_model=Page[RevisionResponse])
def list_revisions(
model_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[RevisionResponse]:
return page_values(service.revisions(model_id), page, page_size)
@router.post(
"/models/{model_id}/revisions",
response_model=RevisionResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_revision(
model_id: uuid.UUID, request: RevisionCreate, service: Service
) -> RevisionResponse:
return service.create_revision(model_id, request)
@router.delete(
"/revisions/{revision_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_revision(revision_id: uuid.UUID, service: Service) -> Response:
service.delete("model_revision", revision_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/revisions/{revision_id}/artifacts", response_model=Page[ArtifactResponse])
def list_artifacts(
revision_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[ArtifactResponse]:
return page_values(service.artifacts(revision_id), page, page_size)
@router.post(
"/revisions/{revision_id}/artifacts",
response_model=ArtifactResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_artifact(
revision_id: uuid.UUID, request: ArtifactCreate, service: Service
) -> ArtifactResponse:
return service.create_artifact(revision_id, request)
@router.delete(
"/artifacts/{artifact_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_artifact(artifact_id: uuid.UUID, service: Service) -> Response:
service.delete("model_artifact", artifact_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/artifacts/{artifact_id}/verify",
response_model=VerifyResponse,
dependencies=[Depends(require_operator)],
)
def verify_artifact(
artifact_id: uuid.UUID, location_id: uuid.UUID, service: Service
) -> VerifyResponse:
return service.verify_artifact(artifact_id, location_id)
@router.get(
"/revisions/{revision_id}/derived-artifacts", response_model=Page[DerivedArtifactResponse]
)
def list_derived_artifacts(
revision_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[DerivedArtifactResponse]:
return page_values(service.derived(revision_id), page, page_size)
@router.post(
"/derived-artifacts",
response_model=DerivedArtifactResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_derived_artifact(
request: DerivedArtifactCreate, service: Service
) -> DerivedArtifactResponse:
return service.create_derived(request)
@router.delete(
"/derived-artifacts/{artifact_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_derived_artifact(artifact_id: uuid.UUID, service: Service) -> Response:
service.delete("derived_artifact", artifact_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/storage-roots", response_model=list[StorageRootResponse])
def list_storage_roots(
service: Service, node_id: uuid.UUID | None = None
) -> list[StorageRootResponse]:
return service.storage_roots(node_id)
@router.post(
"/storage-roots",
response_model=StorageRootResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_admin)],
)
def create_storage_root(request: StorageRootCreate, service: Service) -> StorageRootResponse:
return service.create_storage_root(request)
@router.patch(
"/storage-roots/{root_id}",
response_model=StorageRootResponse,
dependencies=[Depends(require_admin)],
)
def update_storage_root(
root_id: uuid.UUID, request: StorageRootUpdate, service: Service
) -> StorageRootResponse:
return service.update_storage_root(root_id, request)
@router.post(
"/storage-roots/{root_id}/observations",
response_model=StorageRootResponse,
dependencies=[Depends(require_operator)],
)
def observe_storage_root(
root_id: uuid.UUID, request: StorageRootObservation, service: Service
) -> StorageRootResponse:
return service.observe_storage_root(root_id, request)
@router.get(
"/storage-roots/{root_id}/capacity",
response_model=CapacityDecision,
dependencies=[Depends(require_operator)],
)
def check_capacity(root_id: uuid.UUID, requested_bytes: int, service: Service) -> CapacityDecision:
return service.check_capacity(root_id, requested_bytes)
@router.delete(
"/storage-roots/{root_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_storage_root(root_id: uuid.UUID, service: Service) -> Response:
service.delete("storage_root", root_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,187 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.runtime import (
CompatibilityAssessmentCreate,
CompatibilityAssessmentResponse,
DeploymentCandidateResponse,
ExecutionApprovalCreate,
ExecutionApprovalResponse,
RuntimeEnvironmentCreate,
RuntimeEnvironmentResponse,
RuntimeProbeCreate,
RuntimeProbeResponse,
RuntimeProfileCreate,
RuntimeProfileResponse,
)
from modelforge_api.services.runtime import RuntimeService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1", tags=["runtime-plane"], dependencies=[Depends(require_viewer)]
)
def get_runtime_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RuntimeService:
return RuntimeService(session, settings)
Service = Annotated[RuntimeService, Depends(get_runtime_service)]
@router.post(
"/runtime-environments",
response_model=RuntimeEnvironmentResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_environment(
request: RuntimeEnvironmentCreate, service: Service
) -> RuntimeEnvironmentResponse:
return service.create_environment(request)
@router.get("/runtime-environments", response_model=list[RuntimeEnvironmentResponse])
def environments(service: Service) -> list[RuntimeEnvironmentResponse]:
return service.environments()
@router.get("/runtime-environments/{environment_id}", response_model=RuntimeEnvironmentResponse)
def environment(environment_id: uuid.UUID, service: Service) -> RuntimeEnvironmentResponse:
return service.environment(environment_id)
@router.post(
"/runtime-profiles",
response_model=RuntimeProfileResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_profile(request: RuntimeProfileCreate, service: Service) -> RuntimeProfileResponse:
return service.create_profile(request)
@router.get("/runtime-profiles", response_model=list[RuntimeProfileResponse])
def profiles(service: Service) -> list[RuntimeProfileResponse]:
return service.profiles()
@router.get("/runtime-profiles/{profile_id}", response_model=RuntimeProfileResponse)
def profile(profile_id: uuid.UUID, service: Service) -> RuntimeProfileResponse:
return service.profile(profile_id)
@router.post(
"/artifact-sets/{artifact_set_id}/compatibility-assessments",
response_model=CompatibilityAssessmentResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def assess(
artifact_set_id: uuid.UUID,
request: CompatibilityAssessmentCreate,
service: Service,
) -> CompatibilityAssessmentResponse:
profile = service.repo.profile(request.runtime_profile_id)
if not profile or profile.artifact_set_id != artifact_set_id:
from modelforge_api.services.registry import RegistryConflict
raise RegistryConflict("runtime profile belongs to another artifact set")
return service.assess(request)
@router.get("/compatibility-assessments", response_model=list[CompatibilityAssessmentResponse])
def assessments(
service: Service,
artifact_set_id: Annotated[uuid.UUID | None, Query()] = None,
model_id: Annotated[uuid.UUID | None, Query()] = None,
compute_node_id: Annotated[uuid.UUID | None, Query()] = None,
runtime_profile_id: Annotated[uuid.UUID | None, Query()] = None,
status: Annotated[str | None, Query()] = None,
) -> list[CompatibilityAssessmentResponse]:
return service.assessments(
artifact_set_id=artifact_set_id,
model_id=model_id,
compute_node_id=compute_node_id,
runtime_profile_id=runtime_profile_id,
status=status,
)
@router.get(
"/compatibility-assessments/{assessment_id}",
response_model=CompatibilityAssessmentResponse,
)
def assessment(assessment_id: uuid.UUID, service: Service) -> CompatibilityAssessmentResponse:
return service.assessment(assessment_id)
@router.post(
"/artifact-sets/{artifact_set_id}/execution-approvals",
response_model=ExecutionApprovalResponse,
status_code=201,
dependencies=[Depends(require_admin)],
)
def approve_execution(
artifact_set_id: uuid.UUID,
request: ExecutionApprovalCreate,
service: Service,
) -> ExecutionApprovalResponse:
return service.approve(artifact_set_id, request)
@router.get("/execution-approvals", response_model=list[ExecutionApprovalResponse])
def approvals(
service: Service,
artifact_set_id: Annotated[uuid.UUID | None, Query()] = None,
) -> list[ExecutionApprovalResponse]:
return service.approvals(artifact_set_id)
@router.post(
"/runtime-probes",
response_model=RuntimeProbeResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_probe(request: RuntimeProbeCreate, service: Service) -> RuntimeProbeResponse:
return service.create_probe(request)
@router.get("/runtime-probes", response_model=list[RuntimeProbeResponse])
def probes(service: Service) -> list[RuntimeProbeResponse]:
return service.probes()
@router.get("/runtime-probes/{probe_id}", response_model=RuntimeProbeResponse)
def probe(probe_id: uuid.UUID, service: Service) -> RuntimeProbeResponse:
return service.probe(probe_id)
@router.post(
"/runtime-probes/{probe_id}/cancel",
response_model=RuntimeProbeResponse,
dependencies=[Depends(require_operator)],
)
def cancel_probe(probe_id: uuid.UUID, service: Service) -> RuntimeProbeResponse:
return service.cancel(probe_id)
@router.get("/deployment-candidates", response_model=list[DeploymentCandidateResponse])
def deployment_candidates(service: Service) -> list[DeploymentCandidateResponse]:
return service.candidates()
@router.get("/deployment-candidates/{candidate_id}", response_model=DeploymentCandidateResponse)
def deployment_candidate(candidate_id: uuid.UUID, service: Service) -> DeploymentCandidateResponse:
return service.candidate(candidate_id)
@@ -0,0 +1,582 @@
from __future__ import annotations
import uuid
from functools import lru_cache
from typing import Annotated
from fastapi import APIRouter, Depends, Header, Query, Request, Response, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.api.routes.agent import AgentIdentity
from modelforge_api.db import get_session
from modelforge_api.domain.serving import (
AgentServingJobComplete,
AgentServingJobFailure,
AgentServingJobLease,
AgentServingStateAck,
AgentServingStateReport,
CapabilityDeploymentResponse,
CapabilityExperimentCreate,
CapabilityExperimentResponse,
CapabilityPromotionCreate,
CoResidencyEvidenceCreate,
CoResidencyEvidenceResponse,
EmbeddingInvokeRequest,
EmbeddingInvokeResponse,
GatewayRequestResponse,
OCRInvokeRequest,
OCRInvokeResponse,
OpenAIEmbeddingItem,
OpenAIEmbeddingRequest,
OpenAIEmbeddingResponse,
OpenAIUsage,
PlacementPlanRequest,
PlacementPlanResponse,
ProductionApprovalCreate,
ProductionApprovalResponse,
ProjectFitEvidenceCreate,
ProjectFitEvidenceResponse,
ProjectIntegrationResponse,
RerankingInvokeRequest,
RerankingInvokeResponse,
ResidencyPolicyUpdate,
SchedulerBudgetResponse,
SchedulerMetricsResponse,
SchedulerPolicyResponse,
SchedulerPolicyUpdate,
ServiceClientCreate,
ServiceClientCreated,
ServiceClientResponse,
SpeechTranscriptionInvokeRequest,
SpeechTranscriptionInvokeResponse,
StableEmbeddingInvokeResponse,
VisionEmbeddingInvokeRequest,
VisionEmbeddingInvokeResponse,
)
from modelforge_api.persistence.models import ServiceClient
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
from modelforge_api.services.serving import (
CapabilityAuthenticationEvidence,
ServingError,
ServingService,
)
from modelforge_api.services.transient_payloads import RedisPayloadStore
from modelforge_api.settings import Settings, get_settings
router = APIRouter(tags=["capability-serving"])
@lru_cache
def payload_store() -> RedisPayloadStore:
return RedisPayloadStore(get_settings().redis_url)
def get_serving_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
manifests: Annotated[ManifestRegistry, Depends(get_manifest_registry)],
) -> ServingService:
return ServingService(session, settings, manifests, payload_store())
Service = Annotated[ServingService, Depends(get_serving_service)]
def _authenticate_capability_client(
request: Request,
service: ServingService,
authorization: str | None,
capability: str,
) -> ServiceClient:
evidence = getattr(request.state, "capability_authentication", None)
if isinstance(evidence, CapabilityAuthenticationEvidence):
return service.reuse_authentication(evidence, authorization, capability)
return service.authenticate(authorization, capability)
def authenticate_rag_embedding_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "rag.embedding@1")
def authenticate_rag_reranking_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "rag.reranking@1")
def authenticate_document_ocr_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "document.ocr@1")
def authenticate_vision_embedding_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "vision.embedding@1")
def authenticate_speech_transcription_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(
request, service, authorization, "speech.transcription@1"
)
RagEmbeddingClient = Annotated[ServiceClient, Depends(authenticate_rag_embedding_client)]
RagRerankingClient = Annotated[ServiceClient, Depends(authenticate_rag_reranking_client)]
DocumentOcrClient = Annotated[ServiceClient, Depends(authenticate_document_ocr_client)]
VisionEmbeddingClient = Annotated[ServiceClient, Depends(authenticate_vision_embedding_client)]
SpeechTranscriptionClient = Annotated[
ServiceClient, Depends(authenticate_speech_transcription_client)
]
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/production-approvals",
response_model=ProductionApprovalResponse,
status_code=status.HTTP_201_CREATED,
)
def approve_production(
candidate_id: uuid.UUID,
request: ProductionApprovalCreate,
service: Service,
_admin: Admin,
) -> ProductionApprovalResponse:
return service.approve_production(candidate_id, request)
@router.get(
"/api/v1/admin/production-approvals",
response_model=list[ProductionApprovalResponse],
)
def production_approvals(service: Service, _admin: Admin) -> list[ProductionApprovalResponse]:
return service.approvals()
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/promote",
response_model=CapabilityDeploymentResponse,
status_code=status.HTTP_201_CREATED,
)
def promote_candidate(
candidate_id: uuid.UUID,
request: CapabilityPromotionCreate,
service: Service,
_admin: Admin,
) -> CapabilityDeploymentResponse:
return service.promote(candidate_id, request)
@router.get(
"/api/v1/capability-deployments",
response_model=list[CapabilityDeploymentResponse],
dependencies=[Depends(require_viewer)],
)
def capability_deployments(service: Service) -> list[CapabilityDeploymentResponse]:
return service.deployments()
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/experiments",
response_model=CapabilityExperimentResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_experiment(
candidate_id: uuid.UUID,
request: CapabilityExperimentCreate,
service: Service,
_admin: Admin,
) -> CapabilityExperimentResponse:
return service.create_experiment(candidate_id, request)
@router.get(
"/api/v1/capability-experiments",
response_model=list[CapabilityExperimentResponse],
dependencies=[Depends(require_viewer)],
)
def capability_experiments(service: Service) -> list[CapabilityExperimentResponse]:
return service.experiments()
@router.post(
"/api/v1/admin/capability-experiments/{experiment_id}/deactivate",
response_model=CapabilityExperimentResponse,
)
def deactivate_capability_experiment(
experiment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityExperimentResponse:
return service.deactivate_experiment(experiment_id)
@router.post(
"/api/v1/admin/capability-deployments/{deployment_id}/unload",
response_model=CapabilityDeploymentResponse,
)
def unload_deployment(
deployment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityDeploymentResponse:
return service.request_unload(deployment_id)
@router.post(
"/api/v1/admin/capability-deployments/{deployment_id}/drain",
response_model=CapabilityDeploymentResponse,
)
def drain_deployment(
deployment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityDeploymentResponse:
return service.request_unload(deployment_id, drain=True)
@router.post(
"/api/v1/admin/service-clients",
response_model=ServiceClientCreated,
status_code=status.HTTP_201_CREATED,
)
def create_service_client(
request: ServiceClientCreate,
service: Service,
_admin: Admin,
response: Response,
) -> ServiceClientCreated:
response.headers["Cache-Control"] = "no-store"
return service.create_client(request)
@router.get("/api/v1/admin/service-clients", response_model=list[ServiceClientResponse])
def service_clients(service: Service, _admin: Admin) -> list[ServiceClientResponse]:
return service.clients()
@router.delete(
"/api/v1/admin/service-clients/{client_id}/credential",
response_model=ServiceClientResponse,
)
def revoke_service_credential(
client_id: uuid.UUID, service: Service, _admin: Admin
) -> ServiceClientResponse:
return service.revoke_client_credential(client_id)
@router.post(
"/api/v1/admin/service-clients/{client_id}/credential/rotate",
response_model=ServiceClientCreated,
)
def rotate_service_credential(
client_id: uuid.UUID, service: Service, _admin: Admin, response: Response
) -> ServiceClientCreated:
response.headers["Cache-Control"] = "no-store"
return service.rotate_client_credential(client_id)
@router.get(
"/api/v1/project-integrations",
response_model=list[ProjectIntegrationResponse],
dependencies=[Depends(require_viewer)],
)
def project_integrations(service: Service) -> list[ProjectIntegrationResponse]:
return service.project_integrations()
@router.post(
"/api/v1/admin/project-fit-evidence",
response_model=ProjectFitEvidenceResponse,
status_code=status.HTTP_201_CREATED,
)
def record_project_fit_evidence(
request: ProjectFitEvidenceCreate, service: Service, _admin: Admin
) -> ProjectFitEvidenceResponse:
return service.record_project_fit(request)
@router.get(
"/api/v1/scheduler",
response_model=list[SchedulerBudgetResponse],
dependencies=[Depends(require_viewer)],
)
def scheduler_overview(service: Service) -> list[SchedulerBudgetResponse]:
return service.scheduler_overview()
@router.get(
"/api/v1/scheduler/co-residency",
response_model=list[CoResidencyEvidenceResponse],
dependencies=[Depends(require_viewer)],
)
def co_residency_matrix(service: Service) -> list[CoResidencyEvidenceResponse]:
return service.co_residency_matrix()
@router.post(
"/api/v1/admin/scheduler/co-residency-evidence",
response_model=CoResidencyEvidenceResponse,
status_code=status.HTTP_201_CREATED,
)
def record_co_residency_evidence(
request: CoResidencyEvidenceCreate, service: Service, _admin: Admin
) -> CoResidencyEvidenceResponse:
return service.record_co_residency_evidence(request)
@router.get("/api/v1/admin/scheduler/policy", response_model=SchedulerPolicyResponse)
def scheduler_policy(service: Service, _admin: Admin) -> SchedulerPolicyResponse:
return service.scheduler_policy()
@router.get("/api/v1/admin/scheduler/metrics", response_model=SchedulerMetricsResponse)
def scheduler_metrics(service: Service, _admin: Admin) -> SchedulerMetricsResponse:
return service.scheduler_metrics()
@router.put("/api/v1/admin/scheduler/policy", response_model=SchedulerPolicyResponse)
def update_scheduler_policy(
request: SchedulerPolicyUpdate, service: Service, _admin: Admin
) -> SchedulerPolicyResponse:
return service.update_scheduler_policy(request)
@router.post(
"/api/v1/admin/scheduler/placements/{deployment_id}/dry-run",
response_model=PlacementPlanResponse,
)
def dry_run_placement(
deployment_id: uuid.UUID,
request: PlacementPlanRequest,
service: Service,
_admin: Admin,
) -> PlacementPlanResponse:
return service.dry_run_placement(deployment_id, request)
@router.get(
"/api/v1/admin/scheduler/placements",
response_model=list[PlacementPlanResponse],
)
def placement_history(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[PlacementPlanResponse]:
return service.placement_history(limit)
@router.put(
"/api/v1/admin/capability-deployments/{deployment_id}/residency-policy",
response_model=CapabilityDeploymentResponse,
)
def update_residency_policy(
deployment_id: uuid.UUID,
request: ResidencyPolicyUpdate,
service: Service,
_admin: Admin,
) -> CapabilityDeploymentResponse:
return service.update_residency_policy(deployment_id, request)
@router.get("/api/v1/gateway/requests", response_model=list[GatewayRequestResponse])
def gateway_requests(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[GatewayRequestResponse]:
return service.request_history(limit)
@router.get("/api/v1/admin/latency-traces", response_model=list[GatewayRequestResponse])
def latency_traces(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[GatewayRequestResponse]:
"""Return bounded span summaries; request content and input digests are excluded."""
return service.request_history(limit)
@router.post(
"/api/v1/capabilities/rag.embedding@1/invoke",
response_model=StableEmbeddingInvokeResponse,
)
def invoke_embedding(
request: EmbeddingInvokeRequest,
service: Service,
client: RagEmbeddingClient,
) -> StableEmbeddingInvokeResponse:
return StableEmbeddingInvokeResponse.model_validate(service.invoke(request, client))
@router.post(
"/api/v1/capabilities/rag.reranking@1/invoke",
response_model=RerankingInvokeResponse,
)
def invoke_reranking(
request: RerankingInvokeRequest,
service: Service,
client: RagRerankingClient,
) -> RerankingInvokeResponse:
return service.invoke_reranking(request, client)
@router.post(
"/api/v1/capabilities/document.ocr@1/invoke",
response_model=OCRInvokeResponse,
)
def invoke_ocr(
request: OCRInvokeRequest,
service: Service,
client: DocumentOcrClient,
) -> OCRInvokeResponse:
result, request_id, execution = service.invoke_modality(
"document.ocr", request.model_dump(mode="json"), client
)
return OCRInvokeResponse(request_id=request_id, execution=execution, **result)
@router.post(
"/api/v1/capabilities/vision.embedding@1/invoke",
response_model=VisionEmbeddingInvokeResponse,
)
def invoke_vision_embedding(
request: VisionEmbeddingInvokeRequest,
service: Service,
client: VisionEmbeddingClient,
) -> VisionEmbeddingInvokeResponse:
result, request_id, execution = service.invoke_modality(
"vision.embedding", request.model_dump(mode="json"), client
)
return VisionEmbeddingInvokeResponse(
request_id=request_id,
execution=execution,
dimension=int(result["dimension"]),
embedding_space_id=result["embedding_space_id"],
data=result["vectors"],
)
@router.post(
"/api/v1/capabilities/speech.transcription@1/invoke",
response_model=SpeechTranscriptionInvokeResponse,
)
def invoke_speech_transcription(
request: SpeechTranscriptionInvokeRequest,
service: Service,
client: SpeechTranscriptionClient,
) -> SpeechTranscriptionInvokeResponse:
result, request_id, execution = service.invoke_modality(
"speech.transcription", request.model_dump(mode="json"), client
)
return SpeechTranscriptionInvokeResponse(
request_id=request_id,
execution=execution,
**result,
)
@router.post(
"/api/v1/capability-experiments/{route_key}/invoke",
response_model=EmbeddingInvokeResponse,
)
def invoke_embedding_experiment(
route_key: str,
request: EmbeddingInvokeRequest,
service: Service,
client: RagEmbeddingClient,
) -> EmbeddingInvokeResponse:
route = service.repo.experiment_route(route_key)
deployment = (
service.repo.deployment(route.capability_deployment_id)
if route and route.status == "active"
else None
)
if not deployment:
raise ServingError(404, "EXPERIMENT_NOT_FOUND", "capability experiment is unavailable")
return service.invoke(
request,
client,
deployment=deployment,
experiment_route=route_key,
)
@router.post("/v1/embeddings", response_model=OpenAIEmbeddingResponse)
def openai_embeddings(
request: OpenAIEmbeddingRequest,
service: Service,
client: RagEmbeddingClient,
) -> OpenAIEmbeddingResponse:
native = service.invoke(EmbeddingInvokeRequest(input=request.input), client)
return OpenAIEmbeddingResponse(
data=[
OpenAIEmbeddingItem(index=index, embedding=embedding)
for index, embedding in enumerate(native.data)
],
usage=OpenAIUsage(
prompt_tokens=native.usage.input_tokens,
total_tokens=native.usage.input_tokens,
),
)
@router.get(
"/api/v1/agent/serving-jobs/next",
response_model=AgentServingJobLease | None,
)
def claim_serving_job(
service: Service,
identity: AgentIdentity,
wait_seconds: Annotated[float, Query(ge=0.0, le=5.0)] = 0.0,
) -> AgentServingJobLease | None:
_credential, node = identity
return service.claim_next(node, wait_seconds=wait_seconds)
@router.post("/api/v1/agent/serving-jobs/{job_id}/complete")
def complete_serving_job(
job_id: uuid.UUID,
request: AgentServingJobComplete,
service: Service,
identity: AgentIdentity,
) -> dict[str, str]:
_credential, node = identity
service.complete_job(job_id, node, request)
return {"status": "accepted"}
@router.post("/api/v1/agent/serving-jobs/{job_id}/fail")
def fail_serving_job(
job_id: uuid.UUID,
request: AgentServingJobFailure,
service: Service,
identity: AgentIdentity,
) -> dict[str, str]:
_credential, node = identity
service.fail_job(job_id, node, request)
return {"status": "accepted"}
@router.post(
"/api/v1/agent/serving-state",
response_model=AgentServingStateAck,
)
def report_serving_state(
request: AgentServingStateReport,
service: Service,
identity: AgentIdentity,
) -> AgentServingStateAck:
_credential, node = identity
return service.report_state(node, request)