Files
ModelForge-Public/backend/src/modelforge_api/domain/contracts.py
T

433 lines
14 KiB
Python

from __future__ import annotations
import hashlib
import json
import re
from datetime import UTC, datetime
from typing import Any, Literal, Protocol
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from .enums import (
DeploymentChannel,
FailureCode,
FailureOwner,
HealthStatus,
MigrationStatus,
ResidencyPolicy,
UpgradeClass,
VerificationStatus,
WorkloadPriority,
)
CAPABILITY_KEY = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
SHA256 = re.compile(r"^[a-f0-9]{64}$")
COMMIT_SHA = re.compile(r"^[a-f0-9]{40,64}$")
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class SchemaDocument(StrictModel):
schema_: dict[str, Any] = Field(alias="schema")
class ModalityContract(StrictModel):
input: list[str] = Field(min_length=1)
output: list[str] = Field(min_length=1)
class VectorContract(StrictModel):
dimensionality: int | None = Field(default=None, gt=0)
normalized: bool | None = None
cross_deployment_compatible: bool = False
class SLOContract(StrictModel):
latency_p95_ms: int | None = Field(default=None, gt=0)
availability_percent: float | None = Field(default=None, ge=0, le=100)
max_concurrency: int | None = Field(default=None, gt=0)
class PrivacyContract(StrictModel):
classification: Literal["public", "internal", "confidential", "restricted"] = "internal"
allow_persistence: bool = False
allow_logging_payloads: bool = False
allow_network_egress: bool = False
class ResourceRequirement(StrictModel):
accelerator_required: bool = False
minimum_vram_mb: int | None = Field(default=None, ge=0)
cpu_fallback_allowed: bool = False
CapabilityCategory = Literal[
"TEXT", "RAG", "DOCUMENT", "VISION", "AUDIO", "GENERATION", "ASSISTANTS"
]
CapabilityStability = Literal["stable", "experimental", "blocked", "planned"]
EvaluationType = Literal[
"retrieval", "ocr", "visual-retrieval", "asr", "tts", "generation", "llm"
]
ResourceClass = Literal["LIGHT", "MEDIUM", "HEAVY", "EXCLUSIVE_GPU"]
class PayloadLimits(StrictModel):
max_bytes: int = Field(ge=1, le=67_108_864)
max_batch_count: int = Field(default=1, ge=1, le=64)
max_width: int | None = Field(default=None, ge=1, le=16_384)
max_height: int | None = Field(default=None, ge=1, le=16_384)
max_pages: int | None = Field(default=None, ge=1, le=128)
max_duration_seconds: float | None = Field(default=None, gt=0, le=3600)
class CapabilityEstateMetadata(StrictModel):
category: CapabilityCategory
purpose: str = Field(min_length=1, max_length=1000)
stability: CapabilityStability
resource_class: ResourceClass
evaluation_type: EvaluationType
consumers: list[str] = Field(default_factory=list)
payload_limits: PayloadLimits
class FallbackContract(StrictModel):
allowed: bool = False
mode: Literal["none", "compatible_deployment", "degrade", "hard_fail"] = "hard_fail"
capability: str | None = None
@model_validator(mode="after")
def validate_fallback(self) -> FallbackContract:
if not self.allowed and self.mode not in {"none", "hard_fail"}:
raise ValueError("disabled fallback must use none or hard_fail")
return self
class CapabilityContractManifest(StrictModel):
capability: str
version: int = Field(ge=1)
description: str = Field(min_length=1)
input_schema: dict[str, Any]
output_schema: dict[str, Any]
modalities: ModalityContract
languages: list[str] = Field(default_factory=list)
streaming: bool = False
structured_output: bool = False
vector: VectorContract | None = None
slo: SLOContract = Field(default_factory=SLOContract)
quality_metrics: list[str] = Field(min_length=1)
upgrade_class: UpgradeClass
fallback: FallbackContract = Field(default_factory=FallbackContract)
privacy: PrivacyContract = Field(default_factory=PrivacyContract)
resources: ResourceRequirement = Field(default_factory=ResourceRequirement)
production_priority: WorkloadPriority
default_residency: ResidencyPolicy
estate: CapabilityEstateMetadata
@field_validator("capability")
@classmethod
def validate_key(cls, value: str) -> str:
if not CAPABILITY_KEY.fullmatch(value):
raise ValueError("capability must be a dotted, lowercase logical key")
return value
@model_validator(mode="after")
def vector_safety(self) -> CapabilityContractManifest:
vector_output = self.vector is not None or "embedding" in self.capability
if vector_output and not self.vector:
raise ValueError("vector-producing capabilities require a vector contract")
if (
vector_output
and self.vector is not None
and not self.vector.cross_deployment_compatible
and self.upgrade_class is not UpgradeClass.REQUIRES_REINDEX
):
raise ValueError("incompatible vector spaces require requires_reindex")
return self
class ProjectDefinition(StrictModel):
id: str = Field(pattern=r"^[a-z][a-z0-9-]*$")
name: str = Field(min_length=1)
description: str = Field(min_length=1)
class ProjectBindingManifest(StrictModel):
contract_version: int = Field(ge=1)
channel: DeploymentChannel
priority: WorkloadPriority
optional: bool = False
fallback: FallbackContract = Field(default_factory=FallbackContract)
migration_support: Literal["none", "reindex", "schema"] = "none"
slo: SLOContract = Field(default_factory=SLOContract)
benchmark_suites: list[str] = Field(default_factory=list)
class ProjectManifest(StrictModel):
project: ProjectDefinition
bindings: dict[str, ProjectBindingManifest]
notes: list[str] = Field(default_factory=list)
@field_validator("bindings")
@classmethod
def validate_bindings(
cls, value: dict[str, ProjectBindingManifest]
) -> dict[str, ProjectBindingManifest]:
for key in value:
if not CAPABILITY_KEY.fullmatch(key):
raise ValueError(f"invalid capability binding key: {key}")
return value
class CandidateManifest(StrictModel):
id: str
display_name: str
source: str
intended_capabilities: list[str] = Field(min_length=1)
proposed_role: str
preferred_runtime: str | None = None
verification_status: VerificationStatus = VerificationStatus.UNVERIFIED
deployment_status: Literal["not_deployed"] = "not_deployed"
class CandidateRegistryManifest(StrictModel):
candidates: list[CandidateManifest]
class BenchmarkSuiteDefinition(StrictModel):
id: str
capability: str
version: int = Field(ge=1)
purpose: str
dataset_revision: str
runnable: bool = False
class BenchmarkSuiteManifest(StrictModel):
suite: BenchmarkSuiteDefinition
metrics: dict[str, list[str]]
performance: dict[str, list[str]]
release_policy: dict[str, Any]
@model_validator(mode="after")
def require_pinned_dataset_for_runnable_suite(self) -> BenchmarkSuiteManifest:
if self.suite.runnable and self.suite.dataset_revision.startswith("pending"):
raise ValueError("runnable benchmark suites require a pinned dataset revision")
return self
class SecurityDefaults(StrictModel):
trust_remote_code: Literal[False] = False
prefer_safetensors: Literal[True] = True
require_exact_revision_for_approval: Literal[True] = True
require_artifact_digest: Literal[True] = True
inference_network_egress: Literal[False] = False
expose_runtime_workers: Literal[False] = False
class LifecycleDefaults(StrictModel):
automatic_production_promotion: Literal[False] = False
require_local_benchmark_for_stable: Literal[True] = True
retain_previous_stable_as_rollback_target: Literal[True] = True
destructive_cleanup_requires_explicit_approval: Literal[True] = True
class SchedulerDefaults(StrictModel):
priority_order: list[WorkloadPriority]
reserve_vram_mb: int = Field(ge=0)
default_warm_ttl_seconds: int = Field(ge=0)
benchmark_may_disrupt_production: Literal[False] = False
class UpgradeDefaults(StrictModel):
default_behavioral_change_class: Literal[UpgradeClass.BEHAVIORAL]
vector_producer_default_change_class: Literal[UpgradeClass.REQUIRES_REINDEX]
schema_change_requires_contract_version_bump: Literal[True] = True
class PolicyDefaults(StrictModel):
security: SecurityDefaults
lifecycle: LifecycleDefaults
scheduler: SchedulerDefaults
upgrades: UpgradeDefaults
class ArtifactProvenance(StrictModel):
upstream_repository: str
resolved_commit_sha: str
filename: str
artifact_type: str
sha256: str
source_artifact_sha256: str | None = None
derivation_tool: str | None = None
derivation_tool_version: str | None = None
derivation_arguments: dict[str, Any] = Field(default_factory=dict)
imported_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
@field_validator("resolved_commit_sha")
@classmethod
def validate_commit(cls, value: str) -> str:
value = value.lower()
if not COMMIT_SHA.fullmatch(value):
raise ValueError("resolved commit must be a 40-64 character hexadecimal digest")
return value
@field_validator("sha256", "source_artifact_sha256")
@classmethod
def validate_digest(cls, value: str | None) -> str | None:
if value is not None and not SHA256.fullmatch(value.lower()):
raise ValueError("artifact digest must be lowercase SHA-256")
return value.lower() if value else None
@model_validator(mode="after")
def validate_lineage(self) -> ArtifactProvenance:
derived = self.source_artifact_sha256 is not None
if derived and (not self.derivation_tool or not self.derivation_tool_version):
raise ValueError("derived artifacts require derivation tool and version")
return self
class RuntimeProfileContract(StrictModel):
runtime_type: Literal["vllm", "transformers", "diffusers", "llama_cpp", "custom"]
runtime_version: str
runtime_image_digest: str | None = Field(default=None, pattern=r"^sha256:[a-f0-9]{64}$")
artifact_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
quantization: str | None = None
context_length: int | None = Field(default=None, gt=0)
max_concurrency: int = Field(default=1, gt=0)
launch_arguments: dict[str, Any] = Field(default_factory=dict)
environment_constraints: dict[str, Any] = Field(default_factory=dict)
trust_remote_code: bool = False
network_egress: bool = False
@property
def fingerprint(self) -> str:
payload = self.model_dump(mode="json", exclude_none=True)
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
class ResourceEnvelopeContract(StrictModel):
deployment_id: str
accelerator_kind: str
context_length: int | None = Field(default=None, gt=0)
concurrency: int = Field(ge=1)
batch_size: int = Field(ge=1)
idle_vram_mb: int = Field(ge=0)
peak_vram_mb: int = Field(ge=0)
safety_margin_mb: int = Field(default=1024, ge=0)
@model_validator(mode="after")
def peak_not_below_idle(self) -> ResourceEnvelopeContract:
if self.peak_vram_mb < self.idle_vram_mb:
raise ValueError("peak VRAM cannot be below idle VRAM")
return self
class LeaseRequest(StrictModel):
deployment_id: str
priority: WorkloadPriority
residency: ResidencyPolicy
envelope: ResourceEnvelopeContract
warm_ttl_seconds: int = Field(default=900, ge=0)
exclusive: bool = False
allow_cpu_fallback: bool = False
class BenchmarkEnvironmentFingerprint(StrictModel):
runtime_type: str
runtime_version: str
runtime_image_digest: str | None = None
launch_arguments: dict[str, Any]
cuda_version: str | None
driver_version: str | None
accelerator_name: str | None
accelerator_uuid: str | None
context_length: int | None
concurrency: int = Field(ge=1)
seed: int | None
operating_system: str | None
python_version: str | None = None
@property
def digest(self) -> str:
raw = json.dumps(self.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode()).hexdigest()
def comparable_with(self, other: BenchmarkEnvironmentFingerprint) -> bool:
relevant = (
"runtime_type",
"runtime_version",
"runtime_image_digest",
"launch_arguments",
"cuda_version",
"driver_version",
"accelerator_name",
"context_length",
"concurrency",
)
return all(getattr(self, field) == getattr(other, field) for field in relevant)
class BenchmarkRunContract(StrictModel):
deployment_id: str
model_revision_sha: str = Field(pattern=r"^[a-f0-9]{40,64}$")
artifact_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
suite_key: str
suite_revision: str
dataset_revision: str
environment: BenchmarkEnvironmentFingerprint
started_at: datetime
completed_at: datetime | None = None
class MigrationContract(StrictModel):
project_id: str
capability: str
source_deployment_id: str
target_deployment_id: str
upgrade_class: UpgradeClass
current_index_ref: str
shadow_index_ref: str
status: MigrationStatus = MigrationStatus.PLANNED
rollback_retain_until: datetime
@model_validator(mode="after")
def require_reindex(self) -> MigrationContract:
if (
"embedding" in self.capability
and self.upgrade_class is not UpgradeClass.REQUIRES_REINDEX
):
raise ValueError("embedding migrations must be classified requires_reindex")
if self.current_index_ref == self.shadow_index_ref:
raise ValueError("shadow index must be distinct from current production index")
return self
class LayerHealth(StrictModel):
process: HealthStatus
runtime: HealthStatus
model: HealthStatus
capability: HealthStatus
project: HealthStatus
reasons: list[str] = Field(default_factory=list)
class FailurePolicy(StrictModel):
code: FailureCode
owner: FailureOwner
retryable: bool
fallback_allowed: bool
hard_failure: bool
class RuntimeAdapter(Protocol):
"""Stable control-plane boundary; concrete adapters arrive in M5."""
runtime_type: str
def validate_profile(self, profile: RuntimeProfileContract) -> None: ...
def probe(self, profile: RuntimeProfileContract) -> LayerHealth: ...
def load(self, profile: RuntimeProfileContract, lease_id: str) -> str: ...
def unload(self, deployment_id: str) -> None: ...