Skip to content

Unified collection import/export format; LMX collections distributed via Hugging Face#2099

Open
jeremyfowers wants to merge 5 commits into
mainfrom
jfowers/lmx-hf
Open

Unified collection import/export format; LMX collections distributed via Hugging Face#2099
jeremyfowers wants to merge 5 commits into
mainfrom
jfowers/lmx-hf

Conversation

@jeremyfowers
Copy link
Copy Markdown
Member

@jeremyfowers jeremyfowers commented Jun 4, 2026

What

Makes omni collections (recipe: collection.omni) behave like virtual models: they import/export through the exact same pipeline as regular models, using one JSON format shared by the server, GUI, CLI, and Hugging Face.

The format

A collection file is the regular-model export format plus components (ordered names) and models (each component's full definition, normalized by the same per-model transform):

{
    "model_name": "user.LMX-Omni-5.5B-Lite",
    "recipe": "collection.omni",
    "checkpoints": { "main": "lemonade-sdk/LMX-Omni-5.5B-Lite" },
    "components": ["Qwen3.5-4B-MTP-GGUF", "SD-Turbo", "Whisper-Tiny", "kokoro-v1"],
    "models": [ { "model_name": "Qwen3.5-4B-MTP-GGUF", "recipe": "llamacpp", "checkpoints": { "...": "..." }, "labels": ["..."], "size": 3.66 }, "..." ],
    "labels": [],
    "recipe_options": {},
    "size": 9.3
}

The same file works verbatim as: a POST /v1/pull body, a lemonade import file, and an HF-hosted manifest (<CollectionName>.json).

Example Model Files

https://huggingface.co/lemonade-sdk/LMX-Omni-52B-Halo
https://huggingface.co/lemonade-sdk/LMX-Omni-5.5B-Lite

Changes

  • Server read side: /v1/models and /v1/models/{id} embed an ordered models[] array for collections (each component's full model object, parallel to components).
  • Server pull side: /v1/pull accepts the exported file. Unknown components are registered from the embedded models[] definitions; known names keep the local definition (local-wins, drift logged as a warning). The existing component fan-out, cycle guard, and progress streaming are unchanged.
  • HF distribution: the built-in LMX-Omni-52B-Halo / LMX-Omni-5.5B-Lite entries are slim stubs (checkpoint = HF repo); pulling fetches the manifest from the repo, discovered by content (the repo's *.json with recipe: collection.omni) rather than a hardcoded filename. Manifests cache in the HF cache like any model file, so collections survive restarts and work offline after first pull.
  • CLI: lemonade export/import work for collections via the shared validate_and_transform_model_json transform (idmodel_name, kKnownKeys allow-list, checkpoint dedup, per-element normalization). Regular-model export output is byte-identical to before.
  • GUI: collection and regular-model export both fetch /models/{id} and save the normalized file (shared helper mirroring the CLI transform); collection import is a single POST /pull; export filenames drop the user. prefix; the legacy {version, collections, models} bundle format is removed.
  • Exports strip user-specific state: suggested, created, downloaded never appear in files (top level or inside models[]); the server regenerates them on import.
  • Docs: format documented on /v1/pull (lemonade.md, incl. a "Collection Files: Export, Import, and Hugging Face" section) and the model object (openai.md).

Breaking changes

  • Previously exported GUI collection bundles ({version, exportedAt, collections, models}) no longer import (hard cutover; the GUI was the only producer/consumer).

Verification

  • server_endpoints.py: 50/50 OK · server_cli2.py: 58/58 OK (1 skipped) · server_omni.py --wrapped-server llamacpp --backend vulkan: 7/7 OK (pre-pulls LMX-Omni-5.5B-Lite through the HF manifest path, then drives the server-side orchestration loop)
  • App regression suite: 31/31; collection unit tests rewritten for the new transform
  • Round-trips verified live: export ≡ uploaded HF manifest (JSON-equal); fresh-cache pull of both LMX collections; import of a collection file with an unknown component (registered as user.* from the embedded def, downloaded, survives restart); verbatim POST /pull of an exported file
  • Builds: C++ (Ninja Release) and web-app clean

🤖 Generated with Claude Code

jeremyfowers and others added 3 commits June 3, 2026 19:27
Collections (recipe collection.omni) now import/export exactly like
regular models — one JSON format shared by the server, GUI, CLI, and
Hugging Face:

- /models/{id} (and the /models list) embed an ordered models[] array
  for collections: each component's full model object, parallel to
  components[].
- Export (CLI `lemonade export` and the GUI Export button) fetches the
  live /models/{id} object and normalizes it into an import-ready /pull
  body via the shared transform (id -> model_name + user. prefix,
  kKnownKeys allow-list, checkpoint dedup, per-element normalization).
  Exported files never carry suggested/created/downloaded; the server
  regenerates them on import.
- /pull accepts the exported file verbatim: unknown components are
  registered from the embedded models[] definitions (local-wins + drift
  warning for known names), then the normal component fan-out runs.
- HF-hosted collections (the built-in LMX-Omni stubs) use the same file
  as their manifest, named <CollectionName>.json and discovered by
  content (recipe == collection.omni) instead of a hardcoded filename.
- GUI: collection import is now a single POST /pull; the legacy
  {version, collections, models} export bundle is removed (hard
  cutover); export filenames drop the user. prefix.
- Docs: format documented on /pull (lemonade.md) and the model object
  (openai.md).

Verified: server_endpoints.py 50/50, server_cli2.py 58/58,
server_omni.py 7/7 (pulls LMX-Omni-5.5B-Lite through the HF manifest
path and drives orchestration), app regression suite 31/31, and
`lemonade export LMX-Omni-5.5B-Lite` reproduces the uploaded HF
manifest exactly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremyfowers jeremyfowers self-assigned this Jun 4, 2026
jeremyfowers and others added 2 commits June 4, 2026 14:06
… guide

Review fixes for the collection import/export feature:
- Bound model_info_to_json's collection embedding at depth 3 so a cyclic
  collection registration cannot crash GET /v1/models
- Match handle_pull's canonicalization-skip condition to download_model's
  collection-file branch (models must be an array)
- Require a complete cached manifest (components AND models arrays) before
  do_not_upgrade skips the refresh in fetch_collection_manifest
- Apply the reserved-name check (and an empty-name guard) when registering
  components from remote manifest content
- Roll back a collection registered earlier in the same pull when its
  component resolution fails (no zombie user_models.json entries)
- Rename populate_collection_components_from_cache to *_locked per the
  existing convention and document the mutex precondition
- Surface model-export failures in the desktop app modal instead of only
  console.error

Docs: move the collection-file format and export/import/Hugging Face
workflows from the /v1/pull API reference into the custom models guide
("Share a collection"); the API page keeps an endpoint-specific note that
exported files work verbatim as /v1/pull bodies. Link the LMX omni models
table to their Hugging Face repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`lemonade pull <org>/<repo>` previously only recognized GGUF and ONNX
RyzenAI repositories; a repo carrying an exported collection manifest
failed with "No supported model files". Variant discovery now detects a
third repo kind: when a repo has no GGUF files but contains
<RepoName>.json, that file is fetched (size-capped) and validated as a
collection manifest (recipe "collection.omni" with components/models
arrays). The filename is the discovery key; a same-named file that is
not a manifest is a clear error rather than a fall-through.

The CLI then uses the manifest as the /pull body directly (it is
import-ready by design), with checkpoints.main set to the repo id so
the registered collection is HF-backed and a later pull refreshes its
definition from the repo.

The unsupported-repo error now lists all three supported repo types,
including the exact manifest filename that was looked for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremyfowers jeremyfowers marked this pull request as ready for review June 4, 2026 20:27
@jeremyfowers jeremyfowers requested a review from fl0rianr June 4, 2026 20:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f929003f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// the manifest to learn its components. On an explicit pull
// (do_not_upgrade=false) always refresh so newly added components are
// picked up.
components = resolve_collection_components_from_manifest(repo_id, do_not_upgrade);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist refreshed collection components

When an already-registered HF-backed user collection is pulled again after its manifest changes, this branch fetches and downloads the refreshed component list but never writes components back to user_models.json. The cache invalidation below then rebuilds user collections from the persisted stale components (only built-in empty-component collections call populate_collection_components_from_cache_locked), so /models, load/routing, and downloaded status continue to reflect the old collection even though the new manifest was fetched.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

@fl0rianr fl0rianr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a good direction overall: sharing one import/export shape across the server, CLI, GUI, and HF manifests should simplify the collection story a lot.

Before merge let's fix two correctness issues:

  1. I agree with the existing Codex inline comment on src/cpp/server/model_manager.cpp:3139: when an already-registered HF-backed user collection is refreshed from a changed manifest, the refreshed component list is used for the current download but is not persisted back into user_models.json. After cache invalidation/rebuild, the registered collection can still expose/load the stale components. Please persist the canonical refreshed components for user.* collections after manifest resolution, and add a regression test for manifest A/B -> A/B/C refresh.

  2. Inline collection imports should fail closed when components and models do not match. The format is documented as ordered/parallel; silently dropping missing or invalid component definitions can create a different collection than the exported file describes.

register_user_model(canonical, def);
components.push_back(canonical);
} else {
LOG(WARNING, "ModelManager") << "Skipping unknown collection component with no "
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be a hard import failure for inline collection files, not a warning/skip. validate_collection_request() only checks that models is an array; after that, any unknown component without a matching valid definition is skipped here. If at least one component remains, the collection can be persisted with a shortened component list, which is not the same collection the exported file described.

Can we require models to match components exactly for inline imports (same names, no missing definitions, no invalid entries), and abort/rollback instead of silently dropping components?

// the custom models guide). The filename is the discovery key; the content
// is then validated — a same-named file that is not a manifest is an
// error, not a fall-through.
const std::string manifest_filename = suggested_name + ".json";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation here requires the manifest to be named exactly .json. That matches the new user docs, but the PR description says HF manifests are discovered "by content" as the repo's *.json with recipe: collection.omni.

@jeremyfowers jeremyfowers added this to the Lemonade v10.7 milestone Jun 5, 2026
@github-actions github-actions Bot added enhancement New feature or request area::api HTTP REST API surface and route handlers labels Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area::api HTTP REST API surface and route handlers enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants