feat(models): scan + add-by-path + model-dir setting#313
Merged
Conversation
0dcc6e4 to
af6f467
Compare
The dashboard now lets operators point hal0 at any model directory
(e.g. /mnt/ai-models), scan it recursively, and register individual
files by absolute path — all without dropping to `hal0 config edit`.
Backend
• New POST /api/models/add-from-path — single-file convenience
register. Validates path readability + the [models].file_extensions
allow-list, runs detect() for capabilities/backends, writes through
ModelRegistry.add(). Typed errors: model.path_missing,
model.unsupported_format, model.path_relative, model.already_exists.
overwrite=true replaces the existing entry in place.
• POST /api/models/scan/preview and POST /api/models/scan already
existed (and back the discover.py walker) — wiring through unchanged.
Settings
• New "Models" section in Settings — surfaces [models].roots,
[models].pull_root, [models].auto_scan_on_start via the existing
deep-merge PUT /api/settings, persisted to /etc/hal0/hal0.toml.
UI
• Two new model-page actions: "Scan directory" (recursive walk with
new/registered badges + checkbox commit) and "Add by path" (single
file with optional id/name/labels overrides).
• New hooks: useScanPreview, useAddModelFromPath, useSettings,
useSettingsUpdate, useSettingsReload.
Tests: 8 new tests cover the happy path, missing path, unsupported
extension, relative path, duplicate id, overwrite, and bad body shapes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
af6f467 to
490c46c
Compare
6 tasks
thinmintdev
added a commit
that referenced
this pull request
May 25, 2026
…tion (#319) One field replaces #313's roots + pull_root as the source of truth for where hal0 reads and writes model files. Propagates atomically to both consumers — the pull engine and Lemonade's extra_models_dir — and restarts hal0-lemonade.service when the latter changed. Backward-compatible: when ``[models].store`` is unset we fall back to the legacy ``[models].pull_root`` so PR-#313 installs keep working without an edit. The legacy field is documented as deprecated. Two new endpoints surface the workflow: GET /api/settings/models/store — current state + suggestions POST /api/settings/models/store — set + dry-run/migrate apply POST /api/settings/models/store/migrate — explicit migrate-then-apply The POST handler is a dry-run by default: when the current store has files at a different path it returns ``{status: "needs_migration", plan}`` and persists nothing. The UI renders a confirmation modal; on confirm the migrate endpoint moves files (same-fs rename or cross-fs copy via shutil.move), propagates to Lemonade config.json, restarts the unit, and persists hal0.toml. Move-first / persist-last so a failed move leaves prior state intact. Settings → Models swaps the two #313 fields for the single Storage location field plus suggestion chips (preset paths with per-path state probes — exists/files/free-bytes). FirstRun gains a new "Storage" step between picker and confirm so the bundle's downloads land in the right place. The Lemonade admin panel's locked extra_models_dir invariant is now derived from ``effective_store()`` so operators who set the store via Settings → Models can still edit Lemonade config coherently. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thinmintdev
added a commit
that referenced
this pull request
May 28, 2026
…rough + gut installer auth section (#390) - docs/operate/lemonade.md (new, .md canonical): operator reference for the v0.2 Lemonade runtime — what it is, where state lives, the /v1/* proxy + dispatcher fallthrough (PRs #248/#277), slot ↔ Lemonade model mapping (PRs #281/#282), max_loaded_models = 8 LRU cap (PR #283), per-type LRU eviction per ADR-0008 (supersedes nuclear-evict ADR-0007), OFFLINE-on-eviction (PR #276), and the three known v0.3 caveats (Vulkan KV gauge missing, whisper RUNPATH workaround, GPU cleanup unload hang). - docs/dashboard/v3.md (new, .md canonical, new docs/dashboard/ dir): page-by-page tour of the v3 React dashboard shipped in v0.3.0-alpha.1 (PR #235). Covers the shell + Mock-badge convention, /dashboard (system overview after #356), /chat (real surface per #309/#314/#315/#351), /slots (sidebar mirror per #357 + #344 UX sweep), /models (#313/#319/#353), /mcp (#304/#300), /agents (Peers per #299), /memory (graph #297, throughput #308), Settings (no Auth tab post-ADR-0012), and the footer journal (Epic #322 — PRs #321/#328/#329/#330/#332). Mock-fallback issues linked via the dashboard-v3 label, not enumerated. - installer/README.md: gut ~95 lines of stale auth prose (Caddy, Bearer-token mint/use/revoke, first-run OTP claim wizard, HAL0_AUTH_ENABLED/HAL0_AUTH_DISABLED, password recovery, basic_auth upgrade path, the TLS recipe). Replace with one paragraph pointing at docs/operate/auth.mdx for the reverse-proxy recipe and docs/agents/identity.md for the X-hal0-Agent identity model. Auth was removed in v0.3.0-alpha.1 per ADR-0012; the README hadn't caught up. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Discovery summary (before/after)
Before. The backend already had a full scan/preview pipeline (
POST /api/models/scan/preview+POST /api/models/scanin two modes — empty-body auto-scan walks[models].roots; body{"rows":[...]}commits a user-edited preview). It also hadPOST /api/modelsfor a rawModelpayload.ModelsConfigalready carriedroots,pull_root,auto_scan_on_start,file_extensions.GET/PUT /api/settingsalready round-tripped the wholeHal0Config(deep-merge PUT, atomic TOML write). What was missing was the front door — Settings had no editor formodels.roots/pull_root, the Models page had no scan or add-by-path buttons, and a single-file add required hand-crafting a fullModelbody (and dropped HF metadata per memoryhal0_model_store_layout).After. New thin endpoint
POST /api/models/add-from-pathreusesdetect()+ the registry write path so a one-line POST is enough to register an already-downloaded model. New Settings → Models section editsmodels.roots+pull_root+auto_scan_on_start. New Models-page actions "Scan directory" (preview + bulk register) and "Add by path" (single file). Five new UI hooks (useScanPreview,useAddModelFromPath,useSettings,useSettingsUpdate,useSettingsReload).Endpoint specs
POST /api/models/add-from-path— single-file registerRequest:
{ "path": "/mnt/ai-models/local/qwen3-4b-test/qwen3-4b-q4_k_m.gguf", "id": "user.qwen3-4b-local", // optional — derived from filename otherwise "name": "My Display Name", // optional "labels": ["chat", "tool-calling"], // optional — falls back to detect() "overwrite": false // default false }Response (201): the canonical
Modelrow (id/name/path/size_bytes/capabilities/backends/metadata/ns).Errors:
400 model.path_missing— file does not exist / not readable400 model.unsupported_format— extension not in[models].file_extensions400 model.path_relative— path wasn't absolute409 model.already_exists— duplicate id; passoverwrite=trueto replaceSettings field
[models]in/etc/hal0/hal0.toml. The Settings UI surfaces three fields:roots(list of absolute scan directories — first one drives the default Scan path)pull_root(where HF pulls land)auto_scan_on_start(boolean)file_extensionsis shown read-only ("edit viahal0 config edit") to keep the v1 surface small.Live verification (commands actually run against hal0 LXC)
Initial state —
/etc/hal0/hal0.tomlhad no[models]section;/api/modelsreturnedcount: 0.1. PUT /api/settings to set the model dir:
Verified
cat /etc/hal0/hal0.toml:[models]block now present.2. Seeded two fixture GGUFs:
3. POST /api/models/scan/preview against /mnt/ai-models (recursive):
4. POST /api/models/add-from-path:
/api/modelsnow lists the new entry withinstalled: true, owned_by: "local". The registry TOML at/var/lib/hal0/registry/registry.tomlcarries the entry verbatim (survives a reload).5. Error paths exercised:
6. UI smoke (Playwright headless against the live hal0-api on the LXC):
user.qwen3-4b-localrow, detail pane shows the/mnt/ai-models/local/qwen3-4b-test/qwen3-4b-q4_k_m.ggufpath, ns=pulled, labels chat + tool-calling./mnt/ai-models, pull_root/mnt/ai-models, auto-scan checkbox enabled, file-extensions read-only chip, "Stored at /etc/hal0/hal0.toml" hint, working Save/Reset buttons (disabled when no diff)./mnt/ai-modelspre-filled, hits Scan, lists both fixture GGUFs with capability hints +new/registeredbadges (registered row has its checkbox disabled so the operator can't double-add).Tests
tests/api/test_models_add_from_path.py— 8 new tests:test_add_from_path_registers_gguftest_add_from_path_honours_explicit_id_and_labelstest_add_from_path_rejects_missing_filetest_add_from_path_rejects_unsupported_extensiontest_add_from_path_rejects_relative_pathtest_add_from_path_409_on_duplicate_idtest_add_from_path_overwrites_when_requestedtest_add_from_path_rejects_bad_bodytests/api/test_models_scan.py,test_models_preview.py,test_models_crud.py,test_settings_routes.py,tests/registry/test_discover.pycontinue to pass (43 passed in the relevant slice; 430 passed in the fulltests/registry + tests/apirun).ruff check+ruff format --checkboth green for the changed files.Out-of-scope deferrals noticed during the work
[models].file_extensionsfrom the Settings UI — surfaced read-only with ahal0 config edithint; not commonly tweaked./api/models/add-from-pathendpoint — the Scan modal loops single-file POSTs instead (keeps failure mode obvious; one bad path doesn't poison the rest).auto_scan_on_start.hal0-apiwhich already runs as root on the LXC; no additional sandbox per the v1 brief.🤖 Generated with Claude Code