Push HuggingFace models to OCI registries (v1: per-file pipeline, single-PATCH per blob)
Project description
oci-modelcar v1.0
Push HuggingFace models to OCI registries as multi-layer images, suitable for KServe with native OCI image volumes (KEP-4639).
v1.0 uses a per-file pipeline (download → tar → push) with a single PATCH per blob (Jib-style), eliminating per-PATCH routing decisions on Artifactory HA clusters and Harbor reverse-proxy setups.
Why
Pushing a HuggingFace model to an OCI registry typically means:
- Triple-trip: HF -> local cache -> registry
- One huge layer: no cross-repo blob mount possible
- No resume: a 5 GB shard failing at 4.5 GB starts over
oci-modelcar handles this in pure Python:
- Per-file pipeline: download, tar, and push run concurrently per file
- One uncompressed tar layer per file (
digest == diff_id) - Single PATCH per blob — same wire shape as Jib and
containers/image - Parallel workers (
--workers, cap 8)
Documentation
- User guide — complete CLI reference, CI/CD examples, troubleshooting, exit codes
- CHANGELOG — release history, breaking changes
- Design spec: docs/superpowers/specs/2026-05-08-oci-modelcar-v1-design.md
Install
pip install oci-modelcar
# or via uv (recommended for CI)
uv tool install oci-modelcar
Requires Python 3.11+.
Quick start
export HF_TOKEN=hf_...
export OCI_USERNAME=...
export OCI_PASSWORD=...
oci-modelcar push \
--hf-repo Qwen/Qwen2.5-7B-Instruct \
--registry registry.acme.com \
--target-repo models/qwen-7b
The image tag defaults to the first 12 characters of the resolved HF commit
SHA (e.g. a3f47b09c8d2).
Disk space
v1 spools downloaded HF files and built tar layers under --spool-dir
(default $TMPDIR/oci-modelcar). Roughly 2× the largest layer per worker
in flight, plus the cumulative size of all source files unless
--clean-hf-after-push is set. The push aborts up-front with a clear
error if free space is insufficient.
Migration from v0.5
v1.0 is a clean rewrite. Breaking changes:
--state-fileremoved (registry HEAD is the source of truth)--chunk-mibremoved (single PATCH per blob)--upload-moderemoved (one mode)- Default
--oci-max-retrieslowered from 10 to 5 (each retry is a full PATCH replay) - Two new flags:
--spool-dir,--clean-hf-after-push
See CHANGELOG.md for full details.
Authentication
HuggingFace (aligned with huggingface-cli):
HF_TOKENorHUGGING_FACE_HUB_TOKENenv var (recommended)~/.cache/huggingface/token(created byhuggingface-cli login)- Opt-out: set
HF_HUB_DISABLE_IMPLICIT_TOKEN=1to skip implicit token sources
OCI registry:
OCI_USERNAME+OCI_PASSWORDenv vars (recommended for CI)~/.docker/config.json(docker loginwrites here)$XDG_RUNTIME_DIR/containers/auth.json(podman login)
Local registries (hostnames localhost, 127.x.x.x, ::1) automatically
use plain HTTP. Pass an explicit http:// or https:// prefix on
--registry to override.
Common options
| Option | Default | Description |
|---|---|---|
--hf-revision |
main |
Branch, tag, or 40-char SHA |
--target-tag |
<sha[:12]> |
Image tag |
--also-tag |
— | CSV of alias tags |
--workers |
1 |
Parallel layers (cap 8) |
--spool-dir |
$TMPDIR/oci-modelcar |
Directory for downloaded files and built tar layers |
--clean-hf-after-push |
off | Delete each HF file after its layer is pushed |
--oci-max-retries |
5 |
Max PATCH retries per blob (each is a full replay) |
--fail-fast / --continue-on-error |
fail-fast | Failure policy |
--log-style |
auto | text or azure |
--dry-run |
— | List files, don't push |
Full list: oci-modelcar push --help. For complete usage, scenarios, and
troubleshooting see docs/user-guide.md.
Resume after failure / re-push
v1 uses the registry as the source of truth. No local state file is needed.
Two layers of skipping kick in on a re-run:
- Cross-run reuse (no HF traffic). Every layer is annotated with the
HuggingFace path and (for LFS files) the upstream sha256. On every push,
the pipeline GETs the existing manifest at the target tag, indexes its
layers by
(hf-path, hf-sha256), and for each unchanged file skips the HF download + tar build + PATCH entirely — only a HEAD-blob is issued to confirm the layer is still in the registry. A re-push of an unchanged HF revision touches HF for zero bytes. - Same-run blob skip (resume after crash). Even when the reuse map
misses, the worker still HEADs each freshly-built layer digest and skips
the PUT if the registry already has the blob. Combined with the cached
spool sources (
<spool>/sources/<path>is reused if it exists at the expected size), this turns a killed-mid-way push into a near-instant completion on the next run.
# First run, killed mid-way
oci-modelcar push --hf-repo X --registry Y --target-repo Z
# ^C
# Re-run: cached files + cached blobs picked up, only the missing pieces transfer
oci-modelcar push --hf-repo X --registry Y --target-repo Z
--force bypasses both layers (reuse map is not built, HEAD checks are
ignored on blobs and on the manifest tag).
OCI compliance
Compliant with OCI Distribution v1.1 and OCI Image Spec v1.1:
- Single PATCH per blob with upfront
Content-Length(Jib-style); on retry the full PATCH is replayed from the local spool file Content-Range: N-M(inclusive, nobytesprefix per OCI spec)- HEAD validation cross-checks
Docker-Content-Digest - Layers use
application/vnd.oci.image.layer.v1.tar(uncompressed) solayer.digest == diff_idby construction
Signing & verification
oci-modelcar itself does not sign artifacts — signature is delegated to
cosign, the canonical OCI signing tool.
Each push exposes the canonical digest reference for direct piping into
cosign:
oci-modelcar push --hf-repo ... --registry ... --target-repo ...
# IMAGEREFDIGEST=registry.example.com/models/qwen3-30b@sha256:...
# Sign keyless (CI with OIDC, e.g. GitHub Actions with id-token: write)
cosign sign $IMAGEREFDIGEST
# Sign with a static key (offline / regulated environments)
cosign generate-key-pair # one-time, produces cosign.key + cosign.pub
cosign sign --key cosign.key $IMAGEREFDIGEST
# Verify (consumer side, e.g. KServe operator)
cosign verify $IMAGEREFDIGEST \
--certificate-identity-regexp '^https://github\.com/your-org/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
# Or with the static public key
cosign verify --key cosign.pub $IMAGEREFDIGEST
The signature is stored as an additional artifact in the same OCI registry,
attached to the manifest by digest (referrers API for OCI Distribution v1.1+,
or :sha256-<digest>.sig tag for legacy registries — cosign auto-detects).
PyPI artifact
The oci-modelcar PyPI distribution is signed with PEP 740 digital
attestations generated by GitHub Actions in keyless OIDC mode. Verify with:
pip install pypi-attestations
# Replace with the version you want to verify
VERSION=1.0.0
python -m pypi_attestations verify pypi \
--repository https://github.com/codanael/oci-modelcar \
pypi:oci_modelcar-${VERSION}-py3-none-any.whl
python -m pypi_attestations verify pypi \
--repository https://github.com/codanael/oci-modelcar \
pypi:oci_modelcar-${VERSION}.tar.gz
pypi-attestations fetches the artifact and its provenance from PyPI on its
own when you pass pypi:<filename>, so no separate pip download step is
needed. --repository expects the full GitHub/GitLab URL of the publishing
repo; an OK: <filename> line per artifact (exit 0) means the provenance
chains correctly through Sigstore (Fulcio cert + Rekor transparency log).
Releasing (maintainers)
- Bump
versioninpyproject.tomland updateCHANGELOG.md. - Tag:
git tag -a v1.0.0 -m 'release notes' && git push origin v1.0.0. - The
release.ymlworkflow builds, publishes to PyPI via Trusted Publishing, and creates a GitHub Release.
PyPI trusted publisher must be configured once: on pypi.org -> Project Settings -> Publishing -> Add publisher with:
- Owner:
codanael - Repo:
oci-modelcar - Workflow:
release.yml - Environment:
pypi
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file oci_modelcar-1.1.0.tar.gz.
File metadata
- Download URL: oci_modelcar-1.1.0.tar.gz
- Upload date:
- Size: 203.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24da0bf44e57f5bb344d7a4aa20c50ee38951ab26292c691fead6ea59fb6af3c
|
|
| MD5 |
f077f5a2a36e1a1a17c2f67a07f7108b
|
|
| BLAKE2b-256 |
b279a60e2136b6bae08bec675252d97f57c9c674ec5638eeb5d485723defa579
|
Provenance
The following attestation bundles were made for oci_modelcar-1.1.0.tar.gz:
Publisher:
release.yml on codanael/oci-modelcar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oci_modelcar-1.1.0.tar.gz -
Subject digest:
24da0bf44e57f5bb344d7a4aa20c50ee38951ab26292c691fead6ea59fb6af3c - Sigstore transparency entry: 1506094925
- Sigstore integration time:
-
Permalink:
codanael/oci-modelcar@7aeff6472f8803b1f7ec1131ea29b5755a0291c5 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/codanael
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7aeff6472f8803b1f7ec1131ea29b5755a0291c5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oci_modelcar-1.1.0-py3-none-any.whl.
File metadata
- Download URL: oci_modelcar-1.1.0-py3-none-any.whl
- Upload date:
- Size: 30.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4180003f7a82c1fcc6f7210438893a4e451c985c8bf896ec93ec1b0a82fe809f
|
|
| MD5 |
669ce20dfac43fb750099fb195ea8219
|
|
| BLAKE2b-256 |
c44d54beddc53a43914ae28936d031f9b4fd71ba03762070b1fd3ac1fd0b28da
|
Provenance
The following attestation bundles were made for oci_modelcar-1.1.0-py3-none-any.whl:
Publisher:
release.yml on codanael/oci-modelcar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oci_modelcar-1.1.0-py3-none-any.whl -
Subject digest:
4180003f7a82c1fcc6f7210438893a4e451c985c8bf896ec93ec1b0a82fe809f - Sigstore transparency entry: 1506095028
- Sigstore integration time:
-
Permalink:
codanael/oci-modelcar@7aeff6472f8803b1f7ec1131ea29b5755a0291c5 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/codanael
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7aeff6472f8803b1f7ec1131ea29b5755a0291c5 -
Trigger Event:
push
-
Statement type: