Download distinct_agent/weights.py from User1342/distinct: direct link, hf CLI and curl.
- Browser
- Download file 27.5 kB
-
https://huggingface.co/spaces/User1342/distinct/resolve/main/distinct_agent/weights.py
- Command line
-
hf download hf://spaces/User1342/distinct/distinct_agent/weights.py
-
curl -L -o weights.py https://huggingface.co/spaces/User1342/distinct/resolve/main/distinct_agent/weights.py
27.5 kB
| """Fetch model weights on demand from Hugging Face, into a capped cache. | |
| Why this module replaced ``--model ID=PATH`` | |
| -------------------------------------------- | |
| Agent setup used to hand the worker a filesystem path to a GGUF file the | |
| operator had already downloaded by some route the worker never saw. That is | |
| backwards on two counts. | |
| **It makes the operator the integration.** The server publishes a catalogue of | |
| models it may ask an agent to run. The operator's decision is which of those | |
| they are willing to host on their hardware. Turning that decision into "find | |
| the right file, put it somewhere, and type its path correctly" is asking a | |
| volunteer to do a build step, and every step of it can go wrong silently: a | |
| requantisation from a different publisher has the same filename and different | |
| weights. | |
| **It makes identity unverifiable in practice.** A digest is only meaningful | |
| against a repository and a revision. A path says nothing about where the file | |
| came from, so a path-configured worker could only ever report "no digest | |
| recorded", which is the state this project treats as an unanswered question | |
| rather than a passing check. | |
| So the flow is now: the operator names *models*, never paths. The worker | |
| resolves each to the pinned repository, revision and filename, downloads it | |
| when a run first needs it, verifies it against the published digest before it | |
| is ever loaded, and keeps it in a cache whose size the operator sets at run. | |
| The rule that follows, and it is deliberate | |
| ------------------------------------------- | |
| **A model that cannot be fetched this way is not offered.** Fetchable means all | |
| three of repository, revision and digest are recorded. A manifest missing any | |
| of them stays in the catalogue as a documented absence and is never advertised, | |
| because the alternatives are worse: downloading from a repository with no | |
| pinned revision fetches whatever is at the head of a branch today, and | |
| verifying nothing means the worker cannot say what it ran. | |
| What this module never does | |
| --------------------------- | |
| * It never substitutes. If the pinned file cannot be obtained or does not match | |
| its digest, the run fails naming the model. That is the no-fallback rule. | |
| * It never writes a file into place before verifying it. Downloads land in a | |
| temporary part-file, are hashed as they are written, and are renamed only | |
| after the digest matches. | |
| * It never evicts to make room for something that would not fit anyway. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import shutil | |
| import threading | |
| import time | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from collections.abc import Callable, Iterable, Mapping, Sequence | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any, Optional | |
| from distinct_protocol import ModelManifest | |
| from .models import ( | |
| KNOWN_MODEL_MANIFESTS, | |
| DiscoveredModel, | |
| ModelVerificationError, | |
| sha256_file, | |
| ) | |
| #: The only host this module will fetch from. Not configurable: a worker that | |
| #: could be pointed at an arbitrary origin by configuration is a worker whose | |
| #: weights provenance is decided by whoever wrote the configuration. | |
| WEIGHTS_HOST = "huggingface.co" | |
| #: Where a weights download may be redirected to, and nowhere else. | |
| #: | |
| #: Hugging Face answers a ``/resolve/`` URL with a redirect to its CDN, so | |
| #: refusing redirects outright would refuse every download. Following them | |
| #: without checking was the other extreme, and it was a blind request | |
| #: primitive: whoever could answer that first response could point this at any | |
| #: host and any scheme, including a cloud metadata service on the volunteer's | |
| #: own network. The pinned digest stops a substituted file; it does not stop | |
| #: the request being made, and the request is the finding. | |
| #: | |
| #: So redirects are followed, to https only, and only to these hosts or a | |
| #: subdomain of one. | |
| WEIGHTS_REDIRECT_HOSTS = frozenset({"huggingface.co", "hf.co", "cdn-lfs.huggingface.co", "cdn-lfs.hf.co", "cdn-lfs-us-1.hf.co", "cdn-lfs-eu-1.hf.co", "xet.huggingface.co", "transfer.xethub.hf.co", "cas-bridge.xethub.hf.co", "cas-server.xethub.hf.co"}) | |
| class WeightsRedirects(urllib.request.HTTPRedirectHandler): | |
| """Follow a redirect only to Hugging Face, only over TLS. | |
| A redirect anywhere else is refused with a sentence rather than followed, | |
| because a download that has been bounced to another host is no longer the | |
| download the operator asked for even when the digest would have caught a | |
| substitution. | |
| """ | |
| #: A legitimate chain is one or two hops. Ten is generous and finite. | |
| max_redirections = 10 | |
| def _permitted(url: str) -> bool: | |
| parsed = urllib.parse.urlsplit(url) | |
| if parsed.scheme != "https": | |
| return False | |
| host = (parsed.hostname or "").casefold().rstrip(".") | |
| if not host: | |
| return False | |
| return any( | |
| host == allowed or host.endswith("." + allowed) | |
| for allowed in WEIGHTS_REDIRECT_HOSTS | |
| ) | |
| def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D102 | |
| if not self._permitted(newurl): | |
| raise urllib.error.HTTPError( | |
| newurl, | |
| code, | |
| "the weights download was redirected off Hugging Face and was refused", | |
| headers, | |
| fp, | |
| ) | |
| return super().redirect_request(req, fp, code, msg, headers, newurl) | |
| def _weights_opener() -> Callable[..., Any]: | |
| """The default fetcher: redirects allowed, but only to the expected place.""" | |
| return urllib.request.build_opener(WeightsRedirects).open | |
| #: Default cache ceiling. Small enough to be a polite default on a volunteer's | |
| #: machine, large enough to hold the models in the shipped catalogue. | |
| #: The cache ceiling, in gigabytes. | |
| #: | |
| #: A DEFAULT SMALLER THAN THE CATALOGUE IS A DEFAULT THAT THRASHES. | |
| #: | |
| #: This was 8, and the two models this project ships come to about nine | |
| #: together. So a worker offering both downloaded the first, evicted it to fit | |
| #: the second, and re-downloaded it on the next start — five gigabytes over | |
| #: the wire, every run, for ever, on a project whose entire subject is the | |
| #: energy cost of running models. Nothing reported it, because eviction was | |
| #: silent and the usage line only counted the models that run offered. | |
| #: | |
| #: Sized to hold the shipped catalogue with room for one more, so the common | |
| #: case never evicts anything. An operator with a small disk lowers it and is | |
| #: told what that costs. | |
| DEFAULT_CACHE_GB = 16.0 | |
| #: Anything above this is almost certainly a typo rather than an intention. | |
| MAX_CACHE_GB = 512.0 | |
| _CHUNK = 1024 * 1024 | |
| def shared_cache_directory(environ: Mapping[str, str] | None = None) -> Path: | |
| """Where weights live so that everything on the machine can share them. | |
| A model file is hundreds of megabytes and is not specific to this project. | |
| Putting the cache inside the checkout meant a second agent, a second clone, | |
| or any other tool on the same machine downloaded its own copy of the same | |
| bytes: wasteful on disk, wasteful on bandwidth, and a strange thing for a | |
| project about energy to make people do. | |
| Resolution order, most specific first: | |
| 1. ``DISTINCT_MODELS_DIR``, for an operator who wants to say exactly where. | |
| 2. ``HF_HOME``/``HUGGINGFACE_HUB_CACHE``, because a machine that already | |
| has a Hugging Face cache has already answered this question, and a | |
| model fetched by anything else is a model this worker should reuse. | |
| 3. The platform's own user cache: ``%LOCALAPPDATA%`` on Windows, | |
| ``XDG_CACHE_HOME`` or ``~/.cache`` elsewhere. | |
| ``--cache-dir`` still overrides all of it. This only decides the default. | |
| """ | |
| values = os.environ if environ is None else environ | |
| explicit = values.get("DISTINCT_MODELS_DIR", "").strip() | |
| if explicit: | |
| return Path(explicit).expanduser() | |
| for name in ("HUGGINGFACE_HUB_CACHE", "HF_HOME"): | |
| value = values.get(name, "").strip() | |
| if value: | |
| return Path(value).expanduser() / "distinct-models" | |
| if os.name == "nt": | |
| base = values.get("LOCALAPPDATA", "").strip() | |
| root = Path(base).expanduser() if base else Path.home() / "AppData" / "Local" | |
| return root / "distinct" / "models" | |
| base = values.get("XDG_CACHE_HOME", "").strip() | |
| root = Path(base).expanduser() if base else Path.home() / ".cache" | |
| return root / "distinct" / "models" | |
| class WeightsUnavailable(RuntimeError): | |
| """The pinned weights could not be obtained, and nothing was substituted.""" | |
| class CacheTooSmall(WeightsUnavailable): | |
| """The model does not fit in the cache the operator configured. | |
| Kept separate because the remedy is different: this is not a network | |
| problem and retrying will not fix it. The operator raises the cap or | |
| stops offering the model. | |
| """ | |
| def is_fetchable(manifest: ModelManifest) -> bool: | |
| """Can this manifest be obtained and verified without a human in the loop?""" | |
| return bool(manifest.source_repo and manifest.source_revision and manifest.digest_recorded) | |
| def unfetchable_reason(manifest: ModelManifest) -> str: | |
| """Why a manifest is not offered. Named parts, so it can be fixed.""" | |
| missing = [] | |
| if not manifest.source_repo: | |
| missing.append("upstream repository") | |
| if not manifest.source_revision: | |
| missing.append("pinned revision") | |
| if not manifest.digest_recorded: | |
| missing.append("SHA-256 digest") | |
| if not missing: | |
| return "" | |
| if len(missing) == 1: | |
| parts = missing[0] | |
| else: | |
| parts = f"{', '.join(missing[:-1])} or {missing[-1]}" | |
| return ( | |
| f"{manifest.id}: no {parts} recorded, so this worker cannot obtain the " | |
| "exact published file or confirm what it ran" | |
| ) | |
| def fetchable_manifests( | |
| manifests: Sequence[ModelManifest] = KNOWN_MODEL_MANIFESTS, | |
| ) -> tuple[ModelManifest, ...]: | |
| """The models a worker may offer. Everything else is a documented absence.""" | |
| return tuple(manifest for manifest in manifests if is_fetchable(manifest)) | |
| def weights_url(manifest: ModelManifest) -> str: | |
| """The exact revision-pinned download URL for one manifest. | |
| ``/resolve/<revision>/<filename>`` rather than ``/resolve/main/...``: main | |
| moves, and a digest pinned against a moving reference is a digest pinned | |
| against nothing. | |
| """ | |
| if not is_fetchable(manifest): | |
| raise WeightsUnavailable(unfetchable_reason(manifest)) | |
| quoted = urllib.parse.quote(manifest.filename) | |
| return ( | |
| f"https://{WEIGHTS_HOST}/{manifest.source_repo}" | |
| f"/resolve/{manifest.source_revision}/{quoted}?download=true" | |
| ) | |
| class CacheUsage: | |
| used_bytes: int | |
| limit_bytes: int | |
| model_ids: tuple[str, ...] | |
| def free_bytes(self) -> int: | |
| return max(0, self.limit_bytes - self.used_bytes) | |
| class WeightsCache: | |
| """A size-capped directory of verified weights, filled on demand. | |
| ``limit_bytes`` is a ceiling on the *cache*, not on one model. A model | |
| larger than the ceiling is refused outright rather than being allowed to | |
| evict every other model and then still not fit. | |
| """ | |
| def __init__( | |
| self, | |
| directory: Path | str, | |
| *, | |
| limit_bytes: int, | |
| manifests: Sequence[ModelManifest] = KNOWN_MODEL_MANIFESTS, | |
| opener: Optional[Callable[..., Any]] = None, | |
| clock: Callable[[], float] = time.time, | |
| ) -> None: | |
| if isinstance(limit_bytes, bool) or not isinstance(limit_bytes, int): | |
| raise ValueError("limit_bytes must be an integer") | |
| if limit_bytes < 1: | |
| raise ValueError("limit_bytes must be positive") | |
| self.directory = Path(directory).expanduser() | |
| self.limit_bytes = limit_bytes | |
| self.manifests = {manifest.id: manifest for manifest in manifests} | |
| self._opener = opener if opener is not None else _weights_opener() | |
| self._clock = clock | |
| self._lock = threading.RLock() | |
| # Inspection --------------------------------------------------------- | |
| def path_for(self, manifest: ModelManifest) -> Path: | |
| """Where this model lives when cached. | |
| Keyed by model id, not by filename: two manifests can legitimately | |
| share a filename across publishers, and the id is what the run asked | |
| for. | |
| """ | |
| return self.directory / manifest.id / manifest.filename | |
| def is_cached(self, manifest: ModelManifest) -> bool: | |
| path = self.path_for(manifest) | |
| try: | |
| return path.is_file() and path.stat().st_size > 0 | |
| except OSError: | |
| return False | |
| def adopt_loose_file(self, manifest: ModelManifest) -> bool: | |
| """Take over a correctly named file sitting loose in the cache root. | |
| Weights used to be placed by hand, typically as | |
| ``models/Qwen3-0.6B-Q4_K_M.gguf``; the cache keys them by model id, so | |
| the same file now belongs at ``models/qwen3-0.6b/...``. Without this, | |
| every existing installation would re-download several hundred megabytes | |
| of a file it already has, which is a poor way to repay someone for | |
| lending their machine, and a strange thing for a project about energy | |
| to do. | |
| The file is **verified before it is adopted**, not after and not | |
| instead. A loose file that does not match the pinned digest is left | |
| exactly where it is and the normal download proceeds: this is a | |
| convenience, and a convenience is never allowed to become a way for an | |
| unverified file to enter the cache. | |
| """ | |
| if not manifest.digest_recorded or self.is_cached(manifest): | |
| return False | |
| for candidate in self._loose_candidates(manifest): | |
| try: | |
| if not candidate.is_file() or candidate.stat().st_size <= 0: | |
| continue | |
| if sha256_file(candidate) != manifest.sha256: | |
| continue | |
| target = self.path_for(manifest) | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| candidate.replace(target) | |
| except OSError: | |
| continue | |
| return True | |
| return False | |
| def _loose_candidates(self, manifest: ModelManifest) -> list[Path]: | |
| """Places a copy of this file may already be sitting. | |
| The cache root first, then any directory an operator listed in | |
| ``DISTINCT_EXTRA_MODEL_DIRS``. Each is still verified against the | |
| pinned digest before it is adopted, so a wrong file in a shared | |
| location is skipped rather than trusted. | |
| """ | |
| names = {manifest.filename, manifest.filename.lower()} | |
| roots = [self.directory] | |
| extra = os.environ.get("DISTINCT_EXTRA_MODEL_DIRS", "") | |
| roots += [ | |
| Path(part.strip()).expanduser() | |
| for part in extra.split(os.pathsep) | |
| if part.strip() | |
| ] | |
| return [root / name for root in roots for name in names] | |
| def _cached_entries(self) -> list[tuple[str, Path, int]]: | |
| """Everything actually in the cache, as ``(model_id, path, bytes)``. | |
| THE CACHE IS ON DISK, NOT IN THIS RUN'S ARGUMENTS. | |
| This used to be derived from ``self.manifests``, which is only the | |
| models the current invocation offers. A worker started with | |
| ``--models olmoe`` therefore reported an empty cache while several | |
| gigabytes of another model sat right there in it — the operator was | |
| told "0.00 GB used, ready: none" about a directory that was nearly | |
| full — and the eviction arithmetic below inherited the same blind | |
| spot, so it believed there was room it did not have. | |
| """ | |
| entries: list[tuple[str, Path, int]] = [] | |
| try: | |
| folders = sorted(self.directory.iterdir()) | |
| except OSError: | |
| return entries | |
| for folder in folders: | |
| if not folder.is_dir(): | |
| continue | |
| try: | |
| for path in folder.iterdir(): | |
| # `.part` files are downloads in flight; they occupy space | |
| # but they are not a cached model and must never be counted | |
| # as one, or an interrupted fetch looks like a hit. | |
| if not path.is_file() or path.suffix == ".part": | |
| continue | |
| entries.append((folder.name, path, path.stat().st_size)) | |
| except OSError: | |
| continue | |
| return entries | |
| def usage(self) -> CacheUsage: | |
| entries = self._cached_entries() | |
| used = sum(size for _, _, size in entries) | |
| present = sorted({model_id for model_id, _, size in entries if size > 0}) | |
| return CacheUsage(used, self.limit_bytes, tuple(present)) | |
| def installed(self) -> tuple[DiscoveredModel, ...]: | |
| """Cached models, each verified against its digest. | |
| Verification happens here rather than at load time so that a corrupted | |
| or replaced cache entry is found when the worker starts, not when a | |
| user is waiting for an answer. | |
| """ | |
| found: list[DiscoveredModel] = [] | |
| for manifest in self.manifests.values(): | |
| self.adopt_loose_file(manifest) | |
| path = self.path_for(manifest) | |
| try: | |
| if not path.is_file(): | |
| continue | |
| size = path.stat().st_size | |
| except OSError: | |
| continue | |
| if size <= 0: | |
| continue | |
| if manifest.digest_recorded and sha256_file(path) != manifest.sha256: | |
| raise ModelVerificationError( | |
| f"cached weights for {manifest.id!r} do not match the pinned " | |
| f"SHA-256; delete {path} and let the worker fetch it again" | |
| ) | |
| found.append(DiscoveredModel(manifest, path, size, manifest.digest_recorded)) | |
| return tuple(found) | |
| # Acquisition -------------------------------------------------------- | |
| def ensure( | |
| self, | |
| manifest: ModelManifest, | |
| *, | |
| progress: Callable[[float, str], None] | None = None, | |
| cancel_event: threading.Event | None = None, | |
| ) -> DiscoveredModel: | |
| """Return the verified local file for ``manifest``, fetching if needed.""" | |
| if not is_fetchable(manifest): | |
| raise WeightsUnavailable(unfetchable_reason(manifest)) | |
| with self._lock: | |
| self.adopt_loose_file(manifest) | |
| path = self.path_for(manifest) | |
| if self.is_cached(manifest): | |
| actual = sha256_file(path) | |
| if actual == manifest.sha256: | |
| self._touch(path) | |
| return DiscoveredModel(manifest, path, path.stat().st_size, True) | |
| # A cached file that no longer matches is not repaired quietly. | |
| # It is removed and re-fetched, and the operator is told. | |
| path.unlink(missing_ok=True) | |
| return self._download(manifest, progress=progress, cancel_event=cancel_event) | |
| def _download( | |
| self, | |
| manifest: ModelManifest, | |
| *, | |
| progress: Callable[[float, str], None] | None, | |
| cancel_event: threading.Event | None, | |
| ) -> DiscoveredModel: | |
| url = weights_url(manifest) | |
| target = self.path_for(manifest) | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| part = target.with_suffix(target.suffix + ".part") | |
| part.unlink(missing_ok=True) | |
| import hashlib | |
| digest = hashlib.sha256() | |
| written = 0 | |
| try: | |
| request = urllib.request.Request( | |
| url, | |
| headers={"User-Agent": "distinct-agent", "Accept": "application/octet-stream"}, | |
| ) | |
| with self._opener(request, timeout=60) as response: | |
| declared = _content_length(response) | |
| if declared: | |
| evicted = self._make_room(declared, keep=manifest.id) | |
| if evicted and progress is not None: | |
| # Deleting gigabytes somebody already waited for is | |
| # not a detail. Silent, it looks like the download | |
| # simply never persisted, which is exactly how it was | |
| # reported to us. | |
| progress( | |
| 0.0, | |
| "Evicted from the weights cache to make room: " | |
| + ", ".join(evicted) | |
| + f" — raise --model-cache-gb above {_gb(self.limit_bytes):.0f}" | |
| " to keep them", | |
| ) | |
| with part.open("wb") as handle: | |
| while True: | |
| if cancel_event is not None and cancel_event.is_set(): | |
| raise WeightsUnavailable( | |
| f"weights download for {manifest.id!r} was cancelled" | |
| ) | |
| chunk = response.read(_CHUNK) | |
| if not chunk: | |
| break | |
| handle.write(chunk) | |
| digest.update(chunk) | |
| written += len(chunk) | |
| if written > self.limit_bytes: | |
| raise CacheTooSmall( | |
| f"{manifest.id!r} exceeds the {_gb(self.limit_bytes):.1f} GB " | |
| "weights cache; raise --model-cache-gb or stop offering " | |
| "this model" | |
| ) | |
| if progress is not None and declared: | |
| progress( | |
| min(0.99, written / declared), | |
| f"Fetching {manifest.id} weights", | |
| ) | |
| except (OSError, urllib.error.URLError, ValueError) as exc: | |
| part.unlink(missing_ok=True) | |
| raise WeightsUnavailable( | |
| f"could not fetch {manifest.id!r} from {WEIGHTS_HOST}: " | |
| f"{type(exc).__name__}: {exc}" | |
| ) from exc | |
| except BaseException: | |
| part.unlink(missing_ok=True) | |
| raise | |
| actual = digest.hexdigest() | |
| if actual != manifest.sha256: | |
| part.unlink(missing_ok=True) | |
| raise ModelVerificationError( | |
| f"downloaded weights for {manifest.id!r} do not match the pinned " | |
| f"SHA-256: expected {manifest.sha256}, computed {actual}. Nothing " | |
| "was installed and no other model was substituted." | |
| ) | |
| # Only now does the file take its real name, so a partial or wrong | |
| # download can never be found by discovery. | |
| part.replace(target) | |
| self._touch(target) | |
| if progress is not None: | |
| progress(1.0, f"{manifest.id} weights verified") | |
| return DiscoveredModel(manifest, target, target.stat().st_size, True) | |
| # Eviction ----------------------------------------------------------- | |
| def _make_room(self, needed_bytes: int, *, keep: str) -> list[str]: | |
| """Evict least-recently-used models until ``needed_bytes`` fits. | |
| Returns the ids evicted, so the caller can say what happened. Eviction | |
| is a real cost: the next run that wants an evicted model pays the | |
| download again, and a worker that silently churned its cache would | |
| look slow for no visible reason. | |
| """ | |
| if needed_bytes > self.limit_bytes: | |
| raise CacheTooSmall( | |
| f"{_gb(needed_bytes):.1f} GB of weights cannot fit in a " | |
| f"{_gb(self.limit_bytes):.1f} GB cache" | |
| ) | |
| evicted: list[str] = [] | |
| while True: | |
| usage = self.usage() | |
| if usage.used_bytes + needed_bytes <= self.limit_bytes: | |
| return evicted | |
| # Any model in the cache is a candidate, not only the ones this | |
| # run happens to offer: they take up the same disk either way, and | |
| # a run that could not see them could not free them. | |
| candidates = [ | |
| (self._last_used(path), model_id) | |
| for model_id, path, _ in self._cached_entries() | |
| if model_id != keep | |
| ] | |
| if not candidates: | |
| raise CacheTooSmall( | |
| f"the weights cache is full at {_gb(self.limit_bytes):.1f} GB and " | |
| "nothing else can be evicted to make room" | |
| ) | |
| candidates.sort() | |
| _, victim = candidates[0] | |
| self.evict(victim) | |
| evicted.append(victim) | |
| def evict(self, model_id: str) -> bool: | |
| """Remove one cached model. Returns whether anything was removed.""" | |
| manifest = self.manifests.get(model_id) | |
| if manifest is None: | |
| return False | |
| path = self.path_for(manifest) | |
| directory = path.parent | |
| removed = False | |
| try: | |
| if path.is_file(): | |
| path.unlink() | |
| removed = True | |
| if directory.is_dir() and not any(directory.iterdir()): | |
| shutil.rmtree(directory, ignore_errors=True) | |
| except OSError: | |
| return removed | |
| return removed | |
| def _touch(self, path: Path) -> None: | |
| """Record use, so eviction can be least-recently-used rather than random.""" | |
| try: | |
| now = self._clock() | |
| os.utime(path, (now, now)) | |
| except OSError: | |
| pass | |
| def _last_used(self, path: Path) -> float: | |
| try: | |
| return path.stat().st_mtime | |
| except OSError: | |
| return 0.0 | |
| def _content_length(response: Any) -> int: | |
| for getter in ("headers", "info"): | |
| source = getattr(response, getter, None) | |
| if source is None: | |
| continue | |
| headers = source() if callable(source) else source | |
| try: | |
| value = headers.get("Content-Length") | |
| except AttributeError: | |
| continue | |
| try: | |
| return max(0, int(value)) | |
| except (TypeError, ValueError): | |
| continue | |
| return 0 | |
| def _gb(value: int) -> float: | |
| return value / (1024**3) | |
| def cache_bytes(gigabytes: float) -> int: | |
| if not isinstance(gigabytes, (int, float)) or isinstance(gigabytes, bool): | |
| raise ValueError("cache size must be a number of gigabytes") | |
| if not 0.1 <= float(gigabytes) <= MAX_CACHE_GB: | |
| raise ValueError(f"cache size must be between 0.1 and {MAX_CACHE_GB:g} GB") | |
| return int(float(gigabytes) * (1024**3)) | |
| def describe_catalogue( | |
| manifests: Iterable[ModelManifest] = KNOWN_MODEL_MANIFESTS, | |
| ) -> tuple[list[str], list[str]]: | |
| """``(offerable, withheld)`` lines for the setup console. | |
| The withheld list is printed rather than filtered away in silence. An | |
| operator who wonders why a model they expected is missing should be able to | |
| read the reason on the same screen. | |
| """ | |
| offerable: list[str] = [] | |
| withheld: list[str] = [] | |
| for manifest in manifests: | |
| if is_fetchable(manifest): | |
| offerable.append(f"{manifest.id} {manifest.label}") | |
| else: | |
| withheld.append(unfetchable_reason(manifest)) | |
| return offerable, withheld | |