Skip to content

feat(sdk)!: pull OCI registry models via llmman - #1412

Open
Eric Curtin (ericcurtin) wants to merge 1 commit into
qualcomm:mainfrom
ericcurtin:feat/llmman-oci-pull
Open

feat(sdk)!: pull OCI registry models via llmman#1412
Eric Curtin (ericcurtin) wants to merge 1 commit into
qualcomm:mainfrom
ericcurtin:feat/llmman-oci-pull

Conversation

@ericcurtin

Copy link
Copy Markdown
Contributor

Summary

Replaces the Docker Hub source added in #1165 with one that delegates
OCI-registry acquisition to llmman,
talking to a running llmman serve daemon.

#1165 landed a 797-line hand-rolled Docker Registry HTTP API V2 client
(anonymous token exchange, content-negotiated manifest GET, config blob
GET, both application/vnd.docker.ai.* layouts). That is registry
protocol code GenieX has no particular interest in owning, and it stopped
at Docker Hub — GHCR, quay, ECR, and self-hosted registries were all out
of scope, as was CNCF ModelPack (explicitly rejected with an error).

llmman already implements all of it, plus content-addressed resume. So:

  • sdk/model-manager/crates/core/src/source/llmman.rs (new, replaces
    dockerhub.rs): a ModelSource that probes GET /api/version, streams
    POST /api/pull (NDJSON), then shells to llmman resolve --no-pull for
    the resulting on-disk path and emits FileSpec { BytesSource::LocalLink }.
  • GENIEX_HUB_DOCKERGENIEX_HUB_LLMMAN, keeping value 5. No symbol
    added or removed, so the CMake export list and the whole FFI table are
    untouched.
  • mapping.rs: is_docker_hub_referenceis_llmman_reference,
    broadened to ghcr.io/, quay.io/, gcr.io/, mcr.microsoft.com/,
    public.ecr.aws/, and an oci:// scheme. New
    llmman_store_name_from_reference derives the org/repo store path,
    and reference_has_tag keeps a registry port from being read as a tag.
  • Bindings: Go HubDockerHubLlmman, Python 'docker''llmman'/'oci',
    Kotlin DOCKER(5)LLMMAN(5), per the FFI-update rule.

Two design points worth review

1. plan() now moves bytes for this one source. llmman does the
downloading, so by the time we can name the files they have already landed.
Rather than let a multi-gigabyte pull run with no progress bar, ModelSource
gains a provided method plan_with_progress(Option<&ProgressCallback>)
that defaults to plan(). Only LlmmanSource overrides it; no other source
or test changed. llmman's aggregate total/completed map onto
geniex_FileProgress, and returning false still cancels.

2. Path resolution goes through the CLI, not HTTP. llmman serve
deliberately exposes no local path — /api/show returns only a digest and
size, and /props hardcodes "model_path": "". llmman resolve is the
documented interface for this (it's what llmman's own vLLM plugin uses), runs
in-process, and --no-pull guarantees it only reports on bytes /api/pull
already fetched. So an llmman pull needs both the daemon reachable and the
binary on PATH (or GENIEX_LLMMAN_BIN); each missing piece has its own
actionable error.

New BytesSource::LocalLink hard-links (falling back to copy) so a model
shared with llmman's store costs its bytes once rather than twice. It is
deliberately not used for LocalFsSource, whose source is a user-owned
directory they may edit in place.

Also fixed along the way: routing a bare name through
canonicalize_model_name would have stored geniex pull gemma3 --model-hub llmman under qualcomm/gemma3, colliding with the real AI Hub model. It now
uses ai/, matching llmman's own bare-name expansion.

geniex_model_query returns an error for this hub — answering it would mean
performing the entire download, and a registry tag names one artifact rather
than a set of GGUF quantizations.

Try it

llmman serve &
geniex pull docker.io/ai/gemma3
geniex infer docker.io/ai/gemma3
geniex pull ghcr.io/org/model:v1
geniex pull ai/gemma3 --model-hub llmman

Test plan

  • cargo test --workspace in sdk/model-manager — 186 unit tests
    (was 185) + every integration suite green.
  • New crates/core/tests/llmman_pull.rs (7 tests): full pull against a
    wiremock daemon + stub llmman binary, asserting forwarded progress,
    the published geniex.json, and st_ino/nlink to prove the GGUF was
    hard-linked rather than copied; blob-path naming; no daemon listening;
    a non-llmman server on the port; in-band error at HTTP 200; a stream
    that ends without success; and callback cancellation.
  • 12 new unit tests in source::llmman (endpoint parsing, NDJSON line
    handling, filename synthesis, safetensors rejection) and 5 in mapping.
  • cargo clippy --workspace --all-targets — 9 warnings, identical to
    main; none in any touched file.
  • cargo fmt --all --check — clean.
  • gofmt -l cli/ bindings/go/ — clean.
  • Header verified by compiling a TU with _Static_assert on all seven
    geniex_HubSource values and the geniex_model_resolve_hub prototype.
  • nm on libgeniex_model.a — all 23 geniex_model_* symbols still
    exported, matching the CMake /EXPORT: list unchanged.
  • go build ./... for bindings/go + cli — not verified here (needs
    the CMake-staged sdk/pkg-geniex tree). Changes are a constant rename
    plus comments, and gofmt is clean.

Note: tests/executor.rs::downloads_multi_file_multi_chunk_and_marks_progress
is flaky on main today (reproduced 2/6 on a clean checkout with these
changes stashed). Untouched here.

Replaces the hand-rolled Docker Registry HTTP API V2 client from qualcomm#1165
with a source that delegates acquisition to llmman, so GenieX stops
maintaining registry protocol code it isn't in the business of owning.

llmman already implements registry auth, both Docker
`application/vnd.docker.ai.*` layouts, CNCF ModelPack, and
content-addressed resume, and covers GHCR / quay / ECR / self-hosted
registries that dockerhub.rs never did.

GENIEX_HUB_DOCKER is renamed to GENIEX_HUB_LLMMAN, keeping value 5 so
the ABI is unchanged; `--model-hub docker` becomes `--model-hub llmman`.

BREAKING CHANGE: GENIEX_HUB_DOCKER is now GENIEX_HUB_LLMMAN, and
`--model-hub docker|dockerhub` is now `--model-hub llmman|oci`. The
enum value (5) and the FFI symbol table are unchanged, so only source
compatibility breaks. Pulling an OCI reference now requires llmman
installed with `llmman serve` running.

Signed-off-by: Eric Curtin <eric.curtin@docker.com>
@mengshengwu

Copy link
Copy Markdown
Contributor

Hi, Eric Curtin (@ericcurtin) thank you for your contribution. This idea is quite informative. GenieX indeed has no intention of maintaining duplicate OCI‑related protocol implementations. However, the current PR requires users to install llmman on their devices, which creates additional usage costs for users. This is unacceptable from the perspectives of maintenance and user openbox availability. If you have other ideas, for instance, leveraging llmman's OCI protocol and other creates to replace the existing logic, that would be a viable practical approach. Thank you again for your constructive PR.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Mengsheng Wu (@mengshengwu) we could create libllmman and you statically link that in. It's more maintenance on the GenieX end though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants