Preserve verified Redux model cards across revision upgrades

This commit is contained in:
baketnk committed 2026-10-04 21:16:22 -04:00
1 parent bbbd31fa6d
commit c0176b8595
6 files changed
+113 -6

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
{"path": "config.json", "size": 12988, "sha256": "503c653b2e3bb788adbcb04f5abdee532d958686564081baeed133ff10143f6e"}, {"path": "config.json", "size": 12988, "sha256": "503c653b2e3bb788adbcb04f5abdee532d958686564081baeed133ff10143f6e"},
{"path": "ternary.json", "size": 57970, "sha256": "1221c6d3ce901ffe09c089da758a8db8b76189f80cff41c5afc244fc61e2051d"}, {"path": "ternary.json", "size": 57970, "sha256": "1221c6d3ce901ffe09c089da758a8db8b76189f80cff41c5afc244fc61e2051d"},
{"path": "tokenizer.json", "size": 1159960, "sha256": "bd321b096832a3f270bd3b2a88823957920f1a5c5ada71114a26ea729d0cbe91"}, {"path": "tokenizer.json", "size": 1159960, "sha256": "bd321b096832a3f270bd3b2a88823957920f1a5c5ada71114a26ea729d0cbe91"},
{"path": "README.md", "size": 8832, "sha256": "930055e6bac7d75114e719850ffaffc8d327e73b4295518c38c81399acd63e46"} {"path": "README.md", "size": 8832, "sha256": "930055e6bac7d75114e719850ffaffc8d327e73b4295518c38c81399acd63e46", "compatible": [{"size": 8533, "sha256": "a8b327f983a8b8ff262ff7bead3a791fbed9350632002af8db85ab5cd84cdaa5"}]}
] ]
}, },
"attribution": "Moondream/M87 Labs, Parakeet Redux, derived from NVIDIA Parakeet TDT 0.6B v3; original model card README.md retained with the files.", "attribution": "Moondream/M87 Labs, Parakeet Redux, derived from NVIDIA Parakeet TDT 0.6B v3; original model card README.md retained with the files.",
+5
View File
@@ -232,6 +232,11 @@ back to desktop/cloud inference. The benchmark ran on revision
`fad622f25f303105c20d70e201bcc477c88b620c`, which no longer resolves after the `fad622f25f303105c20d70e201bcc477c88b620c`, which no longer resolves after the
upstream history was rewritten; the weights, config, ternary map and tokenizer at upstream history was rewritten; the weights, config, ternary map and tokenizer at
`2bf1286` are byte-identical to it (same SHA-256), only the model card README differs. `2bf1286` are byte-identical to it (same SHA-256), only the model card README differs.
The manifest explicitly accepts the previous card's pinned size and SHA-256 for
existing installations, so updating FrameYap requires no model download. New
downloads must verify against the current revision's card; unknown or modified
cards are still rejected. The optional per-file `compatible` list contains exact
`size`/`sha256` pairs for previously installed artifacts, never download fallbacks.
The earlier benchmark used this local API (its source belongs to the originating The earlier benchmark used this local API (its source belongs to the originating
repository, not this project): repository, not this project):
+26 -5
View File
@@ -61,6 +61,7 @@ class ModelFile:
path: str path: str
size: int size: int
sha256: str sha256: str
compatible: tuple[tuple[int, str], ...] = ()
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -127,13 +128,28 @@ def _parse(value):
parsed = [] parsed = []
names = set() names = set()
for item in files: for item in files:
_fields(item, ("path", "size", "sha256"), "model file") if not isinstance(item, dict):
raise ManifestError("invalid model file fields")
_fields(item, ("path", "size", "sha256", "compatible") if "compatible" in item
else ("path", "size", "sha256"), "model file")
name = _relative(item["path"], "model file path") name = _relative(item["path"], "model file path")
size = item["size"] size = item["size"]
if name in names or type(size) is not int or not 0 < size <= _MAX_FILE or not isinstance(item["sha256"], str) or not _HASH.fullmatch(item["sha256"]): if name in names or type(size) is not int or not 0 < size <= _MAX_FILE or not isinstance(item["sha256"], str) or not _HASH.fullmatch(item["sha256"]):
raise ManifestError("duplicate or invalid pinned model file") raise ManifestError("duplicate or invalid pinned model file")
names.add(name) names.add(name)
parsed.append(ModelFile(name, size, item["sha256"])) compatible = item.get("compatible", [])
if not isinstance(compatible, list) or len(compatible) > 8:
raise ManifestError("invalid compatible model files")
pins = []
for previous in compatible:
_fields(previous, ("size", "sha256"), "compatible model file")
old_size, old_hash = previous["size"], previous["sha256"]
if (type(old_size) is not int or not 0 < old_size <= _MAX_FILE or
not isinstance(old_hash, str) or not _HASH.fullmatch(old_hash) or
(old_size, old_hash) in [(size, item["sha256"]), *pins]):
raise ManifestError("duplicate or invalid compatible model file")
pins.append((old_size, old_hash))
parsed.append(ModelFile(name, size, item["sha256"], tuple(pins)))
license_info = value["license"] license_info = value["license"]
_fields(license_info, ("id", "text"), "license") _fields(license_info, ("id", "text"), "license")
requirements = value["requirements"] requirements = value["requirements"]
@@ -177,7 +193,11 @@ def load_backends(manifest_dir=DEFAULT_MANIFEST_DIR):
def check_file(directory, item): def check_file(directory, item):
"""Return (reason, filename); reason None means the pinned file verifies.""" """Verify the current pin or an explicitly compatible existing artifact.
Downloaders construct a temporary ModelFile with only the current pin so
newly fetched bytes must match the revision advertised to the user.
"""
root = Path(directory) root = Path(directory)
if not root.is_absolute(): if not root.is_absolute():
return "model_dir_not_absolute", item.path return "model_dir_not_absolute", item.path
@@ -201,13 +221,14 @@ def check_file(directory, item):
before = os.fstat(file_fd) before = os.fstat(file_fd)
if not stat.S_ISREG(before.st_mode): if not stat.S_ISREG(before.st_mode):
return "unsafe_file", item.path return "unsafe_file", item.path
if before.st_size != item.size: pins = ((item.size, item.sha256), *item.compatible)
if before.st_size not in {size for size, _ in pins}:
return "size_mismatch", item.path return "size_mismatch", item.path
digest = hashlib.sha256() digest = hashlib.sha256()
while chunk := os.read(file_fd, 1024 * 1024): while chunk := os.read(file_fd, 1024 * 1024):
digest.update(chunk) digest.update(chunk)
after = os.fstat(file_fd) after = os.fstat(file_fd)
if (before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns) or digest.hexdigest() != item.sha256: if (before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns) or (before.st_size, digest.hexdigest()) not in pins:
return "hash_mismatch", item.path return "hash_mismatch", item.path
return None, None return None, None
finally: finally:
+7
View File
@@ -21,6 +21,9 @@ class BackendTests(unittest.TestCase):
"tokenizer.json": (1159960, "bd321b096832a3f270bd3b2a88823957920f1a5c5ada71114a26ea729d0cbe91"), "tokenizer.json": (1159960, "bd321b096832a3f270bd3b2a88823957920f1a5c5ada71114a26ea729d0cbe91"),
"README.md": (8832, "930055e6bac7d75114e719850ffaffc8d327e73b4295518c38c81399acd63e46"), "README.md": (8832, "930055e6bac7d75114e719850ffaffc8d327e73b4295518c38c81399acd63e46"),
}) })
self.assertEqual(redux.files[-1].compatible,
((8533, "a8b327f983a8b8ff262ff7bead3a791fbed9350632002af8db85ab5cd84cdaa5"),))
self.assertTrue(all(not file.compatible for file in redux.files[:-1]))
self.assertEqual(redux.launcher["protocol"], "frameyap-worker-v1") self.assertEqual(redux.launcher["protocol"], "frameyap-worker-v1")
self.assertEqual(redux.launcher["arguments"], ["--model", "{model_dir}", "--threads", "{threads}", "--clip-dir", "{clip_dir}"]) self.assertEqual(redux.launcher["arguments"], ["--model", "{model_dir}", "--threads", "{threads}", "--clip-dir", "{clip_dir}"])
self.assertIn("CC-BY-4.0", redux.license_id) self.assertIn("CC-BY-4.0", redux.license_id)
@@ -47,6 +50,10 @@ class BackendTests(unittest.TestCase):
lambda f: f["model"]["files"][0].update(path="../secret"), lambda f: f["model"]["files"][0].update(path="../secret"),
lambda f: f["model"]["files"][0].update(size=True), lambda f: f["model"]["files"][0].update(size=True),
lambda f: f["model"]["files"][0].update(sha256="A" * 64), lambda f: f["model"]["files"][0].update(sha256="A" * 64),
lambda f: f["model"]["files"][0].update(compatible="bad"),
lambda f: f["model"]["files"][0].update(compatible=[{"size": True, "sha256": "a" * 64}]),
lambda f: f["model"]["files"][0].update(compatible=[{"size": 3, "sha256": "a" * 64}]),
lambda f: f["model"]["files"][0].update(compatible=[{"size": 4, "sha256": "a" * 64, "path": "other"}]),
lambda f: f["launcher"]["arguments"].append("{unknown}"), lambda f: f["launcher"]["arguments"].append("{unknown}"),
lambda f: f["launcher"].update(path="/bin/sh"), lambda f: f["launcher"].update(path="/bin/sh"),
lambda f: f["launcher"].update(protocol="not-the-wire-protocol"), lambda f: f["launcher"].update(protocol="not-the-wire-protocol"),
+51
View File
@@ -1206,6 +1206,57 @@ with patch.object(module, "check_host"), patch.object(module.urllib.request, "ur
self.assertIn("mismatched", json.loads(output.getvalue())["message"]) self.assertIn("mismatched", json.loads(output.getvalue())["message"])
fetch.assert_not_called() fetch.assert_not_called()
def test_model_card_upgrade_retains_verified_legacy_without_network(self):
shutil.copyfile(REPO / "python/frameyap/model_files.py",
self.stage / "python/frameyap/model_files.py")
manifest = json.loads((REPO / "assets/backends/redux.json").read_text())
old, new = b"previous attributed card", b"updated attributed model card"
def pin(content):
return {"size": len(content), "sha256": hashlib.sha256(content).hexdigest()}
manifest["model"]["files"] = [{"path": "README.md", **pin(old)}]
manifests = self.stage / "assets/backends"
manifests.mkdir()
card_manifest = manifests / "redux.json"
card_manifest.write_text(json.dumps(manifest))
archive, digest = self.package("0.1.202609241530")
self.install("0.1.202609241530", archive, digest, "--without-model")
destination = self.base / "models"
destination.mkdir()
card = destination / "README.md"
card.write_bytes(old)
manifest["model"]["files"] = [{"path": "README.md", **pin(new),
"compatible": [pin(old)]}]
card_manifest.write_text(json.dumps(manifest))
archive, digest = self.package("0.1.202609241531")
with patch.object(installer.urllib.request, "urlopen") as fetch:
self.install("0.1.202609241531", archive, digest, "--without-model")
module, backend, _ = installer.installed_backend(self.data / "frameyap", "redux")
self.assertEqual(module.check_model(backend, destination)["state"], "installed_verified")
argv = ["--install-model", "--model-dir", str(destination), "--yes", "--json"]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(installer.cli(argv), 0)
fetch.assert_not_called()
self.assertEqual(card.read_bytes(), old)
card.write_bytes(b"x" * len(old))
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(installer.cli(argv), 1)
fetch.assert_not_called()
card.unlink()
class Response(io.BytesIO):
def geturl(self):
return "https://huggingface.co/fixture"
# Fresh downloads require the current card even though the old one is
# permitted for already installed, independently hashed model files.
with patch.object(installer.urllib.request, "urlopen", return_value=Response(old)):
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(installer.cli(argv), 1)
self.assertFalse(card.exists())
with patch.object(installer.urllib.request, "urlopen", return_value=Response(new)) as fetch:
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(installer.cli(argv), 0)
fetch.assert_called_once()
self.assertEqual(card.read_bytes(), new)
def test_runtime_install_is_explicit_cpu_pinned_and_updates_only_python_path(self): def test_runtime_install_is_explicit_cpu_pinned_and_updates_only_python_path(self):
# subprocess is mocked: no venv, pip or network is touched. # subprocess is mocked: no venv, pip or network is touched.
root = self.data / "frameyap" root = self.data / "frameyap"
+23
View File
@@ -74,6 +74,29 @@ class ModelFileTests(unittest.TestCase):
(model / "nested").symlink_to(root) (model / "nested").symlink_to(root)
self.assertEqual(check_model(backend, model)["reason"], "unsafe_file") self.assertEqual(check_model(backend, model)["reason"], "unsafe_file")
def test_compatible_card_still_requires_exact_hash_and_safe_file(self):
with tempfile.TemporaryDirectory() as path:
root = Path(path)
_, backend = self.fixture(root)
current, previous = b"current card", b"old card"
item = ModelFile("README.md", len(current), hashlib.sha256(current).hexdigest(),
((len(previous), hashlib.sha256(previous).hexdigest()),))
backend = replace(backend, files=(item,))
card = root / "README.md"
for content in (current, previous):
card.write_bytes(content)
self.assertEqual(check_model(backend, root)["state"], "installed_verified")
# A download temp pin must never accept an older artifact.
self.assertEqual(check_file(root, ModelFile(item.path, item.size, item.sha256))[0],
"size_mismatch")
card.write_bytes(b"bad card")
self.assertEqual(check_model(backend, root)["reason"], "hash_mismatch")
card.unlink()
outside = root / "outside"
outside.write_bytes(previous)
card.symlink_to(outside)
self.assertEqual(check_model(backend, root)["reason"], "unsafe_file")
def test_second_backend_status_without_runtime_changes(self): def test_second_backend_status_without_runtime_changes(self):
with tempfile.TemporaryDirectory() as path: with tempfile.TemporaryDirectory() as path:
root = Path(path) root = Path(path)