Spaces:
Sleeping
Sleeping
Deploy public authenticated AutoML gateway
Browse files- execution_metadata.py +114 -1
- gateway_app.py +25 -3
- gateway_catalog.json +41 -1
- gateway_contract.py +44 -1
- gateway_routing.py +142 -3
execution_metadata.py
CHANGED
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
| 10 |
|
| 11 |
import json
|
| 12 |
import os
|
|
|
|
| 13 |
import tempfile
|
| 14 |
import zipfile
|
| 15 |
from datetime import datetime, timezone
|
|
@@ -22,6 +23,19 @@ from ui_contracts import DecisionSource, DecisionTrace, FeatureExtractionReport,
|
|
| 22 |
EXECUTION_METADATA_KEY = "execution_metadata"
|
| 23 |
EXECUTION_METADATA_SCHEMA_VERSION = 1
|
| 24 |
APPROVED_RESEARCH_KEY = "approved_research"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
_APPROVED_RESEARCH_FIELDS = {
|
| 26 |
"schema_version",
|
| 27 |
"research_plan",
|
|
@@ -39,6 +53,8 @@ _PIPELINE_CONFIG_FIELDS = {
|
|
| 39 |
"transform_tool",
|
| 40 |
"ml_tool",
|
| 41 |
"image_backend",
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
|
|
@@ -140,6 +156,7 @@ def make_execution_metadata(
|
|
| 140 |
decision_traces: Sequence[DecisionTrace | Mapping[str, Any]] = (),
|
| 141 |
*,
|
| 142 |
approved_research: Mapping[str, Any] | None = None,
|
|
|
|
| 143 |
) -> dict[str, Any]:
|
| 144 |
"""Return the canonical JSON-safe execution metadata container."""
|
| 145 |
|
|
@@ -163,11 +180,16 @@ def make_execution_metadata(
|
|
| 163 |
if approved_research
|
| 164 |
else {}
|
| 165 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
if approved:
|
| 167 |
approval_trace = DecisionTrace.from_mapping(approved["approval_trace"])
|
| 168 |
if all(trace.trace_id != approval_trace.trace_id for trace in traces):
|
| 169 |
traces.insert(0, approval_trace)
|
| 170 |
-
if report is None and not traces and not approved:
|
| 171 |
return {}
|
| 172 |
result = {
|
| 173 |
"schema_version": EXECUTION_METADATA_SCHEMA_VERSION,
|
|
@@ -176,9 +198,77 @@ def make_execution_metadata(
|
|
| 176 |
}
|
| 177 |
if approved:
|
| 178 |
result[APPROVED_RESEARCH_KEY] = approved
|
|
|
|
|
|
|
| 179 |
return result
|
| 180 |
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
def normalize_approved_research(value: Mapping[str, Any]) -> dict[str, Any]:
|
| 183 |
"""Validate the optional approved plan carried into trusted execution."""
|
| 184 |
|
|
@@ -263,6 +353,7 @@ def normalize_execution_metadata(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
| 263 |
data.get("feature_extraction_report"),
|
| 264 |
traces,
|
| 265 |
approved_research=data.get(APPROVED_RESEARCH_KEY),
|
|
|
|
| 266 |
)
|
| 267 |
|
| 268 |
|
|
@@ -271,12 +362,18 @@ def merge_execution_metadata(
|
|
| 271 |
feature_report: FeatureExtractionReport | Mapping[str, Any] | None = None,
|
| 272 |
decision_traces: Sequence[DecisionTrace | Mapping[str, Any]] = (),
|
| 273 |
approved_research: Mapping[str, Any] | None = None,
|
|
|
|
| 274 |
) -> dict[str, Any]:
|
| 275 |
"""Merge producer metadata without duplicating stable trace identifiers."""
|
| 276 |
|
| 277 |
report = feature_report
|
| 278 |
traces: list[DecisionTrace | Mapping[str, Any]] = []
|
| 279 |
approved = approved_research
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
for value in values:
|
| 281 |
canonical = normalize_execution_metadata(value) if value else {}
|
| 282 |
if canonical.get("feature_extraction_report") is not None:
|
|
@@ -284,6 +381,11 @@ def merge_execution_metadata(
|
|
| 284 |
traces.extend(canonical.get("decision_traces", ()))
|
| 285 |
if canonical.get(APPROVED_RESEARCH_KEY):
|
| 286 |
approved = canonical[APPROVED_RESEARCH_KEY]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
traces.extend(decision_traces)
|
| 288 |
|
| 289 |
deduplicated: dict[str, DecisionTrace] = {}
|
|
@@ -302,6 +404,7 @@ def merge_execution_metadata(
|
|
| 302 |
report,
|
| 303 |
[*deduplicated.values(), *anonymous],
|
| 304 |
approved_research=approved,
|
|
|
|
| 305 |
)
|
| 306 |
|
| 307 |
|
|
@@ -377,6 +480,13 @@ def execution_metadata_from_manifest(manifest: Mapping[str, Any]) -> dict[str, A
|
|
| 377 |
return normalize_execution_metadata(_mapping(value, EXECUTION_METADATA_KEY))
|
| 378 |
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
def manifest_with_execution_metadata(
|
| 381 |
manifest: Mapping[str, Any], metadata: Mapping[str, Any]
|
| 382 |
) -> dict[str, Any]:
|
|
@@ -393,6 +503,7 @@ __all__ = [
|
|
| 393 |
"EXECUTION_METADATA_KEY",
|
| 394 |
"EXECUTION_METADATA_SCHEMA_VERSION",
|
| 395 |
"APPROVED_RESEARCH_KEY",
|
|
|
|
| 396 |
"execution_metadata_from_manifest",
|
| 397 |
"make_decision_trace",
|
| 398 |
"make_execution_metadata",
|
|
@@ -402,4 +513,6 @@ __all__ = [
|
|
| 402 |
"merge_execution_metadata",
|
| 403 |
"normalize_execution_metadata",
|
| 404 |
"normalize_approved_research",
|
|
|
|
|
|
|
| 405 |
]
|
|
|
|
| 10 |
|
| 11 |
import json
|
| 12 |
import os
|
| 13 |
+
import re
|
| 14 |
import tempfile
|
| 15 |
import zipfile
|
| 16 |
from datetime import datetime, timezone
|
|
|
|
| 23 |
EXECUTION_METADATA_KEY = "execution_metadata"
|
| 24 |
EXECUTION_METADATA_SCHEMA_VERSION = 1
|
| 25 |
APPROVED_RESEARCH_KEY = "approved_research"
|
| 26 |
+
TRUSTED_EXTRACTOR_KEY = "trusted_extractor"
|
| 27 |
+
_EXTRACTOR_ID_PATTERN = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
|
| 28 |
+
_EXTRACTOR_VERSION_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$")
|
| 29 |
+
_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
|
| 30 |
+
_TRUSTED_EXTRACTOR_FIELDS = {
|
| 31 |
+
"schema_version",
|
| 32 |
+
"extractor_id",
|
| 33 |
+
"version",
|
| 34 |
+
"configuration",
|
| 35 |
+
"bundle_hash",
|
| 36 |
+
"required_assets",
|
| 37 |
+
}
|
| 38 |
+
_TRUSTED_ASSET_FIELDS = {"asset_id", "sha256", "size_bytes"}
|
| 39 |
_APPROVED_RESEARCH_FIELDS = {
|
| 40 |
"schema_version",
|
| 41 |
"research_plan",
|
|
|
|
| 53 |
"transform_tool",
|
| 54 |
"ml_tool",
|
| 55 |
"image_backend",
|
| 56 |
+
"image_backbone",
|
| 57 |
+
"extractor_configuration",
|
| 58 |
}
|
| 59 |
|
| 60 |
|
|
|
|
| 156 |
decision_traces: Sequence[DecisionTrace | Mapping[str, Any]] = (),
|
| 157 |
*,
|
| 158 |
approved_research: Mapping[str, Any] | None = None,
|
| 159 |
+
trusted_extractor: Mapping[str, Any] | None = None,
|
| 160 |
) -> dict[str, Any]:
|
| 161 |
"""Return the canonical JSON-safe execution metadata container."""
|
| 162 |
|
|
|
|
| 180 |
if approved_research
|
| 181 |
else {}
|
| 182 |
)
|
| 183 |
+
extractor_binding = (
|
| 184 |
+
normalize_trusted_extractor_binding(trusted_extractor)
|
| 185 |
+
if trusted_extractor
|
| 186 |
+
else {}
|
| 187 |
+
)
|
| 188 |
if approved:
|
| 189 |
approval_trace = DecisionTrace.from_mapping(approved["approval_trace"])
|
| 190 |
if all(trace.trace_id != approval_trace.trace_id for trace in traces):
|
| 191 |
traces.insert(0, approval_trace)
|
| 192 |
+
if report is None and not traces and not approved and not extractor_binding:
|
| 193 |
return {}
|
| 194 |
result = {
|
| 195 |
"schema_version": EXECUTION_METADATA_SCHEMA_VERSION,
|
|
|
|
| 198 |
}
|
| 199 |
if approved:
|
| 200 |
result[APPROVED_RESEARCH_KEY] = approved
|
| 201 |
+
if extractor_binding:
|
| 202 |
+
result[TRUSTED_EXTRACTOR_KEY] = extractor_binding
|
| 203 |
return result
|
| 204 |
|
| 205 |
|
| 206 |
+
def normalize_trusted_extractor_binding(value: Mapping[str, Any]) -> dict[str, Any]:
|
| 207 |
+
"""Validate a package-safe binding without treating it as executable authority."""
|
| 208 |
+
|
| 209 |
+
data = _mapping(value, TRUSTED_EXTRACTOR_KEY)
|
| 210 |
+
if set(data) != _TRUSTED_EXTRACTOR_FIELDS or data.get("schema_version") != 1:
|
| 211 |
+
raise ValueError("trusted_extractor has an unsupported shape")
|
| 212 |
+
extractor_id = str(data.get("extractor_id") or "").strip().lower()
|
| 213 |
+
version = str(data.get("version") or "").strip()
|
| 214 |
+
bundle_hash = str(data.get("bundle_hash") or "").strip().lower()
|
| 215 |
+
if not _EXTRACTOR_ID_PATTERN.fullmatch(extractor_id):
|
| 216 |
+
raise ValueError("trusted_extractor.extractor_id is invalid")
|
| 217 |
+
if not _EXTRACTOR_VERSION_PATTERN.fullmatch(version):
|
| 218 |
+
raise ValueError("trusted_extractor.version is invalid")
|
| 219 |
+
if not _SHA256_PATTERN.fullmatch(bundle_hash):
|
| 220 |
+
raise ValueError("trusted_extractor.bundle_hash is invalid")
|
| 221 |
+
configuration = data.get("configuration")
|
| 222 |
+
if not isinstance(configuration, Mapping) or not all(
|
| 223 |
+
isinstance(key, str) for key in configuration
|
| 224 |
+
):
|
| 225 |
+
raise ValueError("trusted_extractor.configuration must be a mapping")
|
| 226 |
+
try:
|
| 227 |
+
configuration_copy = json.loads(
|
| 228 |
+
json.dumps(dict(configuration), allow_nan=False)
|
| 229 |
+
)
|
| 230 |
+
except (TypeError, ValueError, OverflowError, RecursionError) as exc:
|
| 231 |
+
raise ValueError("trusted_extractor.configuration must be JSON-safe") from exc
|
| 232 |
+
if len(json.dumps(configuration_copy).encode("utf-8")) > 32_768:
|
| 233 |
+
raise ValueError("trusted_extractor.configuration is too large")
|
| 234 |
+
raw_assets = data.get("required_assets")
|
| 235 |
+
if not isinstance(raw_assets, (list, tuple)) or len(raw_assets) > 64:
|
| 236 |
+
raise ValueError("trusted_extractor.required_assets must be a bounded array")
|
| 237 |
+
assets: list[dict[str, Any]] = []
|
| 238 |
+
asset_ids: set[str] = set()
|
| 239 |
+
for raw in raw_assets:
|
| 240 |
+
asset = _mapping(raw, "trusted extractor asset")
|
| 241 |
+
if set(asset) != _TRUSTED_ASSET_FIELDS:
|
| 242 |
+
raise ValueError("trusted extractor asset has an unsupported shape")
|
| 243 |
+
asset_id = str(asset.get("asset_id") or "").strip()
|
| 244 |
+
digest = str(asset.get("sha256") or "").strip().lower()
|
| 245 |
+
size = asset.get("size_bytes")
|
| 246 |
+
if (
|
| 247 |
+
not asset_id
|
| 248 |
+
or len(asset_id) > 128
|
| 249 |
+
or asset_id in {".", ".."}
|
| 250 |
+
or "/" in asset_id
|
| 251 |
+
or "\\" in asset_id
|
| 252 |
+
or any(ord(character) < 32 for character in asset_id)
|
| 253 |
+
or asset_id in asset_ids
|
| 254 |
+
):
|
| 255 |
+
raise ValueError("trusted extractor asset_id is invalid or duplicated")
|
| 256 |
+
if not _SHA256_PATTERN.fullmatch(digest):
|
| 257 |
+
raise ValueError("trusted extractor asset sha256 is invalid")
|
| 258 |
+
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 259 |
+
raise ValueError("trusted extractor asset size_bytes must be positive")
|
| 260 |
+
asset_ids.add(asset_id)
|
| 261 |
+
assets.append({"asset_id": asset_id, "sha256": digest, "size_bytes": size})
|
| 262 |
+
return {
|
| 263 |
+
"schema_version": 1,
|
| 264 |
+
"extractor_id": extractor_id,
|
| 265 |
+
"version": version,
|
| 266 |
+
"configuration": configuration_copy,
|
| 267 |
+
"bundle_hash": bundle_hash,
|
| 268 |
+
"required_assets": assets,
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
def normalize_approved_research(value: Mapping[str, Any]) -> dict[str, Any]:
|
| 273 |
"""Validate the optional approved plan carried into trusted execution."""
|
| 274 |
|
|
|
|
| 353 |
data.get("feature_extraction_report"),
|
| 354 |
traces,
|
| 355 |
approved_research=data.get(APPROVED_RESEARCH_KEY),
|
| 356 |
+
trusted_extractor=data.get(TRUSTED_EXTRACTOR_KEY),
|
| 357 |
)
|
| 358 |
|
| 359 |
|
|
|
|
| 362 |
feature_report: FeatureExtractionReport | Mapping[str, Any] | None = None,
|
| 363 |
decision_traces: Sequence[DecisionTrace | Mapping[str, Any]] = (),
|
| 364 |
approved_research: Mapping[str, Any] | None = None,
|
| 365 |
+
trusted_extractor: Mapping[str, Any] | None = None,
|
| 366 |
) -> dict[str, Any]:
|
| 367 |
"""Merge producer metadata without duplicating stable trace identifiers."""
|
| 368 |
|
| 369 |
report = feature_report
|
| 370 |
traces: list[DecisionTrace | Mapping[str, Any]] = []
|
| 371 |
approved = approved_research
|
| 372 |
+
extractor_binding: Mapping[str, Any] | None = (
|
| 373 |
+
normalize_trusted_extractor_binding(trusted_extractor)
|
| 374 |
+
if trusted_extractor
|
| 375 |
+
else None
|
| 376 |
+
)
|
| 377 |
for value in values:
|
| 378 |
canonical = normalize_execution_metadata(value) if value else {}
|
| 379 |
if canonical.get("feature_extraction_report") is not None:
|
|
|
|
| 381 |
traces.extend(canonical.get("decision_traces", ()))
|
| 382 |
if canonical.get(APPROVED_RESEARCH_KEY):
|
| 383 |
approved = canonical[APPROVED_RESEARCH_KEY]
|
| 384 |
+
if canonical.get(TRUSTED_EXTRACTOR_KEY):
|
| 385 |
+
incoming_binding = canonical[TRUSTED_EXTRACTOR_KEY]
|
| 386 |
+
if extractor_binding is not None and dict(extractor_binding) != dict(incoming_binding):
|
| 387 |
+
raise ValueError("cannot merge conflicting trusted extractor bindings")
|
| 388 |
+
extractor_binding = incoming_binding
|
| 389 |
traces.extend(decision_traces)
|
| 390 |
|
| 391 |
deduplicated: dict[str, DecisionTrace] = {}
|
|
|
|
| 404 |
report,
|
| 405 |
[*deduplicated.values(), *anonymous],
|
| 406 |
approved_research=approved,
|
| 407 |
+
trusted_extractor=extractor_binding,
|
| 408 |
)
|
| 409 |
|
| 410 |
|
|
|
|
| 480 |
return normalize_execution_metadata(_mapping(value, EXECUTION_METADATA_KEY))
|
| 481 |
|
| 482 |
|
| 483 |
+
def trusted_extractor_from_manifest(manifest: Mapping[str, Any]) -> dict[str, Any]:
|
| 484 |
+
"""Read the optional package binding; legacy packages return an empty mapping."""
|
| 485 |
+
|
| 486 |
+
metadata = execution_metadata_from_manifest(manifest)
|
| 487 |
+
return dict(metadata.get(TRUSTED_EXTRACTOR_KEY) or {})
|
| 488 |
+
|
| 489 |
+
|
| 490 |
def manifest_with_execution_metadata(
|
| 491 |
manifest: Mapping[str, Any], metadata: Mapping[str, Any]
|
| 492 |
) -> dict[str, Any]:
|
|
|
|
| 503 |
"EXECUTION_METADATA_KEY",
|
| 504 |
"EXECUTION_METADATA_SCHEMA_VERSION",
|
| 505 |
"APPROVED_RESEARCH_KEY",
|
| 506 |
+
"TRUSTED_EXTRACTOR_KEY",
|
| 507 |
"execution_metadata_from_manifest",
|
| 508 |
"make_decision_trace",
|
| 509 |
"make_execution_metadata",
|
|
|
|
| 513 |
"merge_execution_metadata",
|
| 514 |
"normalize_execution_metadata",
|
| 515 |
"normalize_approved_research",
|
| 516 |
+
"normalize_trusted_extractor_binding",
|
| 517 |
+
"trusted_extractor_from_manifest",
|
| 518 |
]
|
gateway_app.py
CHANGED
|
@@ -93,6 +93,7 @@ from gateway_routing import (
|
|
| 93 |
route_existing,
|
| 94 |
route_prediction,
|
| 95 |
route_uploaded,
|
|
|
|
| 96 |
)
|
| 97 |
|
| 98 |
|
|
@@ -123,6 +124,7 @@ IMAGE_BACKBONE_CHOICES = [
|
|
| 123 |
"resnet50",
|
| 124 |
"densenet121",
|
| 125 |
"efficientnet_b0",
|
|
|
|
| 126 |
]
|
| 127 |
IMAGE_CLASSIFIER_CHOICES = [
|
| 128 |
AUTO_CHOICE,
|
|
@@ -164,6 +166,7 @@ def _catalog() -> dict[str, Any]:
|
|
| 164 |
"datasets": {
|
| 165 |
"No datasets configured": {"targets": [], "image": False}
|
| 166 |
},
|
|
|
|
| 167 |
}
|
| 168 |
|
| 169 |
|
|
@@ -1790,6 +1793,11 @@ def existing_ui_handler(
|
|
| 1790 |
)
|
| 1791 |
if approved_research:
|
| 1792 |
payload["approved_research"] = dict(approved_research)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1793 |
yield from run_existing_remote(payload, session_token, request)
|
| 1794 |
|
| 1795 |
|
|
@@ -1885,6 +1893,11 @@ def uploaded_ui_handler(
|
|
| 1885 |
)
|
| 1886 |
if approved_research:
|
| 1887 |
payload["approved_research"] = dict(approved_research)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1888 |
yield from run_uploaded_remote(
|
| 1889 |
payload, train_file, test_file, session_token, request
|
| 1890 |
)
|
|
@@ -1923,7 +1936,7 @@ def parallel_existing_ui_handler(
|
|
| 1923 |
config.get("feature_selection_tool") or AUTOMATIC_SELECTION,
|
| 1924 |
config.get("transform_tool") or AUTOMATIC_SELECTION,
|
| 1925 |
config.get("ml_tool") or AUTOMATIC_SELECTION,
|
| 1926 |
-
AUTO_CHOICE,
|
| 1927 |
AUTO_CHOICE,
|
| 1928 |
AUTO_CHOICE,
|
| 1929 |
AUTO_DEVICE,
|
|
@@ -1939,6 +1952,10 @@ def parallel_existing_ui_handler(
|
|
| 1939 |
)
|
| 1940 |
if approved_research:
|
| 1941 |
payload["approved_research"] = dict(approved_research)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1942 |
yield from run_existing_remote(payload, session_token, request)
|
| 1943 |
|
| 1944 |
|
|
@@ -2079,6 +2096,11 @@ def guided_run_ui_handler(
|
|
| 2079 |
active_ml = str(
|
| 2080 |
approved_config.get("ml_tool") or AUTOMATIC_SELECTION
|
| 2081 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2082 |
state = _guided_snapshot_state(
|
| 2083 |
initial_state,
|
| 2084 |
data_source=source,
|
|
@@ -2115,7 +2137,7 @@ def guided_run_ui_handler(
|
|
| 2115 |
active_feature,
|
| 2116 |
active_transform,
|
| 2117 |
active_ml,
|
| 2118 |
-
|
| 2119 |
image_classifier,
|
| 2120 |
image_size,
|
| 2121 |
image_device,
|
|
@@ -2147,7 +2169,7 @@ def guided_run_ui_handler(
|
|
| 2147 |
active_feature,
|
| 2148 |
active_transform,
|
| 2149 |
active_ml,
|
| 2150 |
-
|
| 2151 |
upload_classifier,
|
| 2152 |
upload_size,
|
| 2153 |
upload_device,
|
|
|
|
| 93 |
route_existing,
|
| 94 |
route_prediction,
|
| 95 |
route_uploaded,
|
| 96 |
+
trusted_gateway_extractor_choices,
|
| 97 |
)
|
| 98 |
|
| 99 |
|
|
|
|
| 124 |
"resnet50",
|
| 125 |
"densenet121",
|
| 126 |
"efficientnet_b0",
|
| 127 |
+
*trusted_gateway_extractor_choices(),
|
| 128 |
]
|
| 129 |
IMAGE_CLASSIFIER_CHOICES = [
|
| 130 |
AUTO_CHOICE,
|
|
|
|
| 166 |
"datasets": {
|
| 167 |
"No datasets configured": {"targets": [], "image": False}
|
| 168 |
},
|
| 169 |
+
"trusted_extractors": {},
|
| 170 |
}
|
| 171 |
|
| 172 |
|
|
|
|
| 1793 |
)
|
| 1794 |
if approved_research:
|
| 1795 |
payload["approved_research"] = dict(approved_research)
|
| 1796 |
+
approved_config = dict(approved_research.get("pipeline_config") or {})
|
| 1797 |
+
if approved_config.get("extractor_configuration") is not None:
|
| 1798 |
+
payload["extractor_configuration"] = dict(
|
| 1799 |
+
approved_config["extractor_configuration"]
|
| 1800 |
+
)
|
| 1801 |
yield from run_existing_remote(payload, session_token, request)
|
| 1802 |
|
| 1803 |
|
|
|
|
| 1893 |
)
|
| 1894 |
if approved_research:
|
| 1895 |
payload["approved_research"] = dict(approved_research)
|
| 1896 |
+
approved_config = dict(approved_research.get("pipeline_config") or {})
|
| 1897 |
+
if approved_config.get("extractor_configuration") is not None:
|
| 1898 |
+
payload["extractor_configuration"] = dict(
|
| 1899 |
+
approved_config["extractor_configuration"]
|
| 1900 |
+
)
|
| 1901 |
yield from run_uploaded_remote(
|
| 1902 |
payload, train_file, test_file, session_token, request
|
| 1903 |
)
|
|
|
|
| 1936 |
config.get("feature_selection_tool") or AUTOMATIC_SELECTION,
|
| 1937 |
config.get("transform_tool") or AUTOMATIC_SELECTION,
|
| 1938 |
config.get("ml_tool") or AUTOMATIC_SELECTION,
|
| 1939 |
+
config.get("image_backbone") or AUTO_CHOICE,
|
| 1940 |
AUTO_CHOICE,
|
| 1941 |
AUTO_CHOICE,
|
| 1942 |
AUTO_DEVICE,
|
|
|
|
| 1952 |
)
|
| 1953 |
if approved_research:
|
| 1954 |
payload["approved_research"] = dict(approved_research)
|
| 1955 |
+
if config.get("extractor_configuration") is not None:
|
| 1956 |
+
payload["extractor_configuration"] = dict(
|
| 1957 |
+
config["extractor_configuration"]
|
| 1958 |
+
)
|
| 1959 |
yield from run_existing_remote(payload, session_token, request)
|
| 1960 |
|
| 1961 |
|
|
|
|
| 2096 |
active_ml = str(
|
| 2097 |
approved_config.get("ml_tool") or AUTOMATIC_SELECTION
|
| 2098 |
)
|
| 2099 |
+
active_backbone = str(
|
| 2100 |
+
approved_config.get("image_backbone")
|
| 2101 |
+
or (image_backbone if source == "managed" else upload_backbone)
|
| 2102 |
+
or AUTO_CHOICE
|
| 2103 |
+
)
|
| 2104 |
state = _guided_snapshot_state(
|
| 2105 |
initial_state,
|
| 2106 |
data_source=source,
|
|
|
|
| 2137 |
active_feature,
|
| 2138 |
active_transform,
|
| 2139 |
active_ml,
|
| 2140 |
+
active_backbone,
|
| 2141 |
image_classifier,
|
| 2142 |
image_size,
|
| 2143 |
image_device,
|
|
|
|
| 2169 |
active_feature,
|
| 2170 |
active_transform,
|
| 2171 |
active_ml,
|
| 2172 |
+
active_backbone,
|
| 2173 |
upload_classifier,
|
| 2174 |
upload_size,
|
| 2175 |
upload_device,
|
gateway_catalog.json
CHANGED
|
@@ -37,5 +37,45 @@
|
|
| 37 |
]
|
| 38 |
}
|
| 39 |
},
|
| 40 |
-
"schema_version": 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
|
|
|
| 37 |
]
|
| 38 |
}
|
| 39 |
},
|
| 40 |
+
"schema_version": 1,
|
| 41 |
+
"trusted_extractors": {
|
| 42 |
+
"trusted_tiny_onnx_v1": {
|
| 43 |
+
"allowed_configuration_fields": [
|
| 44 |
+
"image_size",
|
| 45 |
+
"mean",
|
| 46 |
+
"normalize",
|
| 47 |
+
"std"
|
| 48 |
+
],
|
| 49 |
+
"devices": [
|
| 50 |
+
"cpu",
|
| 51 |
+
"cuda"
|
| 52 |
+
],
|
| 53 |
+
"display_name": "Trusted Tiny ONNX Embedding",
|
| 54 |
+
"modality": "image",
|
| 55 |
+
"output_dimension": 8,
|
| 56 |
+
"tasks": [
|
| 57 |
+
"classification"
|
| 58 |
+
],
|
| 59 |
+
"version": "1.0.0"
|
| 60 |
+
},
|
| 61 |
+
"trusted_tiny_torchscript_v1": {
|
| 62 |
+
"allowed_configuration_fields": [
|
| 63 |
+
"image_size",
|
| 64 |
+
"mean",
|
| 65 |
+
"normalize",
|
| 66 |
+
"std"
|
| 67 |
+
],
|
| 68 |
+
"devices": [
|
| 69 |
+
"cpu",
|
| 70 |
+
"cuda"
|
| 71 |
+
],
|
| 72 |
+
"display_name": "Trusted Tiny TorchScript Embedding",
|
| 73 |
+
"modality": "image",
|
| 74 |
+
"output_dimension": 8,
|
| 75 |
+
"tasks": [
|
| 76 |
+
"classification"
|
| 77 |
+
],
|
| 78 |
+
"version": "1.0.0"
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
}
|
gateway_contract.py
CHANGED
|
@@ -148,6 +148,21 @@ def validate_request(kind: str, value: str | Mapping[str, Any]) -> dict[str, Any
|
|
| 148 |
)
|
| 149 |
except (TypeError, ValueError) as exc:
|
| 150 |
raise ContractError("approved_research is invalid.") from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
elif "image_batch_size" in payload:
|
| 152 |
payload["image_batch_size"] = _bounded_int(
|
| 153 |
payload, "image_batch_size", 16
|
|
@@ -370,7 +385,14 @@ def load_gateway_catalog(path: str | Path) -> dict[str, Any]:
|
|
| 370 |
raise ContractError("Gateway catalog datasets must be a JSON object.")
|
| 371 |
if int(catalog.get("schema_version", 1)) != 1:
|
| 372 |
raise ContractError("Unsupported gateway catalog schema version.")
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
for name, metadata in catalog["datasets"].items():
|
| 375 |
if not isinstance(metadata, dict):
|
| 376 |
raise ContractError("Gateway catalog dataset metadata must be an object.")
|
|
@@ -381,6 +403,27 @@ def load_gateway_catalog(path: str | Path) -> dict[str, Any]:
|
|
| 381 |
"targets": targets,
|
| 382 |
"image": bool(metadata.get("image", False)),
|
| 383 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
return normalized
|
| 385 |
|
| 386 |
|
|
|
|
| 148 |
)
|
| 149 |
except (TypeError, ValueError) as exc:
|
| 150 |
raise ContractError("approved_research is invalid.") from exc
|
| 151 |
+
if payload.get("extractor_configuration") is not None:
|
| 152 |
+
configuration = payload["extractor_configuration"]
|
| 153 |
+
if not isinstance(configuration, Mapping) or not all(
|
| 154 |
+
isinstance(key, str) for key in configuration
|
| 155 |
+
):
|
| 156 |
+
raise ContractError("extractor_configuration must be a mapping.")
|
| 157 |
+
try:
|
| 158 |
+
copied_configuration = json.loads(
|
| 159 |
+
json.dumps(dict(configuration), allow_nan=False)
|
| 160 |
+
)
|
| 161 |
+
except (TypeError, ValueError, OverflowError, RecursionError) as exc:
|
| 162 |
+
raise ContractError("extractor_configuration must be JSON-safe.") from exc
|
| 163 |
+
if len(json.dumps(copied_configuration).encode("utf-8")) > 32_768:
|
| 164 |
+
raise ContractError("extractor_configuration is too large.")
|
| 165 |
+
payload["extractor_configuration"] = copied_configuration
|
| 166 |
elif "image_batch_size" in payload:
|
| 167 |
payload["image_batch_size"] = _bounded_int(
|
| 168 |
payload, "image_batch_size", 16
|
|
|
|
| 385 |
raise ContractError("Gateway catalog datasets must be a JSON object.")
|
| 386 |
if int(catalog.get("schema_version", 1)) != 1:
|
| 387 |
raise ContractError("Unsupported gateway catalog schema version.")
|
| 388 |
+
raw_extractors = catalog.get("trusted_extractors", {})
|
| 389 |
+
if not isinstance(raw_extractors, dict):
|
| 390 |
+
raise ContractError("Gateway catalog trusted_extractors must be a JSON object.")
|
| 391 |
+
normalized: dict[str, Any] = {
|
| 392 |
+
"schema_version": 1,
|
| 393 |
+
"datasets": {},
|
| 394 |
+
"trusted_extractors": {},
|
| 395 |
+
}
|
| 396 |
for name, metadata in catalog["datasets"].items():
|
| 397 |
if not isinstance(metadata, dict):
|
| 398 |
raise ContractError("Gateway catalog dataset metadata must be an object.")
|
|
|
|
| 403 |
"targets": targets,
|
| 404 |
"image": bool(metadata.get("image", False)),
|
| 405 |
}
|
| 406 |
+
for extractor_id, metadata in raw_extractors.items():
|
| 407 |
+
if not isinstance(extractor_id, str) or not isinstance(metadata, dict):
|
| 408 |
+
raise ContractError("Gateway custom extractor metadata must be an object.")
|
| 409 |
+
required = {
|
| 410 |
+
"version",
|
| 411 |
+
"display_name",
|
| 412 |
+
"modality",
|
| 413 |
+
"tasks",
|
| 414 |
+
"devices",
|
| 415 |
+
"output_dimension",
|
| 416 |
+
"allowed_configuration_fields",
|
| 417 |
+
}
|
| 418 |
+
if set(metadata) != required:
|
| 419 |
+
raise ContractError("Gateway custom extractor metadata has an unsupported shape.")
|
| 420 |
+
if metadata["modality"] != "image":
|
| 421 |
+
raise ContractError("Gateway custom extractor modality must be image.")
|
| 422 |
+
if not isinstance(metadata["tasks"], list) or not set(metadata["tasks"]) <= {"classification"}:
|
| 423 |
+
raise ContractError("Gateway custom extractor tasks are invalid.")
|
| 424 |
+
if not isinstance(metadata["devices"], list) or not set(metadata["devices"]) <= {"cpu", "cuda"}:
|
| 425 |
+
raise ContractError("Gateway custom extractor devices are invalid.")
|
| 426 |
+
normalized["trusted_extractors"][extractor_id] = dict(metadata)
|
| 427 |
return normalized
|
| 428 |
|
| 429 |
|
gateway_routing.py
CHANGED
|
@@ -6,7 +6,10 @@ shipped in the source-audited public gateway without pulling in ML code.
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
|
|
|
| 9 |
from dataclasses import dataclass
|
|
|
|
| 10 |
from typing import Any, Literal, Mapping
|
| 11 |
|
| 12 |
|
|
@@ -177,6 +180,64 @@ def _normalize(value: Any, aliases: Mapping[str, str], field: str) -> str:
|
|
| 177 |
raise RoutingError(f"Unknown {field} value: {value!r}.") from exc
|
| 178 |
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
def _routing_mode(value: Any) -> str:
|
| 181 |
mode = str(value or DUAL_ROUTING_MODE).strip().lower()
|
| 182 |
if mode not in VALID_ROUTING_MODES:
|
|
@@ -193,9 +254,7 @@ def _normalized_image_fields(
|
|
| 193 |
backend_key = "uploaded_image_backend" if uploaded else "image_backend"
|
| 194 |
backend = _normalize(payload.get(backend_key), _BACKEND_ALIASES, "image backend")
|
| 195 |
device = _normalize(payload.get("image_device"), _DEVICE_ALIASES, "image device")
|
| 196 |
-
backbone =
|
| 197 |
-
payload.get("image_backbone"), _BACKBONE_ALIASES, "image backbone"
|
| 198 |
-
)
|
| 199 |
preset = _normalize(
|
| 200 |
payload.get("cancer_preset"), _PRESET_ALIASES, "cancer preset"
|
| 201 |
)
|
|
@@ -250,6 +309,19 @@ def _normalized_image_fields(
|
|
| 250 |
if effective_backend == "medical_agent" and preset != "auto":
|
| 251 |
backbone = _PRESET_BACKBONES[preset]
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
return {
|
| 254 |
"task": task,
|
| 255 |
"requested_backend": backend,
|
|
@@ -258,6 +330,7 @@ def _normalized_image_fields(
|
|
| 258 |
"backbone": backbone,
|
| 259 |
"preset": preset,
|
| 260 |
"candidates": candidates,
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
|
|
@@ -268,6 +341,7 @@ def _route_image(fields: Mapping[str, Any], mode: str) -> RouteDecision:
|
|
| 268 |
device = str(fields["device"])
|
| 269 |
backbone = str(fields["backbone"])
|
| 270 |
candidates = tuple(fields["candidates"])
|
|
|
|
| 271 |
|
| 272 |
if mode == SINGLE_GPU_ROUTING_MODE:
|
| 273 |
return RouteDecision(
|
|
@@ -316,6 +390,18 @@ def _route_image(fields: Mapping[str, Any], mode: str) -> RouteDecision:
|
|
| 316 |
"all benchmark candidates are CPU-compatible and CPU was selected",
|
| 317 |
)
|
| 318 |
if backend == "medical_agent":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
if device == "cpu":
|
| 320 |
return RouteDecision(
|
| 321 |
CPU_PROFILE,
|
|
@@ -329,6 +415,18 @@ def _route_image(fields: Mapping[str, Any], mode: str) -> RouteDecision:
|
|
| 329 |
)
|
| 330 |
if backend != "existing":
|
| 331 |
raise RoutingError(f"Unsupported effective image backend: {backend!r}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
if backbone in CPU_ONLY_BACKBONES:
|
| 333 |
return RouteDecision(
|
| 334 |
CPU_PROFILE,
|
|
@@ -439,6 +537,47 @@ def route_prediction(
|
|
| 439 |
"gpu_package_required",
|
| 440 |
"this model package requires the private GPU backend",
|
| 441 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
if device == "cuda":
|
| 443 |
return RouteDecision(
|
| 444 |
GPU_PROFILE,
|
|
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import json
|
| 10 |
+
from functools import lru_cache
|
| 11 |
from dataclasses import dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
from typing import Any, Literal, Mapping
|
| 14 |
|
| 15 |
|
|
|
|
| 180 |
raise RoutingError(f"Unknown {field} value: {value!r}.") from exc
|
| 181 |
|
| 182 |
|
| 183 |
+
@lru_cache(maxsize=1)
|
| 184 |
+
def trusted_gateway_extractor_catalog() -> Mapping[str, Mapping[str, Any]]:
|
| 185 |
+
"""Load the generated public-safe projection of the private registry."""
|
| 186 |
+
|
| 187 |
+
path = Path(__file__).resolve().with_name("gateway_catalog.json")
|
| 188 |
+
if not path.is_file():
|
| 189 |
+
return {}
|
| 190 |
+
try:
|
| 191 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 192 |
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
| 193 |
+
return {}
|
| 194 |
+
raw = payload.get("trusted_extractors") if isinstance(payload, Mapping) else None
|
| 195 |
+
if not isinstance(raw, Mapping):
|
| 196 |
+
return {}
|
| 197 |
+
result: dict[str, Mapping[str, Any]] = {}
|
| 198 |
+
required = {
|
| 199 |
+
"version",
|
| 200 |
+
"display_name",
|
| 201 |
+
"modality",
|
| 202 |
+
"tasks",
|
| 203 |
+
"devices",
|
| 204 |
+
"output_dimension",
|
| 205 |
+
"allowed_configuration_fields",
|
| 206 |
+
}
|
| 207 |
+
for extractor_id, metadata in raw.items():
|
| 208 |
+
normalized_id = _token(extractor_id)
|
| 209 |
+
if normalized_id != extractor_id or not isinstance(metadata, Mapping):
|
| 210 |
+
continue
|
| 211 |
+
if set(metadata) != required:
|
| 212 |
+
continue
|
| 213 |
+
tasks = metadata.get("tasks")
|
| 214 |
+
devices = metadata.get("devices")
|
| 215 |
+
if (
|
| 216 |
+
metadata.get("modality") != "image"
|
| 217 |
+
or not isinstance(tasks, list)
|
| 218 |
+
or not set(tasks) <= {"classification"}
|
| 219 |
+
or not isinstance(devices, list)
|
| 220 |
+
or not devices
|
| 221 |
+
or not set(devices) <= {"cpu", "cuda"}
|
| 222 |
+
):
|
| 223 |
+
continue
|
| 224 |
+
result[normalized_id] = dict(metadata)
|
| 225 |
+
return result
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def trusted_gateway_extractor_choices() -> tuple[str, ...]:
|
| 229 |
+
return tuple(sorted(trusted_gateway_extractor_catalog()))
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _normalize_backbone(value: Any) -> str:
|
| 233 |
+
token = _token(value)
|
| 234 |
+
if token in _BACKBONE_ALIASES:
|
| 235 |
+
return _BACKBONE_ALIASES[token]
|
| 236 |
+
if token in trusted_gateway_extractor_catalog():
|
| 237 |
+
return token
|
| 238 |
+
raise RoutingError(f"Unknown image backbone value: {value!r}.")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
def _routing_mode(value: Any) -> str:
|
| 242 |
mode = str(value or DUAL_ROUTING_MODE).strip().lower()
|
| 243 |
if mode not in VALID_ROUTING_MODES:
|
|
|
|
| 254 |
backend_key = "uploaded_image_backend" if uploaded else "image_backend"
|
| 255 |
backend = _normalize(payload.get(backend_key), _BACKEND_ALIASES, "image backend")
|
| 256 |
device = _normalize(payload.get("image_device"), _DEVICE_ALIASES, "image device")
|
| 257 |
+
backbone = _normalize_backbone(payload.get("image_backbone"))
|
|
|
|
|
|
|
| 258 |
preset = _normalize(
|
| 259 |
payload.get("cancer_preset"), _PRESET_ALIASES, "cancer preset"
|
| 260 |
)
|
|
|
|
| 309 |
if effective_backend == "medical_agent" and preset != "auto":
|
| 310 |
backbone = _PRESET_BACKBONES[preset]
|
| 311 |
|
| 312 |
+
custom_metadata = trusted_gateway_extractor_catalog().get(backbone)
|
| 313 |
+
if custom_metadata is not None:
|
| 314 |
+
if task not in custom_metadata["tasks"]:
|
| 315 |
+
raise RoutingError(
|
| 316 |
+
f"Registered custom extractor {backbone!r} does not support {task}."
|
| 317 |
+
)
|
| 318 |
+
supported_devices = set(custom_metadata["devices"])
|
| 319 |
+
requested_device = "cuda" if device == "cuda" else device
|
| 320 |
+
if requested_device != "auto" and requested_device not in supported_devices:
|
| 321 |
+
raise RoutingError(
|
| 322 |
+
f"Registered custom extractor {backbone!r} does not support {requested_device}."
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
return {
|
| 326 |
"task": task,
|
| 327 |
"requested_backend": backend,
|
|
|
|
| 330 |
"backbone": backbone,
|
| 331 |
"preset": preset,
|
| 332 |
"candidates": candidates,
|
| 333 |
+
"custom_devices": tuple(custom_metadata["devices"]) if custom_metadata else (),
|
| 334 |
}
|
| 335 |
|
| 336 |
|
|
|
|
| 341 |
device = str(fields["device"])
|
| 342 |
backbone = str(fields["backbone"])
|
| 343 |
candidates = tuple(fields["candidates"])
|
| 344 |
+
custom_devices = tuple(fields.get("custom_devices") or ())
|
| 345 |
|
| 346 |
if mode == SINGLE_GPU_ROUTING_MODE:
|
| 347 |
return RouteDecision(
|
|
|
|
| 390 |
"all benchmark candidates are CPU-compatible and CPU was selected",
|
| 391 |
)
|
| 392 |
if backend == "medical_agent":
|
| 393 |
+
if custom_devices:
|
| 394 |
+
if device == "cpu" or (device == "auto" and "cuda" not in custom_devices):
|
| 395 |
+
return RouteDecision(
|
| 396 |
+
CPU_PROFILE,
|
| 397 |
+
"trusted_custom_extractor_cpu",
|
| 398 |
+
"the registered custom extractor is assigned to private CPU execution",
|
| 399 |
+
)
|
| 400 |
+
return RouteDecision(
|
| 401 |
+
GPU_PROFILE,
|
| 402 |
+
"trusted_custom_extractor_gpu",
|
| 403 |
+
"the registered custom extractor is assigned to private GPU execution",
|
| 404 |
+
)
|
| 405 |
if device == "cpu":
|
| 406 |
return RouteDecision(
|
| 407 |
CPU_PROFILE,
|
|
|
|
| 415 |
)
|
| 416 |
if backend != "existing":
|
| 417 |
raise RoutingError(f"Unsupported effective image backend: {backend!r}.")
|
| 418 |
+
if custom_devices:
|
| 419 |
+
if device == "cpu" or (device == "auto" and "cuda" not in custom_devices):
|
| 420 |
+
return RouteDecision(
|
| 421 |
+
CPU_PROFILE,
|
| 422 |
+
"trusted_custom_extractor_cpu",
|
| 423 |
+
"the registered custom extractor is assigned to private CPU execution",
|
| 424 |
+
)
|
| 425 |
+
return RouteDecision(
|
| 426 |
+
GPU_PROFILE,
|
| 427 |
+
"trusted_custom_extractor_gpu",
|
| 428 |
+
"the registered custom extractor is assigned to private GPU execution",
|
| 429 |
+
)
|
| 430 |
if backbone in CPU_ONLY_BACKBONES:
|
| 431 |
return RouteDecision(
|
| 432 |
CPU_PROFILE,
|
|
|
|
| 537 |
"gpu_package_required",
|
| 538 |
"this model package requires the private GPU backend",
|
| 539 |
)
|
| 540 |
+
execution_metadata = manifest.get("execution_metadata")
|
| 541 |
+
binding = (
|
| 542 |
+
execution_metadata.get("trusted_extractor")
|
| 543 |
+
if isinstance(execution_metadata, Mapping)
|
| 544 |
+
else None
|
| 545 |
+
)
|
| 546 |
+
if binding is not None:
|
| 547 |
+
if not isinstance(binding, Mapping):
|
| 548 |
+
raise RoutingError("Trusted extractor package binding is invalid.")
|
| 549 |
+
extractor_id = str(binding.get("extractor_id") or "").strip().lower()
|
| 550 |
+
custom_metadata = trusted_gateway_extractor_catalog().get(extractor_id)
|
| 551 |
+
if custom_metadata is None:
|
| 552 |
+
raise RoutingError("Model package references an unregistered custom extractor.")
|
| 553 |
+
supported_devices = set(custom_metadata["devices"])
|
| 554 |
+
if device == "cpu":
|
| 555 |
+
if "cpu" not in supported_devices:
|
| 556 |
+
raise RoutingError("Custom extractor package does not support CPU replay.")
|
| 557 |
+
return RouteDecision(
|
| 558 |
+
CPU_PROFILE,
|
| 559 |
+
"trusted_custom_package_cpu",
|
| 560 |
+
"registered custom extractor package replay uses private CPU",
|
| 561 |
+
)
|
| 562 |
+
if device == "cuda":
|
| 563 |
+
if "cuda" not in supported_devices:
|
| 564 |
+
raise RoutingError("Custom extractor package does not support CUDA replay.")
|
| 565 |
+
return RouteDecision(
|
| 566 |
+
GPU_PROFILE,
|
| 567 |
+
"trusted_custom_package_gpu",
|
| 568 |
+
"registered custom extractor package replay uses private GPU",
|
| 569 |
+
)
|
| 570 |
+
if "cuda" in supported_devices:
|
| 571 |
+
return RouteDecision(
|
| 572 |
+
GPU_PROFILE,
|
| 573 |
+
"trusted_custom_package_gpu",
|
| 574 |
+
"registered custom extractor package replay uses private GPU",
|
| 575 |
+
)
|
| 576 |
+
return RouteDecision(
|
| 577 |
+
CPU_PROFILE,
|
| 578 |
+
"trusted_custom_package_cpu",
|
| 579 |
+
"registered custom extractor package replay uses private CPU",
|
| 580 |
+
)
|
| 581 |
if device == "cuda":
|
| 582 |
return RouteDecision(
|
| 583 |
GPU_PROFILE,
|