Spaces:
Running on Zero
Running on Zero
| """Model adapters, and the governance around them. | |
| ADR 0005 and brief Β§30. A model file is only usable once it can account for | |
| itself: explicit source, licence, checksum, model card, version, and validation | |
| notes. Nothing is downloaded at runtime. | |
| This is not bureaucracy. A livestock record that informs a treatment or a sale | |
| has to be reconstructable months later β "which model said this animal weighed | |
| 386 kg, and had anyone checked it worked in Nigeria?" is a question the system | |
| must be able to answer, and it cannot if artefacts arrive anonymously. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import logging | |
| import os | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from app.adapters import fingerprints, licence_policy, licences | |
| from app.capabilities import UNVALIDATED_GEOGRAPHY, Capability | |
| logger = logging.getLogger(__name__) | |
| #: Read in blocks rather than whole. `yolox_x` is 396 MB and this runs on a | |
| #: 2 vCPU / 4 GiB container at start-up, next to onnxruntime's arenas. | |
| _DIGEST_CHUNK = 1 << 20 | |
| MODELS_DIR = Path(__file__).resolve().parent.parent / "models" | |
| #: Every field a model card must carry before its artefact may be loaded. | |
| REQUIRED_CARD_FIELDS = ( | |
| "model_id", | |
| "version", | |
| "capability_key", | |
| "source", | |
| "license", | |
| "sha256", | |
| "validation_notes", | |
| "geography_validated", | |
| ) | |
| class ArtefactError(RuntimeError): | |
| """An artefact is missing, unaccounted for, or does not match its card.""" | |
| #: Licences Animap may not serve from. Copyleft that reaches a network service, | |
| #: and non-commercial terms that a commercial product cannot satisfy at all. | |
| #: | |
| #: ADR 0017 records what AGPL-3.0 actually says, which is narrower than this | |
| #: service used to claim: Β§13's source-offer duty is conditioned on *modifying* | |
| #: the program, and Β§0 puts network interaction outside "convey". The reason | |
| #: these are refused anyway is not that breach is certain β it is that the | |
| #: vendor's published position asserts it, the question is unsettled, and the | |
| #: measured cost of avoiding it is roughly nil. | |
| #: | |
| #: The list is broader than AGPL because the failure it guards against is "an | |
| #: artefact whose terms this product cannot meet", and a Non-Commercial dataset | |
| #: licence fails that just as completely. The Bristol cattle sets are the live | |
| #: example (`app/dispositions.py`). | |
| DISALLOWED_LICENSES = frozenset({ | |
| "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later", | |
| "GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later", "GPL-2.0", | |
| "SSPL-1.0", | |
| "CC-BY-NC-4.0", "CC-BY-NC-SA-4.0", "CC BY-NC 4.0", | |
| "Non-Commercial Government Licence", | |
| }) | |
| #: Kept as the old name because `is_copyleft` reads better at the call site and | |
| #: the AGPL case is still the one this exists for. | |
| NETWORK_COPYLEFT_LICENSES = DISALLOWED_LICENSES | |
| #: Runtimes that can only load an artefact whose terms Animap may not serve | |
| #: under, whatever the card claims about it. | |
| #: | |
| #: **This used to be described as "the check that matters", and it was not.** A | |
| #: card declares its own licence, so the named threat β somebody editing a card | |
| #: β is exactly the case a self-declaration cannot catch: write | |
| #: `"license": "Apache-2.0"` over a path to `yolo11m.pt` and the licence gate | |
| #: waves it through. The fix was to read `runtime` instead. But `runtime` is | |
| #: also a card field, read eleven lines below a paragraph arguing that the card | |
| #: cannot be trusted, so editing `runtime` rather than `license` defeated it in | |
| #: one line and served AGPL weights as `cattle_identity` under Apache-2.0. | |
| #: | |
| #: **What actually binds a licence to an artefact is | |
| #: `app/adapters/fingerprints.py`**, which reads the file's own structure and | |
| #: says which model it holds. `load_card` runs it, `ModelArtefact.effective_runtime` | |
| #: prefers what the bytes say over what the card claims, and every licence | |
| #: decision below keys off that. This frozenset and the table in | |
| #: `app/adapters/licences.py` both still run, because a layered refusal costs | |
| #: nothing and one of the three may be edited wrongly. | |
| #: | |
| #: The adapter stays in the tree for `evaluation/`, which is the evidence behind | |
| #: ADR 0017. It is unreachable from `discover()`, which is the only path that | |
| #: answers a farmer's request. | |
| DISALLOWED_RUNTIMES = frozenset({"ultralytics"}) | |
| class ModelArtefact: | |
| model_id: str | |
| version: str | |
| capability_key: str | |
| source: str | |
| license: str | |
| sha256: str | |
| validation_notes: str | |
| geography_validated: list[str] | |
| path: Path | |
| #: Which adapter reads this file, **as the card claims**. Optional on the | |
| #: card because governance cares about provenance and the runtime is an | |
| #: implementation detail β but a card without it cannot be run, only | |
| #: inspected. Read `effective_runtime` for the answer that is not a claim. | |
| runtime: str | None = None | |
| #: What the artefact's own bytes say it is. `None` when `load_card` was | |
| #: given no file to read, which happens only in tests. | |
| identity: fingerprints.Identification | None = None | |
| #: How an embedding backbone's input is prepared, **read off the card rather | |
| #: than hard-coded at the call site**. | |
| #: | |
| #: `backbones.preprocess` writes out torchvision's eval transform, and its | |
| #: own docstring says getting it wrong "does not raise β it quietly returns | |
| #: worse vectors". A resize or a normalisation constant that disagrees with | |
| #: the export produces vectors of the right width and the wrong meaning, | |
| #: which is the same silent failure `IdentityIndex` pins the backbone id to | |
| #: catch. So the numbers travel with the artefact they belong to. | |
| #: | |
| #: `None` for every artefact that is not an embedding backbone β YOLOX cards | |
| #: carry none of these β and `OnnxEmbeddingAdapter` keeps its own defaults for | |
| #: that case. `load()` shape-checks the result against the graph either way, | |
| #: so a card that disagrees with its own `.onnx` fails loudly. | |
| input_size: int | None = None | |
| image_mean: tuple[float, float, float] | None = None | |
| image_std: tuple[float, float, float] | None = None | |
| embedding_dimensions: int | None = None | |
| def observed_runtime(self) -> str | None: | |
| """The runtime the bytes identify, or `None` if they identify none.""" | |
| return self.identity.runtime if self.identity else None | |
| def refuted_by(self) -> list[str]: | |
| """Which properties of its own claimed runtime this file fails. | |
| Empty unless the card names a runtime that **has** a fingerprint of the | |
| kind the file was read as, and the bytes fail it. See | |
| `fingerprints.Identification.refutes` for why that case had to be split | |
| out of "unidentified". | |
| """ | |
| if self.identity is None: | |
| return [] | |
| return self.identity.refutes(self.runtime) | |
| def effective_runtime(self) -> str | None: | |
| """Which loader really runs, preferring evidence over assertion. | |
| **This is the field every licence decision should read.** The card's | |
| `runtime` is a claim by whoever wrote the card; the fingerprint is a | |
| property of the bytes. When they disagree the bytes win, because the | |
| bytes are what onnxruntime will load and what a farmer's result will | |
| come out of. | |
| Falls back to the card when nothing recognised the file. That is not a | |
| weakening β an unfingerprinted artefact's identity rested on its card | |
| before this existed and still does, and `identity_conflict` reports the | |
| difference between *"checked and agreed"* and *"not checked"*. | |
| **It does not fall back to a claim the bytes refute.** That fallback was | |
| the hole: `observed_runtime or self.runtime` treats *"no fingerprint | |
| matched"* as *"nothing is known"*, so an Ultralytics export with its | |
| initializers renamed inherited its card's `yolox-onnx` and every licence | |
| decision below was answered about YOLOX. `None` here is what makes | |
| `licences.gate` refuse it, under both policies, as an integrity failure | |
| rather than a licence one β which is the distinction that module's own | |
| docstring draws and the reason the founder's "drop nothing for a licence | |
| right now" instruction does not reach this case. | |
| """ | |
| if self.observed_runtime: | |
| return self.observed_runtime | |
| return None if self.refuted_by else self.runtime | |
| def identity_conflict(self) -> str: | |
| """Why the bytes and the card disagree, or an empty string. | |
| Empty covers two different states on purpose β agreement, and nothing | |
| to compare β because a caller is deciding whether to refuse and both | |
| answers are "do not refuse on this". `identity.detail` is where the | |
| difference is legible. | |
| There are two ways to disagree and they read differently. Either the | |
| bytes matched some *other* fingerprint, and the report can name what the | |
| file actually is; or nothing matched at all but the card's own claim was | |
| testable and failed, and the report can only say what the file is not. | |
| The second is still a conflict, and it is the one that was missing. | |
| **Naming what the file is comes first**, because it is the stronger | |
| statement and the one a licence can be attached to. Only when nothing | |
| matched does the report fall back to saying what was disproved. | |
| """ | |
| observed = self.observed_runtime | |
| if observed is not None and self.runtime and observed != self.runtime: | |
| declared_licence = licences.RUNTIME_LICENCES.get(self.runtime) | |
| real_licence = licences.RUNTIME_LICENCES.get(observed) | |
| return ( | |
| f"The card says runtime {self.runtime!r} " | |
| f"({declared_licence.licence if declared_licence else 'unrecorded'}), " | |
| f"but {self.path.name} is structurally {observed!r} " | |
| f"({real_licence.licence if real_licence else 'unrecorded'}). " | |
| f"{self.identity.detail if self.identity else ''} A sha256 proves " | |
| f"these are the reviewed bytes; it does not prove what the bytes " | |
| f"are, and this does." | |
| ) | |
| refuted = self.refuted_by | |
| if observed is None and refuted: | |
| declared_licence = licences.RUNTIME_LICENCES.get(self.runtime or "") | |
| return ( | |
| f"The card says runtime {self.runtime!r} " | |
| f"({declared_licence.licence if declared_licence else 'unrecorded'}), " | |
| f"and {self.path.name} fails {len(refuted)} of that runtime's " | |
| f"structural properties: {'; '.join(refuted)}. No other " | |
| f"fingerprint matched either, so what this file *is* is unknown " | |
| f"β but the card's claim about it has been disproved, and an " | |
| f"artefact nobody can identify may not be served under a licence " | |
| f"borrowed from a model it is not." | |
| ) | |
| return "" | |
| def is_copyleft(self) -> bool: | |
| """Whether the card's *declared* licence is one Animap may not serve. | |
| A self-declaration, and therefore not a control on its own β see | |
| `effective_runtime` for the answer that does not depend on the card | |
| telling the truth. | |
| """ | |
| return self.license in DISALLOWED_LICENSES | |
| def uses_disallowed_runtime(self) -> bool: | |
| """Keyed to the observed runtime, so editing the card does not clear it. | |
| This is the check ADR 0017 added and the one the original exploit was | |
| aimed at. Reading `effective_runtime` rather than `runtime` is what | |
| makes it hold: a card claiming `dinov3-onnx` over `yolo11m.pt` is | |
| `ultralytics` here, because the file's pickle names | |
| `ultralytics.nn.tasks.DetectionModel` and nothing else can load it. | |
| """ | |
| return (self.effective_runtime or "") in DISALLOWED_RUNTIMES | |
| def is_validated(self) -> bool: | |
| """Validation notes must say something. | |
| An empty string is the tell that a card was filled in to get past the | |
| loader rather than because anyone checked the model. | |
| """ | |
| return bool(self.validation_notes.strip()) | |
| def load_card(card_path: Path) -> ModelArtefact: | |
| card = json.loads(card_path.read_text()) | |
| missing = [f for f in REQUIRED_CARD_FIELDS if not card.get(f)] | |
| if missing: | |
| raise ArtefactError( | |
| f"{card_path.name} is missing required fields: {', '.join(missing)}. " | |
| f"An artefact that cannot account for itself is not usable." | |
| ) | |
| artefact_path = (card_path.parent / card.get("artefact", "")).resolve() | |
| # Cards may point sideways β several capabilities share one detector, and | |
| # storing the file twice would mean two things to keep in step. They may not | |
| # point *out*, because a card is a reviewed artefact reference and not a way | |
| # to load whatever is on the box. | |
| root = card_path.parent.parent.resolve() | |
| if not artefact_path.is_relative_to(root): | |
| raise ArtefactError( | |
| f"{card_path.name} names an artefact outside {root.name}/." | |
| ) | |
| if not artefact_path.is_file(): | |
| raise ArtefactError(f"{card_path.name} names an artefact that is not present.") | |
| digest = _digest(artefact_path) | |
| if digest != card["sha256"]: | |
| raise ArtefactError( | |
| f"{artefact_path.name} does not match the checksum on its card. " | |
| f"Expected {card['sha256'][:12]}β¦, found {digest[:12]}β¦" | |
| ) | |
| # Read *after* the checksum, because there is no point asking what a file is | |
| # when it is not the file that was reviewed. A structure this does not | |
| # recognise is recorded as unidentified and is not an error β see | |
| # `fingerprints.identify`. | |
| identity = fingerprints.identify(artefact_path) | |
| return ModelArtefact( | |
| model_id=card["model_id"], | |
| version=card["version"], | |
| capability_key=card["capability_key"], | |
| source=card["source"], | |
| license=card["license"], | |
| sha256=card["sha256"], | |
| validation_notes=card["validation_notes"], | |
| geography_validated=list(card["geography_validated"]), | |
| path=artefact_path, | |
| runtime=card.get("runtime"), | |
| identity=identity, | |
| input_size=card.get("input_size"), | |
| image_mean=_triple(card.get("image_mean")), | |
| image_std=_triple(card.get("image_std")), | |
| embedding_dimensions=card.get("embedding_dimensions"), | |
| ) | |
| def _triple(value) -> tuple[float, float, float] | None: | |
| """A three-channel normalisation constant, or `None`. | |
| Refuses a malformed one rather than passing it through, because a mean of | |
| the wrong length reaches `backbones.preprocess` as a numpy broadcast error | |
| at request time β out of a farmer's capture β instead of at start-up where | |
| the card is being read. | |
| """ | |
| if value is None: | |
| return None | |
| if not isinstance(value, (list, tuple)) or len(value) != 3: | |
| raise ArtefactError( | |
| f"An image_mean or image_std must be three numbers, one per " | |
| f"channel, and this card carries {value!r}." | |
| ) | |
| return tuple(float(v) for v in value) | |
| def _digest(path: Path) -> str: | |
| sha = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| while chunk := handle.read(_DIGEST_CHUNK): | |
| sha.update(chunk) | |
| return sha.hexdigest() | |
| def _enforcing() -> bool: | |
| """Whether a licence problem stops a load right now. | |
| Every licence refusal in `discover()` routes through this, so the posture is | |
| one setting β `ANIMAP_LICENCE_POLICY`, defined in | |
| `app/adapters/licence_policy.py` β and not three independent checks that | |
| have to be found and changed separately. | |
| """ | |
| return licence_policy.current() is licence_policy.LicencePolicy.ENFORCE | |
| def _true_licence(artefact: ModelArtefact) -> str: | |
| """The licence to write in the ledger, which is not always the card's. | |
| When the bytes identify a runtime, the runtime table's licence is the fact | |
| and the card's `license` is a claim about it. Recording the claim would put | |
| `DINOv3 License` beside a runtime of `ultralytics`, which reads as a | |
| contradiction a person has to unpick rather than as a finding. | |
| """ | |
| real = licences.RUNTIME_LICENCES.get(artefact.effective_runtime or "") | |
| return real.licence if real else artefact.license | |
| def _record_identity(artefact: ModelArtefact, what: str) -> bool: | |
| """Handle a disagreement between an artefact's bytes and its card. | |
| Returns True when the load should stop. Under the default policy it never | |
| does β the founder's instruction is that nothing is dropped on a governance | |
| question right now β and what the caller gets instead is a ledger entry | |
| naming the licence the *bytes* arrive under. | |
| **That substitution is the point of the whole check.** Before it, a card | |
| editing `runtime` from `ultralytics` to `dinov3-onnx` produced a load that | |
| was recorded, if it was recorded at all, as a bespoke Meta licence. It is | |
| AGPL-3.0. A ledger that answers *"what did we ship that we should not | |
| have?"* with the wrong licence is worse than no ledger, because somebody | |
| will believe it. | |
| """ | |
| conflict = artefact.identity_conflict | |
| if not conflict: | |
| if artefact.identity is not None and not artefact.identity.identified: | |
| # Not a failure and not a pass. Logged at INFO because a listing of | |
| # every unfingerprinted model is a to-do list, not an incident. | |
| logger.info( | |
| "%s: %s", what, | |
| artefact.identity.detail, | |
| ) | |
| return False | |
| if artefact.observed_runtime is None and artefact.refuted_by: | |
| # **Stops the load under both identity policies, and that is not the | |
| # founder's "drop nothing" instruction being ignored.** `record` mode | |
| # exists so a governance exception stays findable afterwards, and its | |
| # whole value is that the entry it writes is true. Here nothing matched, | |
| # so there is no licence to name: the only honest record would say | |
| # "loaded something we cannot identify under terms we cannot name", | |
| # which is not an exception anyone can act on. Refusing costs one | |
| # capability and a log line that says exactly what to fix. | |
| logger.error("Refusing %s: %s", what, conflict) | |
| return True | |
| real = licences.RUNTIME_LICENCES.get(artefact.observed_runtime or "") | |
| refuse = ( | |
| licence_policy.identity_policy() is licence_policy.IdentityPolicy.REFUSE | |
| ) | |
| if refuse: | |
| logger.error("Refusing %s: %s", what, conflict) | |
| return True | |
| licence_policy.record_exception( | |
| what=what, | |
| # Recorded under what the file *is*, never under what it claimed. | |
| runtime=artefact.observed_runtime or "", | |
| licence=real.licence if real else "UNRECORDED", | |
| reason=conflict, | |
| source_url=real.source_url if real else "", | |
| ) | |
| return False | |
| def discover() -> dict[str, ModelArtefact]: | |
| """Every validated artefact currently present, keyed by capability. | |
| A card that fails its checks is logged and skipped rather than raised β one | |
| bad artefact must not take the whole service down, and the capability it | |
| belongs to simply stays unavailable, which is the honest outcome. | |
| **Every licence decision below reads `effective_runtime`**, which prefers | |
| what the bytes are over what the card says they are. That is the difference | |
| between this and every previous version of the gate. | |
| """ | |
| found: dict[str, ModelArtefact] = {} | |
| if not MODELS_DIR.is_dir(): | |
| return found | |
| for card_path in sorted(MODELS_DIR.glob("*/model_card.json")): | |
| try: | |
| artefact = load_card(card_path) | |
| except (ArtefactError, json.JSONDecodeError) as exc: | |
| logger.error("Ignoring %s: %s", card_path.parent.name, exc) | |
| continue | |
| if not artefact.is_validated: | |
| logger.error( | |
| "Ignoring %s: validation_notes is empty, so nothing attests that " | |
| "this model works.", card_path.parent.name, | |
| ) | |
| continue | |
| if _record_identity( | |
| artefact, f"{card_path.parent.name}/{artefact.model_id}" | |
| ): | |
| continue | |
| if artefact.is_copyleft: | |
| # Refused under the default policy, recorded under `record`. The | |
| # capability going unavailable is noticed; a log line on a Tuesday | |
| # is not (ADR 0017) β which is why `record` mode logs at ERROR and | |
| # keeps the fact rather than merely warning. | |
| message = ( | |
| f"{artefact.model_id} declares licence {artefact.license}, " | |
| f"which Animap may not serve from." | |
| ) | |
| if _enforcing(): | |
| logger.error( | |
| "Refusing %s: %s Move the capability to a permissively " | |
| "licensed artefact.", card_path.parent.name, message, | |
| ) | |
| continue | |
| licence_policy.record_exception( | |
| what=f"{card_path.parent.name}/{artefact.model_id}", | |
| runtime=artefact.effective_runtime or "", licence=artefact.license, | |
| reason=message, | |
| ) | |
| if artefact.uses_disallowed_runtime and _enforcing(): | |
| # The check the licence field cannot make, keyed to what the bytes | |
| # are. A card that declares Apache-2.0 over a path to an AGPL | |
| # checkpoint passes the test above; a card that also rewrites | |
| # `runtime` used to pass this one, and now does not, because | |
| # `uses_disallowed_runtime` reads `effective_runtime`. | |
| logger.error( | |
| "Refusing %s: runtime %r may not be served, whatever the card " | |
| "declares about the licence (ADR 0017). It exists for " | |
| "evaluation/ only.", | |
| card_path.parent.name, artefact.effective_runtime, | |
| ) | |
| continue | |
| if artefact.uses_disallowed_runtime: | |
| licence_policy.record_exception( | |
| what=f"{card_path.parent.name}/{artefact.model_id}", | |
| runtime=artefact.effective_runtime or "", | |
| licence=_true_licence(artefact), | |
| reason="Runtime is on the hard-coded refusal list (ADR 0017).", | |
| ) | |
| # The same idea, generalised and applied to every runtime rather than to | |
| # one name. `licences.gate` refuses three things the checks above do not: | |
| # a runtime nobody has recorded a licence for, a runtime whose real | |
| # licence is one Animap may not serve, and β the new one β a card whose | |
| # declared licence *disagrees* with what that runtime actually loads. | |
| # | |
| # It is handed `effective_runtime`, so on a mislabelled artefact it is | |
| # asked about the model that is really there. That also means the | |
| # card-versus-runtime disagreement it raises on becomes a three-way | |
| # check: the card's licence, the card's runtime, and the bytes. | |
| try: | |
| licences.gate( | |
| artefact.effective_runtime, artefact.license, | |
| what=f"{card_path.parent.name}/{artefact.model_id}", | |
| ) | |
| except licences.LicenceRefused as refusal: | |
| logger.error("Refusing %s: %s", card_path.parent.name, refusal) | |
| continue | |
| found[artefact.capability_key] = artefact | |
| return found | |
| class InferenceProvider: | |
| """Runs a capability, or explains why it cannot. | |
| `run` is deliberately absent from the base class. A provider with a default | |
| implementation would be a provider that can return something without a model, | |
| and that is the one thing this service must not do. | |
| """ | |
| def can_run(self, capability: Capability) -> bool: | |
| raise NotImplementedError | |
| class LocalArtefactProvider(InferenceProvider): | |
| """Serves capabilities backed by a validated artefact on disk. | |
| Two capabilities have one as of ADR 0018 β `cattle_detection` and | |
| `poultry_count`, both YOLOX-m. The other nineteen have none, and `can_run` | |
| returning False for them is the correct state rather than a gap. | |
| """ | |
| def __init__(self, artefacts: dict[str, ModelArtefact] | None = None): | |
| self.artefacts = artefacts if artefacts is not None else discover() | |
| def can_run(self, capability: Capability) -> bool: | |
| artefact = self.artefacts.get(capability.key) | |
| return artefact is not None and artefact.is_validated and capability.is_runnable | |
| def artefact_for(self, capability: Capability) -> ModelArtefact | None: | |
| return self.artefacts.get(capability.key) | |
| class HostedModelProvider(InferenceProvider): | |
| """Serves capabilities whose model is somebody else's, reached over HTTP. | |
| ## Why this is a provider and not a special case in `LocalArtefactProvider` | |
| `can_run` there asks one question β is there a validated file on disk β and | |
| for these sixteen the answer is permanently no, and correctly so. There is | |
| no artefact to checksum because there are no bytes: directive Β§4 asks for a | |
| hosted visual reasoner precisely where no public dataset exists to train | |
| one. Bending the local provider to answer True without a file would make | |
| `is_validated` mean two different things depending on the capability, and | |
| that check is the thing standing between a card somebody filled in and a | |
| model somebody validated. | |
| So the two providers answer for disjoint sets and `main` asks both. | |
| ## The artefact this returns, and why it names the live model | |
| `InferenceResult` requires `model_id` and `model_version`, and the API | |
| refuses a result whose pair does not equal the run's β *"register a release | |
| for the artefact that actually ran"*. A hosted model has no sha256, so what | |
| identifies it is its name, and that is read **at call time** from | |
| `ANIMAP_MULTIMODAL_MODEL` rather than frozen at import. | |
| That has a consequence worth stating plainly: **rotating the model refuses | |
| every queued run of these capabilities until a release is registered for the | |
| new one.** That is the correct behaviour and not a hazard. A different model | |
| is different evidence, `evidence_correction` is a training set built on | |
| knowing which model said what, and a silent swap would put two models' | |
| answers in one column. Re-registering is a management command β neither a | |
| rebuild nor a redeploy β so a rotation still costs a restart and a | |
| `register_capability_release`, exactly as `multimodal.py` intends. | |
| `capability.model_provider` stays the stable string `hosted-multimodal`. It | |
| is what makes `is_runnable` true and what the Android app copies out of | |
| `capabilities.json`, and it must not change when a model does β a device | |
| would otherwise need a rebuild to keep a capability visible. | |
| """ | |
| #: What `sha256` says when there are no bytes to hash. | |
| #: | |
| #: Not an empty string and not a fake digest. A reader of a stored result | |
| #: asking "which file was this" gets an answer that says there was no file, | |
| #: rather than a blank that reads as a missing field or sixty-four zeroes | |
| #: that read as a checksum. | |
| NO_LOCAL_BYTES = "hosted:no-local-artefact" | |
| def __init__(self, keys: frozenset[str] | None = None): | |
| # Imported here rather than at module scope: `reasoning` imports | |
| # `providers` for `ModelArtefact`, and the pair would not load. | |
| if keys is None: | |
| from app.reasoning import RUBRICS | |
| keys = frozenset(RUBRICS) | |
| self.keys = keys | |
| def _configured_model(self) -> str | None: | |
| provider = os.environ.get("ANIMAP_MULTIMODAL_PROVIDER", "").strip() | |
| model = os.environ.get("ANIMAP_MULTIMODAL_MODEL", "").strip() | |
| if not provider or not model: | |
| return None | |
| return f"{provider}/{model}" | |
| def can_run(self, capability: Capability) -> bool: | |
| """True when this capability is hosted, declared runnable, and reachable. | |
| The third clause is the one that keeps `/health` honest. Without a key | |
| the adapter is not ready and every one of these answers `unavailable` | |
| with a reason, which is a deployment that says what it is missing rather | |
| than one that fails sixteen jobs to find out. | |
| """ | |
| from app.adapters.multimodal import HostedMultimodalAdapter | |
| from app.adapters.transports import transport_from_env | |
| from app.reasoning import HOSTED_PROVIDER | |
| if capability.key not in self.keys: | |
| return False | |
| if capability.model_provider != HOSTED_PROVIDER: | |
| return False | |
| if not capability.is_runnable: | |
| return False | |
| if self._configured_model() is None: | |
| return False | |
| # Built the way `adapters/registry.py` builds it, so `/health`'s | |
| # listing and this answer cannot disagree about whether the vendor is | |
| # reachable. A bare adapter has no transport and is never ready. | |
| return HostedMultimodalAdapter( | |
| transport=transport_from_env() | |
| ).availability().ready | |
| def artefact_for(self, capability: Capability) -> ModelArtefact | None: | |
| model = self._configured_model() | |
| if capability.key not in self.keys or model is None: | |
| return None | |
| return ModelArtefact( | |
| model_id=model, | |
| # The rubric's version, not the vendor's. A materially changed | |
| # question is different evidence from the same model, and this is | |
| # what a release row records it as. Bumped by hand in `reasoning`. | |
| version="1", | |
| capability_key=capability.key, | |
| source="hosted", | |
| license="vendor-terms", | |
| sha256=self.NO_LOCAL_BYTES, | |
| validation_notes=( | |
| "No local artefact and no measurement. This capability is " | |
| "answered by a hosted visual reasoner, is `experimental`, and " | |
| "requires human confirmation β see app/reasoning.py." | |
| ), | |
| geography_validated=[UNVALIDATED_GEOGRAPHY], | |
| path=Path("hosted"), | |
| runtime="http", | |
| ) | |
| class CompositeProvider(InferenceProvider): | |
| """Every provider, asked in order, first answer wins. | |
| The two sets are disjoint β one is capabilities with a file, the other is | |
| capabilities that can never have one β so "first answer wins" resolves | |
| nothing today and is a rule rather than a tiebreak. `_OVERLAP` in `main` | |
| refuses a collision on the runner side for the same reason. | |
| """ | |
| def __init__(self, *providers: InferenceProvider): | |
| self.providers = providers | |
| def artefacts(self) -> dict[str, ModelArtefact]: | |
| """Only the ones on disk. `/health` reports this as `artefacts_loaded`, | |
| and a hosted model is not an artefact anybody loaded.""" | |
| merged: dict[str, ModelArtefact] = {} | |
| for provider in self.providers: | |
| merged.update(getattr(provider, "artefacts", {})) | |
| return merged | |
| def can_run(self, capability: Capability) -> bool: | |
| return any(p.can_run(capability) for p in self.providers) | |
| def artefact_for(self, capability: Capability) -> ModelArtefact | None: | |
| for provider in self.providers: | |
| if provider.can_run(capability): | |
| return provider.artefact_for(capability) | |
| for provider in self.providers: | |
| found = provider.artefact_for(capability) | |
| if found is not None: | |
| return found | |
| return None | |