LocalAI/pkg/huggingface-api/client.go

525 lines
16 KiB
Go
Raw Normal View History

package hfapi
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// Model represents a model from the Hugging Face API
type Model struct {
feat: add distributed mode (#9124) * feat: add distributed mode (experimental) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix data races, mutexes, transactions Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix events and tool stream in agent chat Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * use ginkgo Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(cron): compute correctly time boundaries avoiding re-triggering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not flood of healthy checks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not list obvious backends as text backends Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tests fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Drop redundant healthcheck Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-29 22:47:27 +00:00
ModelID string `json:"modelId"`
Author string `json:"author"`
Downloads int `json:"downloads"`
LastModified string `json:"lastModified"`
PipelineTag string `json:"pipelineTag"`
Private bool `json:"private"`
Tags []string `json:"tags"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Sha string `json:"sha"`
Config map[string]any `json:"config"`
ModelIndex string `json:"model_index"`
LibraryName string `json:"library_name"`
MaskToken string `json:"mask_token"`
TokenizerClass string `json:"tokenizer_class"`
}
// FileInfo represents file information from HuggingFace
type FileInfo struct {
Type string `json:"type"`
Oid string `json:"oid"`
Size int64 `json:"size"`
Path string `json:"path"`
LFS *LFSInfo `json:"lfs,omitempty"`
XetHash string `json:"xetHash,omitempty"`
}
// LFSInfo represents LFS (Large File Storage) information
type LFSInfo struct {
Oid string `json:"oid"`
Size int64 `json:"size"`
PointerSize int `json:"pointerSize"`
}
// ModelFile represents a file in a model repository
type ModelFile struct {
Path string
Size int64
SHA256 string
IsReadme bool
URL string
}
// ModelDetails represents detailed information about a model
type ModelDetails struct {
ModelID string
Author string
Files []ModelFile
ReadmeFile *ModelFile
ReadmeContent string
feat(importer): expand importer flow to almost all backends (#9466) * docs(agents): require importer integration when adding backends Document the importer registry workflow so contributors know that adding a new backend also requires updating the /import-model dropdown source: either a new importer in core/gallery/importers/, extending an existing one for drop-in replacements, or the pref-only slice for backends with no reliable auto-detect signal. Always covered by a table-driven test. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for Batch 0 primitives Introduce failing tests that drive Batch 0 of the importer expansion: - pkg/huggingface-api: assert GetModelDetails populates PipelineTag and LibraryName from /api/models/{repo}, and that a failing metadata endpoint still returns file details (best-effort fetch). - core/gallery/importers/helpers_test.go: new table-driven coverage for HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile. - core/gallery/importers/importers_test.go: assert ErrAmbiguousImport sentinel exists and round-trips through errors.Is. - core/gallery/importers/local_test.go: extend with detection cases for ggml-*.bin (whisper), silero_vad.onnx (silero-vad), and the piper .onnx + .onnx.json pair. - core/http/endpoints/localai/import_model_test.go: assert ImportModelURIEndpoint returns HTTP 400 with a structured {error, detail, hint} body when ErrAmbiguousImport surfaces. All tests fail in the expected places (missing fields, missing helpers, missing sentinel, endpoint still wraps as 500). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): Batch 0 foundation — helpers, sentinel, local detection Implements the Batch 0 primitives that subsequent importer batches build on: - pkg/huggingface-api: ModelDetails gains PipelineTag and LibraryName. GetModelDetails now layers a best-effort GET /api/models/{repo} fetch on top of ListFiles — a metadata outage leaves the fields empty but still returns full file details. Uses a dedicated response struct because the single-model endpoint uses snake_case keys while the list endpoint historically returned camelCase. - core/gallery/importers/helpers.go: generic HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile helpers working on []hfapi.ModelFile so per-backend importers can detect artefact patterns without duplicating string wrangling. - core/gallery/importers/importers.go: adds the ErrAmbiguousImport sentinel. DiscoverModelConfig now returns it (wrapped with fmt.Errorf("%w: ...")) when no importer matched AND the HF pipeline_tag falls in a whitelist of narrow modalities (ASR, TTS, sentence-similarity, text-classification, object-detection). The whitelist is intentionally narrow — unknown tags keep the previous "no importer matched" behaviour to avoid blocking rare repos. - core/gallery/importers/local.go: three new local-path detections, inserted before the existing merged-transformers branch: * ggml-*.bin → whisper * silero*.onnx → silero-vad * *.onnx + *.onnx.json pair → piper - core/http/endpoints/localai/import_model.go: ImportModelURIEndpoint surfaces ErrAmbiguousImport as HTTP 400 with {error, detail, hint} JSON, preserving existing behaviour for unrelated errors. Green tests: go test ./core/gallery/importers/... ./pkg/huggingface-api/... \ ./core/http/endpoints/localai/... Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(importers): red tests for KnownBackend endpoint and importer metadata Add failing tests that drive Batch UI-Dropdown: - importers_test.go: assert importers expose Name/Modality/AutoDetects and that LlamaCPPImporter advertises drop-in replacements via a new AdditionalBackendsProvider interface. A Registry() accessor is also expected. - backend_test.go (new): assert GET /backends/known returns []schema.KnownBackend, covers every importer, exposes drop-in llama-cpp replacements, includes curated pref-only backends, has no duplicates, and is sorted by Modality+Name. These tests fail at compile time against master; they are intentionally red so the follow-up green commit is reviewable. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery): add /backends/known endpoint for importer-aware backend list Extend the Importer interface with Name/Modality/AutoDetects so the import system can self-describe its registry, and introduce the AdditionalBackendsProvider interface so importers can advertise drop-in replacements (llama-cpp advertises ik-llama-cpp and turboquant). Expose the new GET /backends/known endpoint that merges: - the importer registry (auto-detect supported), - drop-in replacements hosted by importers (preference-only), - a curated knownPrefOnlyBackends slice for backends with no dedicated importer (sglang, tinygrad, trl, mlx-vlm, whisperx, kokoros, Qwen TTS variants, sam3-cpp) — kept at the top of backend.go so contributors adding a new pref-only backend have one obvious place to edit, - backends installed on disk but unknown to the importer (marked AutoDetect=false, empty Modality). The endpoint deliberately does NOT filter by gallery membership or host capability (unlike /backends/available): LocalAI may auto-install a backend that is not yet present, so the import form dropdown must show everything the importer knows about. Response is deduplicated (importer wins over pref-only) and sorted by Modality+Name for deterministic output. Registered in core/http/routes/localai.go next to /backends/available under the same admin middleware. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui): source import form backend dropdown from /backends/known Replace the hard-coded BACKENDS constant in ImportModel.jsx with a live fetch of /backends/known on mount. Users now see every backend the importer layer knows about (including preference-only entries) grouped by modality, not a stale subset. Changes: - config.js: add backendsKnown endpoint constant next to backendsAvailable. - api.js: add backendsApi.listKnown() wrapper. - ImportModel.jsx: remove BACKENDS constant, fetch the list via useEffect, and derive grouped options via buildBackendOptions. Preference-only entries render with a " (preference-only)" suffix. Loading state disables the dropdown with a "Loading backends…" placeholder; on fetch failure the form falls back to auto-detect only and surfaces a non-blocking toast. - SearchableSelect.jsx: accept items flagged isHeader=true and render them as non-selectable section dividers. Keyboard navigation skips headers and search queries hide them so filtered output stays relevant. Vitest is not set up in this project (devDependencies ship Playwright only). Per the brief's guard-rail, no frontend test framework is introduced; coverage is provided by the Go handler tests that assert the /backends/known contract consumed by the React form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for whisper importer Asserts detection on ggerganov/whisper.cpp (via ggml-*.bin filename), the preferences.backend=whisper override path for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add whisper importer Recognises whisper.cpp GGML models by the "ggml-*.bin" filename convention (direct URL or HF repo member) and by the explicit preferences.backend="whisper" override. Emits backend: whisper with the transcript use-case. Registered before llama-cpp so the narrow filename signal wins before any generic GGUF match is attempted. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for moonshine importer Asserts detection on UsefulSensors/moonshine-tiny via owner + ONNX files, the preferences.backend=moonshine override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add moonshine importer Matches UsefulSensors-owned HF repos whose artefacts or metadata identify them as ASR: on-disk .onnx files (the canonical Moonshine packaging) OR pipeline_tag=automatic-speech-recognition (covers transformers/safetensors-only sibling repos). preferences.backend= moonshine overrides detection. Test uses the live moonshine-tiny repo because the canonical UsefulSensors/moonshine repo currently hits a recursive-subfolder bug in pkg/huggingface-api ListFiles. Registered after WhisperImporter but before LlamaCPPImporter and TransformersImporter so the narrower owner+ASR signal wins before the generic tokenizer.json check routes the repo to transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for nemo importer Asserts detection on nvidia/parakeet-tdt-0.6b-v3 via owner + .nemo file, the preferences.backend=nemo override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add nemo importer Matches nvidia-owned HF repos that ship a .nemo checkpoint archive, the canonical NeMo ASR packaging. preferences.backend=nemo forces detection. Registered between moonshine and llama-cpp so the narrow owner + extension signal wins before any downstream generic matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for faster-whisper importer Asserts detection on Systran/faster-whisper-large-v3 (owner + model.bin + config.json + ASR pipeline), the preferences.backend= faster-whisper override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add faster-whisper importer Recognises CTranslate2-packaged whisper checkpoints distributed for the faster-whisper runtime: model.bin + config.json + ASR pipeline_tag, narrowed to Systran-owned repos or repo names containing "faster-whisper" to avoid falsely claiming vanilla OpenAI whisper HF repos. preferences.backend=faster-whisper overrides detection. Registered before llama-cpp and transformers so the narrow signal wins before tokenizer.json routes the repo to the generic transformers importer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for qwen-asr importer Asserts detection on Qwen/Qwen3-ASR-1.7B via owner + ASR substring in the repo name, the preferences.backend=qwen-asr override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add qwen-asr importer Matches Qwen-owned HF repos whose name contains "ASR" (case-insensitive), routing them to the qwen-asr backend rather than the generic transformers/vllm path. The substring check scans the repo portion only so the owner field cannot leak a false match. preferences.backend=qwen-asr forces detection. Registered before llama-cpp and transformers so the narrow owner+name signal wins. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): ASR ambiguity surfaces ErrAmbiguousImport Locks in the behaviour added in Batch 0: an HF repo whose pipeline_tag marks it as automatic-speech-recognition but whose artefacts match no ASR importer (and no generic importer) must fail with ErrAmbiguousImport so callers know to pass preferences.backend rather than silently guess. pyannote/voice-activity-detection is the fixture — its file list is only config.yaml + README, leaving every importer's artefact check negative. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for piper importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add piper importer Detects piper TTS voices by the canonical <voice>.onnx + <voice>.onnx.json pair packaging (via HasONNXConfigPair). Narrow enough to skip generic ONNX repos used by other backends (Moonshine ASR, sentence-transformers). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for bark importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add bark importer Detects Suno's Bark TTS checkpoints by HF owner "suno" + repo name prefix "bark". Adds HFOwnerRepoFromURI() helper so importers can fall back to URI parsing when pkg/huggingface-api's recursive tree listing errors on repos with nested subdirectories (suno/bark ships a speaker_embeddings/v2 subtree that trips a pre-existing path-doubling bug in the listFilesInPath recursion). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for fish-speech importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add fish-speech importer Detects Fish Audio TTS releases by HF owner "fishaudio" with a URI-based fallback for repos whose tree recursion trips the pre-existing hfapi path-doubling bug. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for outetts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add outetts importer Detects OuteAI's OuteTTS releases by HF owner "OuteAI" or a case- insensitive "OuteTTS" substring in the repo name, with a URI-based fallback for recursion-bugged repos. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for voxcpm importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add voxcpm importer Detects OpenBMB's VoxCPM TTS family by repo-name substring (community mirrors re-host the weights under many owners — mlx-community, bluryar, callgg, etc). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kokoro importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kokoro importer Detects hexgrad's Kokoro TTS by the "Kokoro" repo-name substring paired with a PyTorch .pth/.pt checkpoint — the pairing excludes ONNX-only mirrors (handled by the pref-only `kokoros` Rust runtime) and GGUF mirrors (handled by llama-cpp). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kitten-tts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kitten-tts importer Detects KittenML's kitten-tts releases by owner or "kitten-tts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for neutts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add neutts importer Detects Neuphonic's NeuTTS releases by owner "neuphonic" or "neutts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for chatterbox importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add chatterbox importer Detects Resemble AI's Chatterbox TTS by owner "ResembleAI" or "chatterbox" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vibevoice importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vibevoice importer Detects Microsoft's VibeVoice TTS by "vibevoice" repo-name substring (case-insensitive) so community mirrors still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for coqui importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add coqui importer Detects Coqui AI's TTS releases (XTTS-v2, YourTTS, …) by the authoritative `coqui` HF owner, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): TTS ambiguity surfaces ErrAmbiguousImport Adds a Ginkgo spec that imports nari-labs/Dia-1.6B — a real HF repo carrying pipeline_tag="text-to-speech" whose artefacts (*.pth, one safetensors shard, preprocessor_config.json, config.json) match none of the Batch-2 TTS importers nor the generic text/image importers — and asserts DiscoverModelConfig wraps ErrAmbiguousImport via errors.Is. Also pivots the endpoint-level ambiguity fixture from hexgrad/Kokoro-82M to nari-labs/Dia-1.6B. Batch 2 added a dedicated kokoro importer that now claims the original fixture; Dia remains genuinely unclaimed and so exercises the same ambiguity code path at the HTTP layer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for stablediffusion-ggml importer Covers HF repo detection (city96/FLUX.1-dev-gguf), raw .gguf URL matching on filename arch tokens, preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add stablediffusion-ggml importer Detects GGUF-packed Stable Diffusion and FLUX checkpoints (leejet owner, city96 FLUX mirrors, second-state SD dumps, raw .gguf URLs with arch tokens) and routes them to the stablediffusion-ggml backend. Registered BEFORE LlamaCPPImporter so .gguf image checkpoints are not stolen by llama-cpp's generic .gguf match. Reuses HFOwnerRepoFromURI for the hfapi-recursion-bug fallback. preferences.backend overrides detection. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for ace-step importer Covers HF repo-name detection (ACE-Step/ACE-Step-v1-3.5B), preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add ace-step importer Routes ACE-Step music generation checkpoints (ACE-Step/ACE-Step-v1-3.5B, ACE-Step/Ace-Step1.5, community mirrors) to the ace-step backend. Matching is case-insensitive on the "ace-step" repo-name substring and owner, with an HFOwnerRepoFromURI fallback for the hfapi recursion bug. KnownUsecaseStrings mirrors the gallery's ace-step-turbo entry (sound_generation, tts). preferences.backend overrides. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on text-to-image misses Adds text-to-image to ambiguousModalities whitelist and covers the h94/IP-Adapter-FaceID case — pipeline_tag=text-to-image but ships only .bin/.safetensors so diffusers, stablediffusion-ggml, llama-cpp, transformers, vllm, mlx, and ace-step all miss. DiscoverModelConfig now surfaces ErrAmbiguousImport for that shape instead of the opaque "no importer matched" error. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vllm-omni importer Introduces the test surface for the forthcoming VLLMOmniImporter: detection via preferences.backend, Qwen owner + Omni repo token, URI-only fallback, negative cases (plain Qwen, random OmniX repo), and Import() emitting backend: vllm-omni with chat + multimodal usecases. Includes a registration-order assertion via DiscoverModelConfig to pin the requirement that vllm-omni wins over vllm for Qwen Omni repos (tokenizer files are usually present too). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vllm-omni importer Adds VLLMOmniImporter for Qwen Omni-style multimodal checkpoints (Qwen3-Omni, Qwen2.5-Omni, …). Detection is narrow: HF owner "Qwen" combined with "omni" in the repo name, or a repo name matching the -Omni-/Omni- naming pattern. preferences.backend="vllm-omni" always wins; HFOwnerRepoFromURI provides a URI-only fallback for the hfapi recursion-bug edge case. Emitted YAML sets backend: vllm-omni and known_usecases: [chat, multimodal], matching the gallery/index.yaml vllm-omni entries. The importer is registered ahead of VLLMImporter so Qwen Omni repos — which also carry tokenizer files — route to vllm-omni rather than the plain vllm backend. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for llama-cpp drop-in preferences Pins the expected drop-in replacement behaviour: preferences.backend of ik-llama-cpp or turboquant must swap the emitted YAML backend field while keeping the llama-cpp file layout identical. Also covers the unknown-backend case (must stay llama-cpp) and re-asserts AdditionalBackends() returns the two curated entries with non-empty descriptions. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): llama-cpp honours ik-llama-cpp and turboquant drop-in preferences preferences.backend set to ik-llama-cpp or turboquant now swaps the emitted YAML backend field while leaving the file layout, model path, mmproj handling and everything else in the llama-cpp Import pipeline untouched. Unknown values are ignored and fall back to backend: llama-cpp so arbitrary input can't leak into the config. Aligns the AdditionalBackends() descriptions with the user-facing naming conventions surfaced via /backends/known. No changes to the pref-only curated list in endpoints/localai/backend.go: the two drop-in names have always lived on the importer side via AdditionalBackends. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for silero-vad importer Add the SileroVADImporter test fixtures covering metadata, preference overrides, snakers4 + onnx detection, silero_vad.onnx canonical filename, URI fallback, and live HF discovery. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add silero-vad importer Recognise the Silero VAD ONNX packaging: the canonical silero_vad.onnx filename or any ONNX file under the snakers4 owner. Emits a backend: silero-vad config with the vad known_usecase, and attaches the canonical file entry when present so the weights download on import. Registered before the generic importers so the unique-filename signal takes precedence over any downstream tokenizer-based matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rerankers importer Cover the RerankersImporter contract: interface metadata, preference override, cross-encoder owner detection, case-insensitive 'reranker' substring match (BAAI/bge-reranker, Alibaba-NLP/gte-reranker), URI fallback, and the full-discovery ordering check that a BAAI reranker repo must route to the rerankers importer rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rerankers importer Recognise reranker repositories — cross-encoder owner or any repo whose name contains 'reranker' (case-insensitive). Emits backend: rerankers with reranking: true and the rerank known_usecase. Registered ahead of sentencetransformers and transformers so reranker repos that happen to ship tokenizer.json or modules.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for sentencetransformers importer Cover the SentenceTransformersImporter contract: interface metadata, preference override, modules.json marker file, sentence_bert_config.json marker file, sentence-transformers owner, URI fallback, and the full-discovery ordering check that ensures a sentence-transformers HF URI routes here rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add sentencetransformers importer Recognise sentence-transformers embedding repos by modules.json, sentence_bert_config.json, or the sentence-transformers owner. Emits backend: sentencetransformers with embeddings: true and the embeddings known_usecase. Registered ahead of transformers so ST repos that carry tokenizer.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rfdetr importer Cover the RFDetrImporter contract: interface metadata, preference override, case-insensitive rf-detr and rfdetr substring matches, URI fallback, and negative cases. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rfdetr importer Recognise RF-DETR object-detection repositories by a case-insensitive 'rf-detr' / 'rfdetr' substring in the repo name. Emits backend: rfdetr with the detection known_usecase. Registered ahead of transformers so RF-DETR repos with tokenizer artefacts still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on sentence-similarity misses Add an ambiguity fixture covering the embeddings/rerankers modality. Qdrant/bm25 carries pipeline_tag=sentence-similarity but ships only config.json + stopword .txt files — none of the Batch 5 importers (silero-vad, rerankers, sentencetransformers, rfdetr) or the generic vllm/transformers/llama-cpp/mlx/diffusers importers match. Because the modality is in the ambiguous whitelist, DiscoverModelConfig must surface ErrAmbiguousImport. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/backend): red tests for KnownBackend.Installed flag Extend the /backends/known suite with three failing cases that pin down the forthcoming Installed field: JSON field presence on every entry, flipping to true when an importer-registered backend is also present on disk (and staying false for non-installed pref-only entries), and surfacing system-only backends with empty modality and AutoDetect=false. A small writeFakeSystemBackend helper plants a run.sh under the backends dir so gallery.ListSystemBackends recognises the fixture. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(schema,localai/backend): add Installed flag to KnownBackend Add an Installed bool to schema.KnownBackend and populate it from the /backends/known handler so the React import form can warn users that picking a not-yet-installed backend will trigger an automatic download on submit. Computation: after merging the importer registry, additional backends provider entries and the curated pref-only slice, the handler walks gallery.ListSystemBackends(systemState) and either flips the existing map entry's Installed flag to true (preserving modality / autodetect / description metadata) or inserts a bare {Installed:true} entry for system-only backends the importer layer doesn't know about. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/import_model): structured ambiguous-import response Add red tests covering the extended ambiguity shape the React import form needs: - ImportModelURIEndpoint must return an HTTP 400 body that exposes the detected `modality` (normalised to the importer modality key, e.g. "tts" for pipeline_tag=text-to-speech) and a list of `candidates` (backend names filtered by modality, excluding text-LLM backends). - The importers package must surface a typed AmbiguousImportError so HTTP consumers can read Modality + Candidates without parsing the error string. errors.Is against the existing sentinel keeps working. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(localai/import_model): structured ambiguity response with modality + candidates DiscoverModelConfig now returns a typed AmbiguousImportError that carries the importer modality key, candidate backend names, the original URI, and the raw HF pipeline_tag. Its Is() preserves errors.Is(err, ErrAmbiguousImport) for legacy callers. The importer modality is pre-mapped from the HF pipeline_tag (automatic-speech-recognition → asr, text-to-speech → tts, etc) via PipelineTagToModality — surfaced as an exported helper so downstream consumers can avoid duplicating the table. CandidatesForModality filters the default importer registry plus AdditionalBackendsProvider drop-ins by modality, sorts deterministically, and is the single source of truth used by ImportModelURIEndpoint. ImportModelURIEndpoint now returns HTTP 400 with { error, detail, modality, candidates, hint } when ambiguity fires, letting the React form render a modality-scoped picker inline instead of a generic toast. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): manual pick badge + tooltip Red Playwright coverage for the preference-only → manual pick rename: - The Backend dropdown renders a "manual pick" badge on every option whose KnownBackend.auto_detect is false. - The badge carries a title attribute with hover-tooltip copy that explains auto-detect won't route to this backend. - Auto-detectable backends must NOT carry the badge. - The legacy " (preference-only)" suffix is gone from every label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): replace preference-only suffix with manual pick badge SearchableSelect option rows now support an optional badge field — a muted pill rendered to the right of the label with an optional title attribute for native hover tooltips. Plain text so screen readers read it alongside the option name. buildBackendOptions in ImportModel stops appending " (preference-only)" to the label and instead sets badge="manual pick" plus a descriptive tooltip on every option whose auto_detect is false. The Backend help text explains what "manual pick" means so users aren't left wondering about the badge. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): inline ambiguity picker Red Playwright coverage for Batch A2 — when the server returns a 400 ambiguity body, the form must render an inline alert instead of a toast, expose one clickable chip per candidate backend, and support both auto-resubmit on pick and silent dismiss. - Mocks /api/models/import-uri with the structured ambiguity body (error, detail, modality, candidates, hint). - On first click of Import, the alert is visible, carries modality-specific copy, and shows a chip per candidate. - Clicking a chip clears the alert, sets the Backend dropdown, and triggers a second POST to /api/models/import-uri. - Dismissing the alert leaves the Backend dropdown on Auto-detect — no implicit backend assignment. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): inline ambiguity alert with candidate chips Adds AmbiguityAlert — a soft, info-coloured card rendered above the URI input when the server returns a structured 400 with { modality, candidates }. Message is modality-aware (tts/asr/embeddings/image/ reranker/detection get purpose-written copy, everything else falls back to a generic template). Each candidate is a clickable chip that shows a download icon when /backends/known marks the backend as not yet installed, so users aren't surprised by an implicit install. ImportModel wires the alert to handleSimpleImport's error path: - api.handleResponse now attaches { status, body } to the thrown Error so pages can pattern-match on structured responses instead of string error messages. - handleSimpleImport detects `status === 400 && body.error === 'ambiguous import'` and flips into the inline-picker mode instead of toasting. - Clicking a chip sets prefs.backend and auto-resubmits (passing the picked backend as an override so setPrefs's asynchrony doesn't leak a stale value). - Dismissing clears the alert; changing the URI or the backend also clears it so a stale alert never sticks around. Test fixtures mock GET /backends/known + POST /models/import-uri so the Playwright specs don't depend on real network reachability. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): auto-install warning Red Playwright coverage for Batch A3 — when the user picks a backend whose KnownBackend.installed is false, the form must render a muted inline note under the Backend dropdown warning that submitting will download the backend first. Picking an installed backend or leaving Auto-detect selected must keep the note hidden. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): auto-install warning under backend dropdown When the user picks a backend whose KnownBackend.installed is false, render a muted inline note under the Backend dropdown's help text warning that submitting will download the backend first. The note lives inside the same form-group so it lines up with the existing hint text; it's hidden when Auto-detect is selected (the selected backend is unknowable at that point) or when the chosen backend is already on disk. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): drop redundant section header, adjust icons, rename HF shortcut - Remove the "Import from URI" card-level <h2> — the page title already says "Import New Model" one row up, so the secondary header was duplicating information. - Swap the fa-star on "Common Preferences" for fa-sliders (stars imply favourites/ratings; this is just a preferences block) and move the Custom Preferences fa-sliders-h to fa-plus-circle so the two blocks read as distinct rather than as two sliders. - Rename the HF shortcut from "Search GGUF on HF" → "Browse models on HF" and drop the `search=gguf` filter on the linked URL. The import form now supports ~40 backends; hard-coding GGUF in the copy no longer matches the form's actual reach. - Pure polish — no behaviour change, covered by the existing Batch A Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): batch B — simple/power switch, options, tabs, dialog Adds a failing Playwright suite covering the full Batch B surface ahead of implementation: - B1: SimplePowerSwitch segmented control renders, toggles, persists to localStorage across reloads. - B2: Simple-mode Options disclosure is collapsed by default; expanding exposes only Backend, Model Name, Description (no quantizations, mmproj, model type, or custom prefs). - B3: Power mode has Preferences and YAML tabs with a persistent selection across reloads; URI/name/description typed in Simple carry over to Power; YAML tab swaps the primary action to Create. - B4: Switching Power -> Simple with a custom preference set triggers the 3-button confirmation dialog (Keep / Discard / Cancel) with the documented semantics. Tests fail against master — implementation lands in the following commits. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add SimplePowerSwitch segmented control Replaces the previous "Advanced Mode / Simple Mode" toggle button in the page header with a two-segment control that flips between Simple and Power. The control reuses the existing .segmented CSS shared with the Sound page for visual consistency. Mode state is persisted to localStorage under `import-form-mode` so reloads land on the same view (default: simple). The boolean alias `isAdvancedMode` is retained internally to minimise diff — subsequent commits reshape the Simple and Power surfaces independently. Closes B1 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): simple mode collapsible options, power tabs, switch dialog Completes the Batch B surface in a single structural pass so Simple and Power mode can evolve independently: Simple mode - URI input + Ambiguity alert + Import button, plus a collapsible "Options" disclosure that exposes ONLY Backend, Model Name, Description. Quantizations / MMProj / Model Type / Diffusers fields / Custom Preferences are no longer rendered in Simple mode. Power mode - In-page segmented "Preferences · YAML" tab strip. Active tab persists to localStorage under `import-form-power-tab`. - Preferences tab = the full existing preferences + custom prefs panel (no progressive disclosure yet — that's Batch D). - YAML tab = the existing CodeEditor. Primary button reads "Create" here, "Import Model" everywhere else. Switch dialog - Power -> Simple with non-default prefs (advanced pref keys set, any custom-pref key non-empty, or YAML edited away from the template) opens a 3-button dialog: Keep & switch / Discard & switch / Cancel. - Keep preserves all state. Discard resets prefs + customPrefs + YAML to defaults. Cancel leaves the user in Power mode. Page subtitle reflects the current surface (Simple, Power/Preferences, Power/YAML). Estimate banner renders everywhere except Power/YAML. Closes B2/B3/B4 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): expand Options disclosure in Batch A tests Batch B hid the Backend dropdown behind a collapsible Options disclosure in Simple mode. The Batch A tests that exercise the dropdown directly (manual-pick badge, ambiguity chip sets the selected backend, auto- install warning) now click the disclosure toggle before asserting on dropdown contents. Test intent is unchanged. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): strip decorative icons from field labels The preference panel had 12 Font Awesome icons decorating field labels (Backend, Model Name, Description, Quantizations, MMProj Quantizations, Model Type, Pipeline Type, Scheduler Type, Enable Parameters, Embeddings, CUDA, plus fa-link on Model URI). Every label screamed equally, flattening the visual hierarchy. Remove them. Keep icons where they carry meaning: page-level section headers, URI format guide entries, primary buttons, the Simple-mode Options disclosure, the ambiguity alert's fa-lightbulb, the auto-install note's fa-download, and the Estimated-requirements banner's fa-memory / fa-microchip / fa-download. No new behaviour, no layout / spacing changes beyond removing the orphaned icon margin. Playwright suite green. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): progressive disclosure of preference fields Cover the Batch D visibility matrix for Power > Preferences: Quantizations, MMProj Quantizations, and Model Type each render only for the backends that can consume them, stay visible when the backend is unset, and preserve any value the user already typed when toggled off and back on. Also pin the shrunk Description textarea at rows=2. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): progressive disclosure + shorter description textarea Gate Quantizations, MMProj Quantizations, and Model Type in the Power > Preferences tab so each field only renders for the backends that can actually consume it. Backend unset keeps everything visible. Hidden fields' state is preserved (the JSX wrapper is guarded, not the underlying prefs state) so users flipping backends back and forth don't lose input. Also shrink the Description textarea from rows=3 to rows=2 — it's shared between Simple Options and Power Preferences so the change applies to both. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): enter-to-submit in Simple mode Red test for Batch F3 — pressing Enter in the URI input must POST /models/import-uri, and Enter in the Description textarea must insert a newline without submitting the form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): enter-to-submit in Simple mode Wrap the Simple-mode URI input + ambiguity alert + Options disclosure in a <form> whose onSubmit calls handleSimpleImport. Pressing Enter in the URI input (or any Simple-mode text input) now submits the import without having to move the mouse to the header button. The Description textarea keeps its native behaviour — Enter inserts a newline. A hidden submit button is included because the visible Import button lives outside the form in the page header; some browsers only fire implicit Enter-submit when the form contains a submit-capable element. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import,SearchableSelect,components): aria-hidden on decorative icons Every Font Awesome icon in the import form is decorative — its meaning is already conveyed by adjacent visible text. Adding aria-hidden="true" prevents screen readers from announcing the unicode glyph point as content. Covers ImportModel.jsx (all remaining <i> glyphs) and SearchableSelect.jsx (the trigger chevron). AmbiguityAlert and SimplePowerSwitch already set aria-hidden on their icons when the components landed in Batches A and B — no change needed there. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(SearchableSelect): responsive dropdown maxHeight + hover focus guard F2 — replace fixed pixel heights with min(pixel, vh) so the dropdown and its inner scroll region don't overflow short viewports. Outer container: 260px -> min(260px, 60vh); inner listbox: 200px -> min(200px, 50vh). Tall viewports still get the original pixel caps. F5 — short-circuit onMouseEnter when the hovered row is already the focused row. Avoids queueing a setFocusIndex call (and a render) for every mousemove inside the same item — the state would be identical. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): aria-label on custom preference rows The Key / Value inputs and trash button in each Custom Preferences row previously relied on placeholder text alone. Placeholders are not accessible names — they vanish on input and screen readers do not announce them consistently. Add row-indexed aria-labels so assistive tech can distinguish "Preference key for row 1" from "row 2", and give the trash button an explicit "Remove this preference" label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): modality chip row Red tests for Batch E — a horizontal modality chip row that filters the Backend dropdown by modality. Covers visibility in Simple-mode Options and Power/Preferences (and absence in Power/YAML), filter behaviour, mismatched-backend clearing with toast, ambiguity-alert auto-selection, and radiogroup keyboard navigation. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add ModalityChips component + filter integration Horizontal chip row (Any, Text, Speech, TTS, Image, Embeddings, Rerankers, Detection, VAD) filters the Backend dropdown options to the selected modality. Default is Any — no filter, current behaviour. - New ModalityChips component (radiogroup pattern, roving tabindex, arrow-key navigation, Home/End). - buildBackendOptions now accepts an optional modalityFilter so grouped output is narrowed before rendering. - Chips render inside Simple-mode Options disclosure and Power > Preferences tab. Power > YAML stays unaffected. - Switching the filter drops a mismatched backend selection and surfaces a toast so the auto-clear is visible. - Ambiguity alerts auto-activate the matching chip so users see only relevant backends even if they dismiss the alert. Tightens the Batch E tests' option-matching to the label <span> so the "↵" keybind hint on the focused row doesn't break accessible-name lookups. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * fix(ui/import): rename Power to Advanced + stop URI-formats toggle from submitting form The "Supported URI Formats" disclosure button inside the Simple-mode form lacked an explicit type attribute, so it defaulted to type="submit". Every click triggered the form's onSubmit and surfaced the empty-URI validation toast ("Please enter a model URI"). Marking it type="button" lets it behave as a pure toggle. While here, rename the user-visible "Power" label to "Advanced" in the mode switch (button text + tooltip) and the Power-mode tab's aria-label, matching the term users actually expect. The internal mode key stays 'power' so tests, localStorage, and data-testid selectors are untouched. Assisted-by: Claude:claude-opus-4-7 * fix(system): fall back to cpu when meta backend lacks default capability Meta backends like vllm and sglang enumerate concrete variants for nvidia/amd/intel/cpu but omit a default: catch-all entry. On a no-GPU host the reported capability is "default", so the previous Capability() returned "default" unconditionally on a miss — IsCompatibleWith then saw no "default" key and filtered the meta out of AvailableBackends. The import flow's auto-install step then failed with "no backend found with name <meta>", contradicting the UI's promise that the backend would be downloaded on demand. Try the explicit "default" key first, then fall back to "cpu" before giving up. vllm now resolves to cpu-vllm on CPU-only Linux without touching the gallery YAML. Assisted-by: Claude:claude-opus-4-7
2026-04-22 20:42:37 +00:00
// PipelineTag mirrors the HuggingFace model-level "pipeline_tag" field
// (e.g. "text-to-speech", "sentence-similarity"). Empty when the /api/models
// metadata endpoint is unreachable or the repo does not declare one.
PipelineTag string
// LibraryName mirrors the HuggingFace "library_name" field
// (e.g. "transformers", "diffusers", "sentence-transformers"). Empty when
// the metadata endpoint is unreachable or the repo does not declare one.
LibraryName string
}
// SearchParams represents the parameters for searching models
type SearchParams struct {
Sort string `json:"sort"`
Direction int `json:"direction"`
Limit int `json:"limit"`
Search string `json:"search"`
}
// Client represents a Hugging Face API client
type Client struct {
baseURL string
client *http.Client
}
// NewClient creates a new Hugging Face API client
func NewClient() *Client {
return &Client{
baseURL: "https://huggingface.co/api/models",
client: &http.Client{},
}
}
// SearchModels searches for models using the Hugging Face API
func (c *Client) SearchModels(params SearchParams) ([]Model, error) {
req, err := http.NewRequest("GET", c.baseURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Add query parameters
q := req.URL.Query()
q.Add("sort", params.Sort)
q.Add("direction", fmt.Sprintf("%d", params.Direction))
q.Add("limit", fmt.Sprintf("%d", params.Limit))
q.Add("search", params.Search)
req.URL.RawQuery = q.Encode()
// Make the HTTP request
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch models. Status code: %d", resp.StatusCode)
}
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Parse the JSON response
var models []Model
if err := json.Unmarshal(body, &models); err != nil {
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
return models, nil
}
// GetLatest fetches the latest GGUF models
func (c *Client) GetLatest(searchTerm string, limit int) ([]Model, error) {
params := SearchParams{
Sort: "lastModified",
Direction: -1,
Limit: limit,
Search: searchTerm,
}
return c.SearchModels(params)
}
// GetTrending fetches models sorted by HuggingFace's trendingScore — the
// same signal the public "Trending" tab uses. Useful when picking fresh
// candidates to add to a gallery: it biases toward repos that are gaining
// attention right now, rather than strictly newest or strictly most
// downloaded overall.
func (c *Client) GetTrending(searchTerm string, limit int) ([]Model, error) {
params := SearchParams{
Sort: "trendingScore",
Direction: -1,
Limit: limit,
Search: searchTerm,
}
return c.SearchModels(params)
}
// BaseURL returns the current base URL
func (c *Client) BaseURL() string {
return c.baseURL
}
// SetBaseURL sets a new base URL (useful for testing)
func (c *Client) SetBaseURL(url string) {
c.baseURL = url
}
// listFilesInPath lists all files in a specific path of a HuggingFace repository (recursive helper)
func (c *Client) listFilesInPath(repoID, path string) ([]FileInfo, error) {
baseURL := strings.TrimSuffix(c.baseURL, "/api/models")
var url string
if path == "" {
url = fmt.Sprintf("%s/api/models/%s/tree/main", baseURL, repoID)
} else {
url = fmt.Sprintf("%s/api/models/%s/tree/main/%s", baseURL, repoID, path)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch files. Status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var items []FileInfo
if err := json.Unmarshal(body, &items); err != nil {
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
var allFiles []FileInfo
for _, item := range items {
switch item.Type {
// If it's a directory/folder, recursively list its contents
case "directory", "folder":
// Build the subfolder path
subPath := item.Path
if path != "" {
subPath = fmt.Sprintf("%s/%s", path, item.Path)
}
// Recursively get files from subfolder
// The recursive call will already prepend the subPath to each file's path
subFiles, err := c.listFilesInPath(repoID, subPath)
if err != nil {
return nil, fmt.Errorf("failed to list files in subfolder %s: %w", subPath, err)
}
allFiles = append(allFiles, subFiles...)
case "file":
// It's a file, prepend the current path to make it relative to root
// if path != "" {
// item.Path = fmt.Sprintf("%s/%s", path, item.Path)
// }
allFiles = append(allFiles, item)
}
}
return allFiles, nil
}
// ListFiles lists all files in a HuggingFace repository, including files in subfolders
func (c *Client) ListFiles(repoID string) ([]FileInfo, error) {
return c.listFilesInPath(repoID, "")
}
// GetFileSHA gets the SHA256 checksum for a specific file by searching through the file list
func (c *Client) GetFileSHA(repoID, fileName string) (string, error) {
files, err := c.ListFiles(repoID)
if err != nil {
feat(importer): unify importing code with CLI (#7299) * feat(importer): support ollama and OCI, unify code Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: support importing from local file Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * support also yaml config files Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Correctly handle local files Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Extract importing errors Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add importer tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add integration tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(UX): improve and specify supported URI formats Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fail if backend does not have a runfile Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Adapt tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(gallery): add cache for galleries Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): remove handler duplicate File input handlers are now handled by Alpine.js @change handlers in chat.html. Removed duplicate listeners to prevent files from being processed twice Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): be consistent in attachments in the chat Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fail if no importer matches Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: propagate ops correctly Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-11-19 19:52:11 +00:00
return "", fmt.Errorf("failed to list files while getting SHA: %w", err)
}
for _, file := range files {
if filepath.Base(file.Path) == fileName {
if file.LFS != nil && file.LFS.Oid != "" {
// The LFS OID contains the SHA256 hash
return file.LFS.Oid, nil
}
// If no LFS, return the regular OID
return file.Oid, nil
}
}
return "", fmt.Errorf("file %s not found", fileName)
}
feat(importer): expand importer flow to almost all backends (#9466) * docs(agents): require importer integration when adding backends Document the importer registry workflow so contributors know that adding a new backend also requires updating the /import-model dropdown source: either a new importer in core/gallery/importers/, extending an existing one for drop-in replacements, or the pref-only slice for backends with no reliable auto-detect signal. Always covered by a table-driven test. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for Batch 0 primitives Introduce failing tests that drive Batch 0 of the importer expansion: - pkg/huggingface-api: assert GetModelDetails populates PipelineTag and LibraryName from /api/models/{repo}, and that a failing metadata endpoint still returns file details (best-effort fetch). - core/gallery/importers/helpers_test.go: new table-driven coverage for HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile. - core/gallery/importers/importers_test.go: assert ErrAmbiguousImport sentinel exists and round-trips through errors.Is. - core/gallery/importers/local_test.go: extend with detection cases for ggml-*.bin (whisper), silero_vad.onnx (silero-vad), and the piper .onnx + .onnx.json pair. - core/http/endpoints/localai/import_model_test.go: assert ImportModelURIEndpoint returns HTTP 400 with a structured {error, detail, hint} body when ErrAmbiguousImport surfaces. All tests fail in the expected places (missing fields, missing helpers, missing sentinel, endpoint still wraps as 500). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): Batch 0 foundation — helpers, sentinel, local detection Implements the Batch 0 primitives that subsequent importer batches build on: - pkg/huggingface-api: ModelDetails gains PipelineTag and LibraryName. GetModelDetails now layers a best-effort GET /api/models/{repo} fetch on top of ListFiles — a metadata outage leaves the fields empty but still returns full file details. Uses a dedicated response struct because the single-model endpoint uses snake_case keys while the list endpoint historically returned camelCase. - core/gallery/importers/helpers.go: generic HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile helpers working on []hfapi.ModelFile so per-backend importers can detect artefact patterns without duplicating string wrangling. - core/gallery/importers/importers.go: adds the ErrAmbiguousImport sentinel. DiscoverModelConfig now returns it (wrapped with fmt.Errorf("%w: ...")) when no importer matched AND the HF pipeline_tag falls in a whitelist of narrow modalities (ASR, TTS, sentence-similarity, text-classification, object-detection). The whitelist is intentionally narrow — unknown tags keep the previous "no importer matched" behaviour to avoid blocking rare repos. - core/gallery/importers/local.go: three new local-path detections, inserted before the existing merged-transformers branch: * ggml-*.bin → whisper * silero*.onnx → silero-vad * *.onnx + *.onnx.json pair → piper - core/http/endpoints/localai/import_model.go: ImportModelURIEndpoint surfaces ErrAmbiguousImport as HTTP 400 with {error, detail, hint} JSON, preserving existing behaviour for unrelated errors. Green tests: go test ./core/gallery/importers/... ./pkg/huggingface-api/... \ ./core/http/endpoints/localai/... Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(importers): red tests for KnownBackend endpoint and importer metadata Add failing tests that drive Batch UI-Dropdown: - importers_test.go: assert importers expose Name/Modality/AutoDetects and that LlamaCPPImporter advertises drop-in replacements via a new AdditionalBackendsProvider interface. A Registry() accessor is also expected. - backend_test.go (new): assert GET /backends/known returns []schema.KnownBackend, covers every importer, exposes drop-in llama-cpp replacements, includes curated pref-only backends, has no duplicates, and is sorted by Modality+Name. These tests fail at compile time against master; they are intentionally red so the follow-up green commit is reviewable. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery): add /backends/known endpoint for importer-aware backend list Extend the Importer interface with Name/Modality/AutoDetects so the import system can self-describe its registry, and introduce the AdditionalBackendsProvider interface so importers can advertise drop-in replacements (llama-cpp advertises ik-llama-cpp and turboquant). Expose the new GET /backends/known endpoint that merges: - the importer registry (auto-detect supported), - drop-in replacements hosted by importers (preference-only), - a curated knownPrefOnlyBackends slice for backends with no dedicated importer (sglang, tinygrad, trl, mlx-vlm, whisperx, kokoros, Qwen TTS variants, sam3-cpp) — kept at the top of backend.go so contributors adding a new pref-only backend have one obvious place to edit, - backends installed on disk but unknown to the importer (marked AutoDetect=false, empty Modality). The endpoint deliberately does NOT filter by gallery membership or host capability (unlike /backends/available): LocalAI may auto-install a backend that is not yet present, so the import form dropdown must show everything the importer knows about. Response is deduplicated (importer wins over pref-only) and sorted by Modality+Name for deterministic output. Registered in core/http/routes/localai.go next to /backends/available under the same admin middleware. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui): source import form backend dropdown from /backends/known Replace the hard-coded BACKENDS constant in ImportModel.jsx with a live fetch of /backends/known on mount. Users now see every backend the importer layer knows about (including preference-only entries) grouped by modality, not a stale subset. Changes: - config.js: add backendsKnown endpoint constant next to backendsAvailable. - api.js: add backendsApi.listKnown() wrapper. - ImportModel.jsx: remove BACKENDS constant, fetch the list via useEffect, and derive grouped options via buildBackendOptions. Preference-only entries render with a " (preference-only)" suffix. Loading state disables the dropdown with a "Loading backends…" placeholder; on fetch failure the form falls back to auto-detect only and surfaces a non-blocking toast. - SearchableSelect.jsx: accept items flagged isHeader=true and render them as non-selectable section dividers. Keyboard navigation skips headers and search queries hide them so filtered output stays relevant. Vitest is not set up in this project (devDependencies ship Playwright only). Per the brief's guard-rail, no frontend test framework is introduced; coverage is provided by the Go handler tests that assert the /backends/known contract consumed by the React form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for whisper importer Asserts detection on ggerganov/whisper.cpp (via ggml-*.bin filename), the preferences.backend=whisper override path for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add whisper importer Recognises whisper.cpp GGML models by the "ggml-*.bin" filename convention (direct URL or HF repo member) and by the explicit preferences.backend="whisper" override. Emits backend: whisper with the transcript use-case. Registered before llama-cpp so the narrow filename signal wins before any generic GGUF match is attempted. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for moonshine importer Asserts detection on UsefulSensors/moonshine-tiny via owner + ONNX files, the preferences.backend=moonshine override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add moonshine importer Matches UsefulSensors-owned HF repos whose artefacts or metadata identify them as ASR: on-disk .onnx files (the canonical Moonshine packaging) OR pipeline_tag=automatic-speech-recognition (covers transformers/safetensors-only sibling repos). preferences.backend= moonshine overrides detection. Test uses the live moonshine-tiny repo because the canonical UsefulSensors/moonshine repo currently hits a recursive-subfolder bug in pkg/huggingface-api ListFiles. Registered after WhisperImporter but before LlamaCPPImporter and TransformersImporter so the narrower owner+ASR signal wins before the generic tokenizer.json check routes the repo to transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for nemo importer Asserts detection on nvidia/parakeet-tdt-0.6b-v3 via owner + .nemo file, the preferences.backend=nemo override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add nemo importer Matches nvidia-owned HF repos that ship a .nemo checkpoint archive, the canonical NeMo ASR packaging. preferences.backend=nemo forces detection. Registered between moonshine and llama-cpp so the narrow owner + extension signal wins before any downstream generic matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for faster-whisper importer Asserts detection on Systran/faster-whisper-large-v3 (owner + model.bin + config.json + ASR pipeline), the preferences.backend= faster-whisper override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add faster-whisper importer Recognises CTranslate2-packaged whisper checkpoints distributed for the faster-whisper runtime: model.bin + config.json + ASR pipeline_tag, narrowed to Systran-owned repos or repo names containing "faster-whisper" to avoid falsely claiming vanilla OpenAI whisper HF repos. preferences.backend=faster-whisper overrides detection. Registered before llama-cpp and transformers so the narrow signal wins before tokenizer.json routes the repo to the generic transformers importer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for qwen-asr importer Asserts detection on Qwen/Qwen3-ASR-1.7B via owner + ASR substring in the repo name, the preferences.backend=qwen-asr override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add qwen-asr importer Matches Qwen-owned HF repos whose name contains "ASR" (case-insensitive), routing them to the qwen-asr backend rather than the generic transformers/vllm path. The substring check scans the repo portion only so the owner field cannot leak a false match. preferences.backend=qwen-asr forces detection. Registered before llama-cpp and transformers so the narrow owner+name signal wins. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): ASR ambiguity surfaces ErrAmbiguousImport Locks in the behaviour added in Batch 0: an HF repo whose pipeline_tag marks it as automatic-speech-recognition but whose artefacts match no ASR importer (and no generic importer) must fail with ErrAmbiguousImport so callers know to pass preferences.backend rather than silently guess. pyannote/voice-activity-detection is the fixture — its file list is only config.yaml + README, leaving every importer's artefact check negative. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for piper importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add piper importer Detects piper TTS voices by the canonical <voice>.onnx + <voice>.onnx.json pair packaging (via HasONNXConfigPair). Narrow enough to skip generic ONNX repos used by other backends (Moonshine ASR, sentence-transformers). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for bark importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add bark importer Detects Suno's Bark TTS checkpoints by HF owner "suno" + repo name prefix "bark". Adds HFOwnerRepoFromURI() helper so importers can fall back to URI parsing when pkg/huggingface-api's recursive tree listing errors on repos with nested subdirectories (suno/bark ships a speaker_embeddings/v2 subtree that trips a pre-existing path-doubling bug in the listFilesInPath recursion). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for fish-speech importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add fish-speech importer Detects Fish Audio TTS releases by HF owner "fishaudio" with a URI-based fallback for repos whose tree recursion trips the pre-existing hfapi path-doubling bug. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for outetts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add outetts importer Detects OuteAI's OuteTTS releases by HF owner "OuteAI" or a case- insensitive "OuteTTS" substring in the repo name, with a URI-based fallback for recursion-bugged repos. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for voxcpm importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add voxcpm importer Detects OpenBMB's VoxCPM TTS family by repo-name substring (community mirrors re-host the weights under many owners — mlx-community, bluryar, callgg, etc). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kokoro importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kokoro importer Detects hexgrad's Kokoro TTS by the "Kokoro" repo-name substring paired with a PyTorch .pth/.pt checkpoint — the pairing excludes ONNX-only mirrors (handled by the pref-only `kokoros` Rust runtime) and GGUF mirrors (handled by llama-cpp). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kitten-tts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kitten-tts importer Detects KittenML's kitten-tts releases by owner or "kitten-tts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for neutts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add neutts importer Detects Neuphonic's NeuTTS releases by owner "neuphonic" or "neutts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for chatterbox importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add chatterbox importer Detects Resemble AI's Chatterbox TTS by owner "ResembleAI" or "chatterbox" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vibevoice importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vibevoice importer Detects Microsoft's VibeVoice TTS by "vibevoice" repo-name substring (case-insensitive) so community mirrors still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for coqui importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add coqui importer Detects Coqui AI's TTS releases (XTTS-v2, YourTTS, …) by the authoritative `coqui` HF owner, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): TTS ambiguity surfaces ErrAmbiguousImport Adds a Ginkgo spec that imports nari-labs/Dia-1.6B — a real HF repo carrying pipeline_tag="text-to-speech" whose artefacts (*.pth, one safetensors shard, preprocessor_config.json, config.json) match none of the Batch-2 TTS importers nor the generic text/image importers — and asserts DiscoverModelConfig wraps ErrAmbiguousImport via errors.Is. Also pivots the endpoint-level ambiguity fixture from hexgrad/Kokoro-82M to nari-labs/Dia-1.6B. Batch 2 added a dedicated kokoro importer that now claims the original fixture; Dia remains genuinely unclaimed and so exercises the same ambiguity code path at the HTTP layer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for stablediffusion-ggml importer Covers HF repo detection (city96/FLUX.1-dev-gguf), raw .gguf URL matching on filename arch tokens, preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add stablediffusion-ggml importer Detects GGUF-packed Stable Diffusion and FLUX checkpoints (leejet owner, city96 FLUX mirrors, second-state SD dumps, raw .gguf URLs with arch tokens) and routes them to the stablediffusion-ggml backend. Registered BEFORE LlamaCPPImporter so .gguf image checkpoints are not stolen by llama-cpp's generic .gguf match. Reuses HFOwnerRepoFromURI for the hfapi-recursion-bug fallback. preferences.backend overrides detection. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for ace-step importer Covers HF repo-name detection (ACE-Step/ACE-Step-v1-3.5B), preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add ace-step importer Routes ACE-Step music generation checkpoints (ACE-Step/ACE-Step-v1-3.5B, ACE-Step/Ace-Step1.5, community mirrors) to the ace-step backend. Matching is case-insensitive on the "ace-step" repo-name substring and owner, with an HFOwnerRepoFromURI fallback for the hfapi recursion bug. KnownUsecaseStrings mirrors the gallery's ace-step-turbo entry (sound_generation, tts). preferences.backend overrides. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on text-to-image misses Adds text-to-image to ambiguousModalities whitelist and covers the h94/IP-Adapter-FaceID case — pipeline_tag=text-to-image but ships only .bin/.safetensors so diffusers, stablediffusion-ggml, llama-cpp, transformers, vllm, mlx, and ace-step all miss. DiscoverModelConfig now surfaces ErrAmbiguousImport for that shape instead of the opaque "no importer matched" error. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vllm-omni importer Introduces the test surface for the forthcoming VLLMOmniImporter: detection via preferences.backend, Qwen owner + Omni repo token, URI-only fallback, negative cases (plain Qwen, random OmniX repo), and Import() emitting backend: vllm-omni with chat + multimodal usecases. Includes a registration-order assertion via DiscoverModelConfig to pin the requirement that vllm-omni wins over vllm for Qwen Omni repos (tokenizer files are usually present too). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vllm-omni importer Adds VLLMOmniImporter for Qwen Omni-style multimodal checkpoints (Qwen3-Omni, Qwen2.5-Omni, …). Detection is narrow: HF owner "Qwen" combined with "omni" in the repo name, or a repo name matching the -Omni-/Omni- naming pattern. preferences.backend="vllm-omni" always wins; HFOwnerRepoFromURI provides a URI-only fallback for the hfapi recursion-bug edge case. Emitted YAML sets backend: vllm-omni and known_usecases: [chat, multimodal], matching the gallery/index.yaml vllm-omni entries. The importer is registered ahead of VLLMImporter so Qwen Omni repos — which also carry tokenizer files — route to vllm-omni rather than the plain vllm backend. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for llama-cpp drop-in preferences Pins the expected drop-in replacement behaviour: preferences.backend of ik-llama-cpp or turboquant must swap the emitted YAML backend field while keeping the llama-cpp file layout identical. Also covers the unknown-backend case (must stay llama-cpp) and re-asserts AdditionalBackends() returns the two curated entries with non-empty descriptions. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): llama-cpp honours ik-llama-cpp and turboquant drop-in preferences preferences.backend set to ik-llama-cpp or turboquant now swaps the emitted YAML backend field while leaving the file layout, model path, mmproj handling and everything else in the llama-cpp Import pipeline untouched. Unknown values are ignored and fall back to backend: llama-cpp so arbitrary input can't leak into the config. Aligns the AdditionalBackends() descriptions with the user-facing naming conventions surfaced via /backends/known. No changes to the pref-only curated list in endpoints/localai/backend.go: the two drop-in names have always lived on the importer side via AdditionalBackends. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for silero-vad importer Add the SileroVADImporter test fixtures covering metadata, preference overrides, snakers4 + onnx detection, silero_vad.onnx canonical filename, URI fallback, and live HF discovery. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add silero-vad importer Recognise the Silero VAD ONNX packaging: the canonical silero_vad.onnx filename or any ONNX file under the snakers4 owner. Emits a backend: silero-vad config with the vad known_usecase, and attaches the canonical file entry when present so the weights download on import. Registered before the generic importers so the unique-filename signal takes precedence over any downstream tokenizer-based matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rerankers importer Cover the RerankersImporter contract: interface metadata, preference override, cross-encoder owner detection, case-insensitive 'reranker' substring match (BAAI/bge-reranker, Alibaba-NLP/gte-reranker), URI fallback, and the full-discovery ordering check that a BAAI reranker repo must route to the rerankers importer rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rerankers importer Recognise reranker repositories — cross-encoder owner or any repo whose name contains 'reranker' (case-insensitive). Emits backend: rerankers with reranking: true and the rerank known_usecase. Registered ahead of sentencetransformers and transformers so reranker repos that happen to ship tokenizer.json or modules.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for sentencetransformers importer Cover the SentenceTransformersImporter contract: interface metadata, preference override, modules.json marker file, sentence_bert_config.json marker file, sentence-transformers owner, URI fallback, and the full-discovery ordering check that ensures a sentence-transformers HF URI routes here rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add sentencetransformers importer Recognise sentence-transformers embedding repos by modules.json, sentence_bert_config.json, or the sentence-transformers owner. Emits backend: sentencetransformers with embeddings: true and the embeddings known_usecase. Registered ahead of transformers so ST repos that carry tokenizer.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rfdetr importer Cover the RFDetrImporter contract: interface metadata, preference override, case-insensitive rf-detr and rfdetr substring matches, URI fallback, and negative cases. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rfdetr importer Recognise RF-DETR object-detection repositories by a case-insensitive 'rf-detr' / 'rfdetr' substring in the repo name. Emits backend: rfdetr with the detection known_usecase. Registered ahead of transformers so RF-DETR repos with tokenizer artefacts still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on sentence-similarity misses Add an ambiguity fixture covering the embeddings/rerankers modality. Qdrant/bm25 carries pipeline_tag=sentence-similarity but ships only config.json + stopword .txt files — none of the Batch 5 importers (silero-vad, rerankers, sentencetransformers, rfdetr) or the generic vllm/transformers/llama-cpp/mlx/diffusers importers match. Because the modality is in the ambiguous whitelist, DiscoverModelConfig must surface ErrAmbiguousImport. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/backend): red tests for KnownBackend.Installed flag Extend the /backends/known suite with three failing cases that pin down the forthcoming Installed field: JSON field presence on every entry, flipping to true when an importer-registered backend is also present on disk (and staying false for non-installed pref-only entries), and surfacing system-only backends with empty modality and AutoDetect=false. A small writeFakeSystemBackend helper plants a run.sh under the backends dir so gallery.ListSystemBackends recognises the fixture. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(schema,localai/backend): add Installed flag to KnownBackend Add an Installed bool to schema.KnownBackend and populate it from the /backends/known handler so the React import form can warn users that picking a not-yet-installed backend will trigger an automatic download on submit. Computation: after merging the importer registry, additional backends provider entries and the curated pref-only slice, the handler walks gallery.ListSystemBackends(systemState) and either flips the existing map entry's Installed flag to true (preserving modality / autodetect / description metadata) or inserts a bare {Installed:true} entry for system-only backends the importer layer doesn't know about. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/import_model): structured ambiguous-import response Add red tests covering the extended ambiguity shape the React import form needs: - ImportModelURIEndpoint must return an HTTP 400 body that exposes the detected `modality` (normalised to the importer modality key, e.g. "tts" for pipeline_tag=text-to-speech) and a list of `candidates` (backend names filtered by modality, excluding text-LLM backends). - The importers package must surface a typed AmbiguousImportError so HTTP consumers can read Modality + Candidates without parsing the error string. errors.Is against the existing sentinel keeps working. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(localai/import_model): structured ambiguity response with modality + candidates DiscoverModelConfig now returns a typed AmbiguousImportError that carries the importer modality key, candidate backend names, the original URI, and the raw HF pipeline_tag. Its Is() preserves errors.Is(err, ErrAmbiguousImport) for legacy callers. The importer modality is pre-mapped from the HF pipeline_tag (automatic-speech-recognition → asr, text-to-speech → tts, etc) via PipelineTagToModality — surfaced as an exported helper so downstream consumers can avoid duplicating the table. CandidatesForModality filters the default importer registry plus AdditionalBackendsProvider drop-ins by modality, sorts deterministically, and is the single source of truth used by ImportModelURIEndpoint. ImportModelURIEndpoint now returns HTTP 400 with { error, detail, modality, candidates, hint } when ambiguity fires, letting the React form render a modality-scoped picker inline instead of a generic toast. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): manual pick badge + tooltip Red Playwright coverage for the preference-only → manual pick rename: - The Backend dropdown renders a "manual pick" badge on every option whose KnownBackend.auto_detect is false. - The badge carries a title attribute with hover-tooltip copy that explains auto-detect won't route to this backend. - Auto-detectable backends must NOT carry the badge. - The legacy " (preference-only)" suffix is gone from every label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): replace preference-only suffix with manual pick badge SearchableSelect option rows now support an optional badge field — a muted pill rendered to the right of the label with an optional title attribute for native hover tooltips. Plain text so screen readers read it alongside the option name. buildBackendOptions in ImportModel stops appending " (preference-only)" to the label and instead sets badge="manual pick" plus a descriptive tooltip on every option whose auto_detect is false. The Backend help text explains what "manual pick" means so users aren't left wondering about the badge. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): inline ambiguity picker Red Playwright coverage for Batch A2 — when the server returns a 400 ambiguity body, the form must render an inline alert instead of a toast, expose one clickable chip per candidate backend, and support both auto-resubmit on pick and silent dismiss. - Mocks /api/models/import-uri with the structured ambiguity body (error, detail, modality, candidates, hint). - On first click of Import, the alert is visible, carries modality-specific copy, and shows a chip per candidate. - Clicking a chip clears the alert, sets the Backend dropdown, and triggers a second POST to /api/models/import-uri. - Dismissing the alert leaves the Backend dropdown on Auto-detect — no implicit backend assignment. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): inline ambiguity alert with candidate chips Adds AmbiguityAlert — a soft, info-coloured card rendered above the URI input when the server returns a structured 400 with { modality, candidates }. Message is modality-aware (tts/asr/embeddings/image/ reranker/detection get purpose-written copy, everything else falls back to a generic template). Each candidate is a clickable chip that shows a download icon when /backends/known marks the backend as not yet installed, so users aren't surprised by an implicit install. ImportModel wires the alert to handleSimpleImport's error path: - api.handleResponse now attaches { status, body } to the thrown Error so pages can pattern-match on structured responses instead of string error messages. - handleSimpleImport detects `status === 400 && body.error === 'ambiguous import'` and flips into the inline-picker mode instead of toasting. - Clicking a chip sets prefs.backend and auto-resubmits (passing the picked backend as an override so setPrefs's asynchrony doesn't leak a stale value). - Dismissing clears the alert; changing the URI or the backend also clears it so a stale alert never sticks around. Test fixtures mock GET /backends/known + POST /models/import-uri so the Playwright specs don't depend on real network reachability. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): auto-install warning Red Playwright coverage for Batch A3 — when the user picks a backend whose KnownBackend.installed is false, the form must render a muted inline note under the Backend dropdown warning that submitting will download the backend first. Picking an installed backend or leaving Auto-detect selected must keep the note hidden. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): auto-install warning under backend dropdown When the user picks a backend whose KnownBackend.installed is false, render a muted inline note under the Backend dropdown's help text warning that submitting will download the backend first. The note lives inside the same form-group so it lines up with the existing hint text; it's hidden when Auto-detect is selected (the selected backend is unknowable at that point) or when the chosen backend is already on disk. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): drop redundant section header, adjust icons, rename HF shortcut - Remove the "Import from URI" card-level <h2> — the page title already says "Import New Model" one row up, so the secondary header was duplicating information. - Swap the fa-star on "Common Preferences" for fa-sliders (stars imply favourites/ratings; this is just a preferences block) and move the Custom Preferences fa-sliders-h to fa-plus-circle so the two blocks read as distinct rather than as two sliders. - Rename the HF shortcut from "Search GGUF on HF" → "Browse models on HF" and drop the `search=gguf` filter on the linked URL. The import form now supports ~40 backends; hard-coding GGUF in the copy no longer matches the form's actual reach. - Pure polish — no behaviour change, covered by the existing Batch A Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): batch B — simple/power switch, options, tabs, dialog Adds a failing Playwright suite covering the full Batch B surface ahead of implementation: - B1: SimplePowerSwitch segmented control renders, toggles, persists to localStorage across reloads. - B2: Simple-mode Options disclosure is collapsed by default; expanding exposes only Backend, Model Name, Description (no quantizations, mmproj, model type, or custom prefs). - B3: Power mode has Preferences and YAML tabs with a persistent selection across reloads; URI/name/description typed in Simple carry over to Power; YAML tab swaps the primary action to Create. - B4: Switching Power -> Simple with a custom preference set triggers the 3-button confirmation dialog (Keep / Discard / Cancel) with the documented semantics. Tests fail against master — implementation lands in the following commits. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add SimplePowerSwitch segmented control Replaces the previous "Advanced Mode / Simple Mode" toggle button in the page header with a two-segment control that flips between Simple and Power. The control reuses the existing .segmented CSS shared with the Sound page for visual consistency. Mode state is persisted to localStorage under `import-form-mode` so reloads land on the same view (default: simple). The boolean alias `isAdvancedMode` is retained internally to minimise diff — subsequent commits reshape the Simple and Power surfaces independently. Closes B1 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): simple mode collapsible options, power tabs, switch dialog Completes the Batch B surface in a single structural pass so Simple and Power mode can evolve independently: Simple mode - URI input + Ambiguity alert + Import button, plus a collapsible "Options" disclosure that exposes ONLY Backend, Model Name, Description. Quantizations / MMProj / Model Type / Diffusers fields / Custom Preferences are no longer rendered in Simple mode. Power mode - In-page segmented "Preferences · YAML" tab strip. Active tab persists to localStorage under `import-form-power-tab`. - Preferences tab = the full existing preferences + custom prefs panel (no progressive disclosure yet — that's Batch D). - YAML tab = the existing CodeEditor. Primary button reads "Create" here, "Import Model" everywhere else. Switch dialog - Power -> Simple with non-default prefs (advanced pref keys set, any custom-pref key non-empty, or YAML edited away from the template) opens a 3-button dialog: Keep & switch / Discard & switch / Cancel. - Keep preserves all state. Discard resets prefs + customPrefs + YAML to defaults. Cancel leaves the user in Power mode. Page subtitle reflects the current surface (Simple, Power/Preferences, Power/YAML). Estimate banner renders everywhere except Power/YAML. Closes B2/B3/B4 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): expand Options disclosure in Batch A tests Batch B hid the Backend dropdown behind a collapsible Options disclosure in Simple mode. The Batch A tests that exercise the dropdown directly (manual-pick badge, ambiguity chip sets the selected backend, auto- install warning) now click the disclosure toggle before asserting on dropdown contents. Test intent is unchanged. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): strip decorative icons from field labels The preference panel had 12 Font Awesome icons decorating field labels (Backend, Model Name, Description, Quantizations, MMProj Quantizations, Model Type, Pipeline Type, Scheduler Type, Enable Parameters, Embeddings, CUDA, plus fa-link on Model URI). Every label screamed equally, flattening the visual hierarchy. Remove them. Keep icons where they carry meaning: page-level section headers, URI format guide entries, primary buttons, the Simple-mode Options disclosure, the ambiguity alert's fa-lightbulb, the auto-install note's fa-download, and the Estimated-requirements banner's fa-memory / fa-microchip / fa-download. No new behaviour, no layout / spacing changes beyond removing the orphaned icon margin. Playwright suite green. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): progressive disclosure of preference fields Cover the Batch D visibility matrix for Power > Preferences: Quantizations, MMProj Quantizations, and Model Type each render only for the backends that can consume them, stay visible when the backend is unset, and preserve any value the user already typed when toggled off and back on. Also pin the shrunk Description textarea at rows=2. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): progressive disclosure + shorter description textarea Gate Quantizations, MMProj Quantizations, and Model Type in the Power > Preferences tab so each field only renders for the backends that can actually consume it. Backend unset keeps everything visible. Hidden fields' state is preserved (the JSX wrapper is guarded, not the underlying prefs state) so users flipping backends back and forth don't lose input. Also shrink the Description textarea from rows=3 to rows=2 — it's shared between Simple Options and Power Preferences so the change applies to both. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): enter-to-submit in Simple mode Red test for Batch F3 — pressing Enter in the URI input must POST /models/import-uri, and Enter in the Description textarea must insert a newline without submitting the form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): enter-to-submit in Simple mode Wrap the Simple-mode URI input + ambiguity alert + Options disclosure in a <form> whose onSubmit calls handleSimpleImport. Pressing Enter in the URI input (or any Simple-mode text input) now submits the import without having to move the mouse to the header button. The Description textarea keeps its native behaviour — Enter inserts a newline. A hidden submit button is included because the visible Import button lives outside the form in the page header; some browsers only fire implicit Enter-submit when the form contains a submit-capable element. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import,SearchableSelect,components): aria-hidden on decorative icons Every Font Awesome icon in the import form is decorative — its meaning is already conveyed by adjacent visible text. Adding aria-hidden="true" prevents screen readers from announcing the unicode glyph point as content. Covers ImportModel.jsx (all remaining <i> glyphs) and SearchableSelect.jsx (the trigger chevron). AmbiguityAlert and SimplePowerSwitch already set aria-hidden on their icons when the components landed in Batches A and B — no change needed there. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(SearchableSelect): responsive dropdown maxHeight + hover focus guard F2 — replace fixed pixel heights with min(pixel, vh) so the dropdown and its inner scroll region don't overflow short viewports. Outer container: 260px -> min(260px, 60vh); inner listbox: 200px -> min(200px, 50vh). Tall viewports still get the original pixel caps. F5 — short-circuit onMouseEnter when the hovered row is already the focused row. Avoids queueing a setFocusIndex call (and a render) for every mousemove inside the same item — the state would be identical. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): aria-label on custom preference rows The Key / Value inputs and trash button in each Custom Preferences row previously relied on placeholder text alone. Placeholders are not accessible names — they vanish on input and screen readers do not announce them consistently. Add row-indexed aria-labels so assistive tech can distinguish "Preference key for row 1" from "row 2", and give the trash button an explicit "Remove this preference" label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): modality chip row Red tests for Batch E — a horizontal modality chip row that filters the Backend dropdown by modality. Covers visibility in Simple-mode Options and Power/Preferences (and absence in Power/YAML), filter behaviour, mismatched-backend clearing with toast, ambiguity-alert auto-selection, and radiogroup keyboard navigation. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add ModalityChips component + filter integration Horizontal chip row (Any, Text, Speech, TTS, Image, Embeddings, Rerankers, Detection, VAD) filters the Backend dropdown options to the selected modality. Default is Any — no filter, current behaviour. - New ModalityChips component (radiogroup pattern, roving tabindex, arrow-key navigation, Home/End). - buildBackendOptions now accepts an optional modalityFilter so grouped output is narrowed before rendering. - Chips render inside Simple-mode Options disclosure and Power > Preferences tab. Power > YAML stays unaffected. - Switching the filter drops a mismatched backend selection and surfaces a toast so the auto-clear is visible. - Ambiguity alerts auto-activate the matching chip so users see only relevant backends even if they dismiss the alert. Tightens the Batch E tests' option-matching to the label <span> so the "↵" keybind hint on the focused row doesn't break accessible-name lookups. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * fix(ui/import): rename Power to Advanced + stop URI-formats toggle from submitting form The "Supported URI Formats" disclosure button inside the Simple-mode form lacked an explicit type attribute, so it defaulted to type="submit". Every click triggered the form's onSubmit and surfaced the empty-URI validation toast ("Please enter a model URI"). Marking it type="button" lets it behave as a pure toggle. While here, rename the user-visible "Power" label to "Advanced" in the mode switch (button text + tooltip) and the Power-mode tab's aria-label, matching the term users actually expect. The internal mode key stays 'power' so tests, localStorage, and data-testid selectors are untouched. Assisted-by: Claude:claude-opus-4-7 * fix(system): fall back to cpu when meta backend lacks default capability Meta backends like vllm and sglang enumerate concrete variants for nvidia/amd/intel/cpu but omit a default: catch-all entry. On a no-GPU host the reported capability is "default", so the previous Capability() returned "default" unconditionally on a miss — IsCompatibleWith then saw no "default" key and filtered the meta out of AvailableBackends. The import flow's auto-install step then failed with "no backend found with name <meta>", contradicting the UI's promise that the backend would be downloaded on demand. Try the explicit "default" key first, then fall back to "cpu" before giving up. vllm now resolves to cpu-vllm on CPU-only Linux without touching the gallery YAML. Assisted-by: Claude:claude-opus-4-7
2026-04-22 20:42:37 +00:00
// modelMetadataResponse mirrors the subset of fields returned by
// GET /api/models/{repoID} that we care about. The public HF endpoint uses
// snake_case (pipeline_tag, library_name) while the list endpoint used by
// SearchModels historically returned camelCase — hence the dedicated struct
// rather than reusing Model.
type modelMetadataResponse struct {
PipelineTag string `json:"pipeline_tag"`
LibraryName string `json:"library_name"`
}
// fetchModelMetadata hits GET /api/models/{repoID} to retrieve high-level
// model metadata such as pipeline_tag and library_name. Best-effort: a non-
// 200 response or transport error returns a zero value and a nil error so
// callers can proceed with file-only data.
func (c *Client) fetchModelMetadata(repoID string) (modelMetadataResponse, error) {
baseURL := strings.TrimSuffix(c.baseURL, "/api/models")
url := fmt.Sprintf("%s/api/models/%s", baseURL, repoID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return modelMetadataResponse{}, err
}
resp, err := c.client.Do(req)
if err != nil {
return modelMetadataResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return modelMetadataResponse{}, nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return modelMetadataResponse{}, err
}
var m modelMetadataResponse
if err := json.Unmarshal(body, &m); err != nil {
return modelMetadataResponse{}, err
}
return m, nil
}
// GetModelDetails gets detailed information about a model including files and checksums
func (c *Client) GetModelDetails(repoID string) (*ModelDetails, error) {
files, err := c.ListFiles(repoID)
if err != nil {
return nil, fmt.Errorf("failed to list files: %w", err)
}
details := &ModelDetails{
ModelID: repoID,
Author: strings.Split(repoID, "/")[0],
Files: make([]ModelFile, 0, len(files)),
}
feat(importer): expand importer flow to almost all backends (#9466) * docs(agents): require importer integration when adding backends Document the importer registry workflow so contributors know that adding a new backend also requires updating the /import-model dropdown source: either a new importer in core/gallery/importers/, extending an existing one for drop-in replacements, or the pref-only slice for backends with no reliable auto-detect signal. Always covered by a table-driven test. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for Batch 0 primitives Introduce failing tests that drive Batch 0 of the importer expansion: - pkg/huggingface-api: assert GetModelDetails populates PipelineTag and LibraryName from /api/models/{repo}, and that a failing metadata endpoint still returns file details (best-effort fetch). - core/gallery/importers/helpers_test.go: new table-driven coverage for HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile. - core/gallery/importers/importers_test.go: assert ErrAmbiguousImport sentinel exists and round-trips through errors.Is. - core/gallery/importers/local_test.go: extend with detection cases for ggml-*.bin (whisper), silero_vad.onnx (silero-vad), and the piper .onnx + .onnx.json pair. - core/http/endpoints/localai/import_model_test.go: assert ImportModelURIEndpoint returns HTTP 400 with a structured {error, detail, hint} body when ErrAmbiguousImport surfaces. All tests fail in the expected places (missing fields, missing helpers, missing sentinel, endpoint still wraps as 500). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): Batch 0 foundation — helpers, sentinel, local detection Implements the Batch 0 primitives that subsequent importer batches build on: - pkg/huggingface-api: ModelDetails gains PipelineTag and LibraryName. GetModelDetails now layers a best-effort GET /api/models/{repo} fetch on top of ListFiles — a metadata outage leaves the fields empty but still returns full file details. Uses a dedicated response struct because the single-model endpoint uses snake_case keys while the list endpoint historically returned camelCase. - core/gallery/importers/helpers.go: generic HasFile, HasExtension, HasONNX, HasONNXConfigPair, HasGGMLFile helpers working on []hfapi.ModelFile so per-backend importers can detect artefact patterns without duplicating string wrangling. - core/gallery/importers/importers.go: adds the ErrAmbiguousImport sentinel. DiscoverModelConfig now returns it (wrapped with fmt.Errorf("%w: ...")) when no importer matched AND the HF pipeline_tag falls in a whitelist of narrow modalities (ASR, TTS, sentence-similarity, text-classification, object-detection). The whitelist is intentionally narrow — unknown tags keep the previous "no importer matched" behaviour to avoid blocking rare repos. - core/gallery/importers/local.go: three new local-path detections, inserted before the existing merged-transformers branch: * ggml-*.bin → whisper * silero*.onnx → silero-vad * *.onnx + *.onnx.json pair → piper - core/http/endpoints/localai/import_model.go: ImportModelURIEndpoint surfaces ErrAmbiguousImport as HTTP 400 with {error, detail, hint} JSON, preserving existing behaviour for unrelated errors. Green tests: go test ./core/gallery/importers/... ./pkg/huggingface-api/... \ ./core/http/endpoints/localai/... Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(importers): red tests for KnownBackend endpoint and importer metadata Add failing tests that drive Batch UI-Dropdown: - importers_test.go: assert importers expose Name/Modality/AutoDetects and that LlamaCPPImporter advertises drop-in replacements via a new AdditionalBackendsProvider interface. A Registry() accessor is also expected. - backend_test.go (new): assert GET /backends/known returns []schema.KnownBackend, covers every importer, exposes drop-in llama-cpp replacements, includes curated pref-only backends, has no duplicates, and is sorted by Modality+Name. These tests fail at compile time against master; they are intentionally red so the follow-up green commit is reviewable. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery): add /backends/known endpoint for importer-aware backend list Extend the Importer interface with Name/Modality/AutoDetects so the import system can self-describe its registry, and introduce the AdditionalBackendsProvider interface so importers can advertise drop-in replacements (llama-cpp advertises ik-llama-cpp and turboquant). Expose the new GET /backends/known endpoint that merges: - the importer registry (auto-detect supported), - drop-in replacements hosted by importers (preference-only), - a curated knownPrefOnlyBackends slice for backends with no dedicated importer (sglang, tinygrad, trl, mlx-vlm, whisperx, kokoros, Qwen TTS variants, sam3-cpp) — kept at the top of backend.go so contributors adding a new pref-only backend have one obvious place to edit, - backends installed on disk but unknown to the importer (marked AutoDetect=false, empty Modality). The endpoint deliberately does NOT filter by gallery membership or host capability (unlike /backends/available): LocalAI may auto-install a backend that is not yet present, so the import form dropdown must show everything the importer knows about. Response is deduplicated (importer wins over pref-only) and sorted by Modality+Name for deterministic output. Registered in core/http/routes/localai.go next to /backends/available under the same admin middleware. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui): source import form backend dropdown from /backends/known Replace the hard-coded BACKENDS constant in ImportModel.jsx with a live fetch of /backends/known on mount. Users now see every backend the importer layer knows about (including preference-only entries) grouped by modality, not a stale subset. Changes: - config.js: add backendsKnown endpoint constant next to backendsAvailable. - api.js: add backendsApi.listKnown() wrapper. - ImportModel.jsx: remove BACKENDS constant, fetch the list via useEffect, and derive grouped options via buildBackendOptions. Preference-only entries render with a " (preference-only)" suffix. Loading state disables the dropdown with a "Loading backends…" placeholder; on fetch failure the form falls back to auto-detect only and surfaces a non-blocking toast. - SearchableSelect.jsx: accept items flagged isHeader=true and render them as non-selectable section dividers. Keyboard navigation skips headers and search queries hide them so filtered output stays relevant. Vitest is not set up in this project (devDependencies ship Playwright only). Per the brief's guard-rail, no frontend test framework is introduced; coverage is provided by the Go handler tests that assert the /backends/known contract consumed by the React form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for whisper importer Asserts detection on ggerganov/whisper.cpp (via ggml-*.bin filename), the preferences.backend=whisper override path for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add whisper importer Recognises whisper.cpp GGML models by the "ggml-*.bin" filename convention (direct URL or HF repo member) and by the explicit preferences.backend="whisper" override. Emits backend: whisper with the transcript use-case. Registered before llama-cpp so the narrow filename signal wins before any generic GGUF match is attempted. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for moonshine importer Asserts detection on UsefulSensors/moonshine-tiny via owner + ONNX files, the preferences.backend=moonshine override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add moonshine importer Matches UsefulSensors-owned HF repos whose artefacts or metadata identify them as ASR: on-disk .onnx files (the canonical Moonshine packaging) OR pipeline_tag=automatic-speech-recognition (covers transformers/safetensors-only sibling repos). preferences.backend= moonshine overrides detection. Test uses the live moonshine-tiny repo because the canonical UsefulSensors/moonshine repo currently hits a recursive-subfolder bug in pkg/huggingface-api ListFiles. Registered after WhisperImporter but before LlamaCPPImporter and TransformersImporter so the narrower owner+ASR signal wins before the generic tokenizer.json check routes the repo to transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for nemo importer Asserts detection on nvidia/parakeet-tdt-0.6b-v3 via owner + .nemo file, the preferences.backend=nemo override for arbitrary URIs, and the Importer interface metadata (name/modality/autodetect). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add nemo importer Matches nvidia-owned HF repos that ship a .nemo checkpoint archive, the canonical NeMo ASR packaging. preferences.backend=nemo forces detection. Registered between moonshine and llama-cpp so the narrow owner + extension signal wins before any downstream generic matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for faster-whisper importer Asserts detection on Systran/faster-whisper-large-v3 (owner + model.bin + config.json + ASR pipeline), the preferences.backend= faster-whisper override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add faster-whisper importer Recognises CTranslate2-packaged whisper checkpoints distributed for the faster-whisper runtime: model.bin + config.json + ASR pipeline_tag, narrowed to Systran-owned repos or repo names containing "faster-whisper" to avoid falsely claiming vanilla OpenAI whisper HF repos. preferences.backend=faster-whisper overrides detection. Registered before llama-cpp and transformers so the narrow signal wins before tokenizer.json routes the repo to the generic transformers importer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for qwen-asr importer Asserts detection on Qwen/Qwen3-ASR-1.7B via owner + ASR substring in the repo name, the preferences.backend=qwen-asr override for arbitrary URIs, and the Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add qwen-asr importer Matches Qwen-owned HF repos whose name contains "ASR" (case-insensitive), routing them to the qwen-asr backend rather than the generic transformers/vllm path. The substring check scans the repo portion only so the owner field cannot leak a false match. preferences.backend=qwen-asr forces detection. Registered before llama-cpp and transformers so the narrow owner+name signal wins. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): ASR ambiguity surfaces ErrAmbiguousImport Locks in the behaviour added in Batch 0: an HF repo whose pipeline_tag marks it as automatic-speech-recognition but whose artefacts match no ASR importer (and no generic importer) must fail with ErrAmbiguousImport so callers know to pass preferences.backend rather than silently guess. pyannote/voice-activity-detection is the fixture — its file list is only config.yaml + README, leaving every importer's artefact check negative. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for piper importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add piper importer Detects piper TTS voices by the canonical <voice>.onnx + <voice>.onnx.json pair packaging (via HasONNXConfigPair). Narrow enough to skip generic ONNX repos used by other backends (Moonshine ASR, sentence-transformers). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for bark importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add bark importer Detects Suno's Bark TTS checkpoints by HF owner "suno" + repo name prefix "bark". Adds HFOwnerRepoFromURI() helper so importers can fall back to URI parsing when pkg/huggingface-api's recursive tree listing errors on repos with nested subdirectories (suno/bark ships a speaker_embeddings/v2 subtree that trips a pre-existing path-doubling bug in the listFilesInPath recursion). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for fish-speech importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add fish-speech importer Detects Fish Audio TTS releases by HF owner "fishaudio" with a URI-based fallback for repos whose tree recursion trips the pre-existing hfapi path-doubling bug. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for outetts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add outetts importer Detects OuteAI's OuteTTS releases by HF owner "OuteAI" or a case- insensitive "OuteTTS" substring in the repo name, with a URI-based fallback for recursion-bugged repos. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for voxcpm importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add voxcpm importer Detects OpenBMB's VoxCPM TTS family by repo-name substring (community mirrors re-host the weights under many owners — mlx-community, bluryar, callgg, etc). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kokoro importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kokoro importer Detects hexgrad's Kokoro TTS by the "Kokoro" repo-name substring paired with a PyTorch .pth/.pt checkpoint — the pairing excludes ONNX-only mirrors (handled by the pref-only `kokoros` Rust runtime) and GGUF mirrors (handled by llama-cpp). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for kitten-tts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add kitten-tts importer Detects KittenML's kitten-tts releases by owner or "kitten-tts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for neutts importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add neutts importer Detects Neuphonic's NeuTTS releases by owner "neuphonic" or "neutts" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for chatterbox importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add chatterbox importer Detects Resemble AI's Chatterbox TTS by owner "ResembleAI" or "chatterbox" repo-name substring, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vibevoice importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vibevoice importer Detects Microsoft's VibeVoice TTS by "vibevoice" repo-name substring (case-insensitive) so community mirrors still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for coqui importer Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add coqui importer Detects Coqui AI's TTS releases (XTTS-v2, YourTTS, …) by the authoritative `coqui` HF owner, with URI-parsing fallback. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): TTS ambiguity surfaces ErrAmbiguousImport Adds a Ginkgo spec that imports nari-labs/Dia-1.6B — a real HF repo carrying pipeline_tag="text-to-speech" whose artefacts (*.pth, one safetensors shard, preprocessor_config.json, config.json) match none of the Batch-2 TTS importers nor the generic text/image importers — and asserts DiscoverModelConfig wraps ErrAmbiguousImport via errors.Is. Also pivots the endpoint-level ambiguity fixture from hexgrad/Kokoro-82M to nari-labs/Dia-1.6B. Batch 2 added a dedicated kokoro importer that now claims the original fixture; Dia remains genuinely unclaimed and so exercises the same ambiguity code path at the HTTP layer. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for stablediffusion-ggml importer Covers HF repo detection (city96/FLUX.1-dev-gguf), raw .gguf URL matching on filename arch tokens, preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add stablediffusion-ggml importer Detects GGUF-packed Stable Diffusion and FLUX checkpoints (leejet owner, city96 FLUX mirrors, second-state SD dumps, raw .gguf URLs with arch tokens) and routes them to the stablediffusion-ggml backend. Registered BEFORE LlamaCPPImporter so .gguf image checkpoints are not stolen by llama-cpp's generic .gguf match. Reuses HFOwnerRepoFromURI for the hfapi-recursion-bug fallback. preferences.backend overrides detection. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for ace-step importer Covers HF repo-name detection (ACE-Step/ACE-Step-v1-3.5B), preference override, and Importer interface metadata. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add ace-step importer Routes ACE-Step music generation checkpoints (ACE-Step/ACE-Step-v1-3.5B, ACE-Step/Ace-Step1.5, community mirrors) to the ace-step backend. Matching is case-insensitive on the "ace-step" repo-name substring and owner, with an HFOwnerRepoFromURI fallback for the hfapi recursion bug. KnownUsecaseStrings mirrors the gallery's ace-step-turbo entry (sound_generation, tts). preferences.backend overrides. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on text-to-image misses Adds text-to-image to ambiguousModalities whitelist and covers the h94/IP-Adapter-FaceID case — pipeline_tag=text-to-image but ships only .bin/.safetensors so diffusers, stablediffusion-ggml, llama-cpp, transformers, vllm, mlx, and ace-step all miss. DiscoverModelConfig now surfaces ErrAmbiguousImport for that shape instead of the opaque "no importer matched" error. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for vllm-omni importer Introduces the test surface for the forthcoming VLLMOmniImporter: detection via preferences.backend, Qwen owner + Omni repo token, URI-only fallback, negative cases (plain Qwen, random OmniX repo), and Import() emitting backend: vllm-omni with chat + multimodal usecases. Includes a registration-order assertion via DiscoverModelConfig to pin the requirement that vllm-omni wins over vllm for Qwen Omni repos (tokenizer files are usually present too). Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add vllm-omni importer Adds VLLMOmniImporter for Qwen Omni-style multimodal checkpoints (Qwen3-Omni, Qwen2.5-Omni, …). Detection is narrow: HF owner "Qwen" combined with "omni" in the repo name, or a repo name matching the -Omni-/Omni- naming pattern. preferences.backend="vllm-omni" always wins; HFOwnerRepoFromURI provides a URI-only fallback for the hfapi recursion-bug edge case. Emitted YAML sets backend: vllm-omni and known_usecases: [chat, multimodal], matching the gallery/index.yaml vllm-omni entries. The importer is registered ahead of VLLMImporter so Qwen Omni repos — which also carry tokenizer files — route to vllm-omni rather than the plain vllm backend. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for llama-cpp drop-in preferences Pins the expected drop-in replacement behaviour: preferences.backend of ik-llama-cpp or turboquant must swap the emitted YAML backend field while keeping the llama-cpp file layout identical. Also covers the unknown-backend case (must stay llama-cpp) and re-asserts AdditionalBackends() returns the two curated entries with non-empty descriptions. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): llama-cpp honours ik-llama-cpp and turboquant drop-in preferences preferences.backend set to ik-llama-cpp or turboquant now swaps the emitted YAML backend field while leaving the file layout, model path, mmproj handling and everything else in the llama-cpp Import pipeline untouched. Unknown values are ignored and fall back to backend: llama-cpp so arbitrary input can't leak into the config. Aligns the AdditionalBackends() descriptions with the user-facing naming conventions surfaced via /backends/known. No changes to the pref-only curated list in endpoints/localai/backend.go: the two drop-in names have always lived on the importer side via AdditionalBackends. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for silero-vad importer Add the SileroVADImporter test fixtures covering metadata, preference overrides, snakers4 + onnx detection, silero_vad.onnx canonical filename, URI fallback, and live HF discovery. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add silero-vad importer Recognise the Silero VAD ONNX packaging: the canonical silero_vad.onnx filename or any ONNX file under the snakers4 owner. Emits a backend: silero-vad config with the vad known_usecase, and attaches the canonical file entry when present so the weights download on import. Registered before the generic importers so the unique-filename signal takes precedence over any downstream tokenizer-based matcher. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rerankers importer Cover the RerankersImporter contract: interface metadata, preference override, cross-encoder owner detection, case-insensitive 'reranker' substring match (BAAI/bge-reranker, Alibaba-NLP/gte-reranker), URI fallback, and the full-discovery ordering check that a BAAI reranker repo must route to the rerankers importer rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rerankers importer Recognise reranker repositories — cross-encoder owner or any repo whose name contains 'reranker' (case-insensitive). Emits backend: rerankers with reranking: true and the rerank known_usecase. Registered ahead of sentencetransformers and transformers so reranker repos that happen to ship tokenizer.json or modules.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for sentencetransformers importer Cover the SentenceTransformersImporter contract: interface metadata, preference override, modules.json marker file, sentence_bert_config.json marker file, sentence-transformers owner, URI fallback, and the full-discovery ordering check that ensures a sentence-transformers HF URI routes here rather than transformers. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add sentencetransformers importer Recognise sentence-transformers embedding repos by modules.json, sentence_bert_config.json, or the sentence-transformers owner. Emits backend: sentencetransformers with embeddings: true and the embeddings known_usecase. Registered ahead of transformers so ST repos that carry tokenizer.json still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): add failing tests for rfdetr importer Cover the RFDetrImporter contract: interface metadata, preference override, case-insensitive rf-detr and rfdetr substring matches, URI fallback, and negative cases. Implementation follows in the next commit. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(gallery/importers): add rfdetr importer Recognise RF-DETR object-detection repositories by a case-insensitive 'rf-detr' / 'rfdetr' substring in the repo name. Emits backend: rfdetr with the detection known_usecase. Registered ahead of transformers so RF-DETR repos with tokenizer artefacts still route here. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(gallery/importers): surface ErrAmbiguousImport on sentence-similarity misses Add an ambiguity fixture covering the embeddings/rerankers modality. Qdrant/bm25 carries pipeline_tag=sentence-similarity but ships only config.json + stopword .txt files — none of the Batch 5 importers (silero-vad, rerankers, sentencetransformers, rfdetr) or the generic vllm/transformers/llama-cpp/mlx/diffusers importers match. Because the modality is in the ambiguous whitelist, DiscoverModelConfig must surface ErrAmbiguousImport. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/backend): red tests for KnownBackend.Installed flag Extend the /backends/known suite with three failing cases that pin down the forthcoming Installed field: JSON field presence on every entry, flipping to true when an importer-registered backend is also present on disk (and staying false for non-installed pref-only entries), and surfacing system-only backends with empty modality and AutoDetect=false. A small writeFakeSystemBackend helper plants a run.sh under the backends dir so gallery.ListSystemBackends recognises the fixture. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(schema,localai/backend): add Installed flag to KnownBackend Add an Installed bool to schema.KnownBackend and populate it from the /backends/known handler so the React import form can warn users that picking a not-yet-installed backend will trigger an automatic download on submit. Computation: after merging the importer registry, additional backends provider entries and the curated pref-only slice, the handler walks gallery.ListSystemBackends(systemState) and either flips the existing map entry's Installed flag to true (preserving modality / autodetect / description metadata) or inserts a bare {Installed:true} entry for system-only backends the importer layer doesn't know about. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(localai/import_model): structured ambiguous-import response Add red tests covering the extended ambiguity shape the React import form needs: - ImportModelURIEndpoint must return an HTTP 400 body that exposes the detected `modality` (normalised to the importer modality key, e.g. "tts" for pipeline_tag=text-to-speech) and a list of `candidates` (backend names filtered by modality, excluding text-LLM backends). - The importers package must surface a typed AmbiguousImportError so HTTP consumers can read Modality + Candidates without parsing the error string. errors.Is against the existing sentinel keeps working. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(localai/import_model): structured ambiguity response with modality + candidates DiscoverModelConfig now returns a typed AmbiguousImportError that carries the importer modality key, candidate backend names, the original URI, and the raw HF pipeline_tag. Its Is() preserves errors.Is(err, ErrAmbiguousImport) for legacy callers. The importer modality is pre-mapped from the HF pipeline_tag (automatic-speech-recognition → asr, text-to-speech → tts, etc) via PipelineTagToModality — surfaced as an exported helper so downstream consumers can avoid duplicating the table. CandidatesForModality filters the default importer registry plus AdditionalBackendsProvider drop-ins by modality, sorts deterministically, and is the single source of truth used by ImportModelURIEndpoint. ImportModelURIEndpoint now returns HTTP 400 with { error, detail, modality, candidates, hint } when ambiguity fires, letting the React form render a modality-scoped picker inline instead of a generic toast. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): manual pick badge + tooltip Red Playwright coverage for the preference-only → manual pick rename: - The Backend dropdown renders a "manual pick" badge on every option whose KnownBackend.auto_detect is false. - The badge carries a title attribute with hover-tooltip copy that explains auto-detect won't route to this backend. - Auto-detectable backends must NOT carry the badge. - The legacy " (preference-only)" suffix is gone from every label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): replace preference-only suffix with manual pick badge SearchableSelect option rows now support an optional badge field — a muted pill rendered to the right of the label with an optional title attribute for native hover tooltips. Plain text so screen readers read it alongside the option name. buildBackendOptions in ImportModel stops appending " (preference-only)" to the label and instead sets badge="manual pick" plus a descriptive tooltip on every option whose auto_detect is false. The Backend help text explains what "manual pick" means so users aren't left wondering about the badge. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): inline ambiguity picker Red Playwright coverage for Batch A2 — when the server returns a 400 ambiguity body, the form must render an inline alert instead of a toast, expose one clickable chip per candidate backend, and support both auto-resubmit on pick and silent dismiss. - Mocks /api/models/import-uri with the structured ambiguity body (error, detail, modality, candidates, hint). - On first click of Import, the alert is visible, carries modality-specific copy, and shows a chip per candidate. - Clicking a chip clears the alert, sets the Backend dropdown, and triggers a second POST to /api/models/import-uri. - Dismissing the alert leaves the Backend dropdown on Auto-detect — no implicit backend assignment. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): inline ambiguity alert with candidate chips Adds AmbiguityAlert — a soft, info-coloured card rendered above the URI input when the server returns a structured 400 with { modality, candidates }. Message is modality-aware (tts/asr/embeddings/image/ reranker/detection get purpose-written copy, everything else falls back to a generic template). Each candidate is a clickable chip that shows a download icon when /backends/known marks the backend as not yet installed, so users aren't surprised by an implicit install. ImportModel wires the alert to handleSimpleImport's error path: - api.handleResponse now attaches { status, body } to the thrown Error so pages can pattern-match on structured responses instead of string error messages. - handleSimpleImport detects `status === 400 && body.error === 'ambiguous import'` and flips into the inline-picker mode instead of toasting. - Clicking a chip sets prefs.backend and auto-resubmits (passing the picked backend as an override so setPrefs's asynchrony doesn't leak a stale value). - Dismissing clears the alert; changing the URI or the backend also clears it so a stale alert never sticks around. Test fixtures mock GET /backends/known + POST /models/import-uri so the Playwright specs don't depend on real network reachability. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): auto-install warning Red Playwright coverage for Batch A3 — when the user picks a backend whose KnownBackend.installed is false, the form must render a muted inline note under the Backend dropdown warning that submitting will download the backend first. Picking an installed backend or leaving Auto-detect selected must keep the note hidden. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): auto-install warning under backend dropdown When the user picks a backend whose KnownBackend.installed is false, render a muted inline note under the Backend dropdown's help text warning that submitting will download the backend first. The note lives inside the same form-group so it lines up with the existing hint text; it's hidden when Auto-detect is selected (the selected backend is unknowable at that point) or when the chosen backend is already on disk. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): drop redundant section header, adjust icons, rename HF shortcut - Remove the "Import from URI" card-level <h2> — the page title already says "Import New Model" one row up, so the secondary header was duplicating information. - Swap the fa-star on "Common Preferences" for fa-sliders (stars imply favourites/ratings; this is just a preferences block) and move the Custom Preferences fa-sliders-h to fa-plus-circle so the two blocks read as distinct rather than as two sliders. - Rename the HF shortcut from "Search GGUF on HF" → "Browse models on HF" and drop the `search=gguf` filter on the linked URL. The import form now supports ~40 backends; hard-coding GGUF in the copy no longer matches the form's actual reach. - Pure polish — no behaviour change, covered by the existing Batch A Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): batch B — simple/power switch, options, tabs, dialog Adds a failing Playwright suite covering the full Batch B surface ahead of implementation: - B1: SimplePowerSwitch segmented control renders, toggles, persists to localStorage across reloads. - B2: Simple-mode Options disclosure is collapsed by default; expanding exposes only Backend, Model Name, Description (no quantizations, mmproj, model type, or custom prefs). - B3: Power mode has Preferences and YAML tabs with a persistent selection across reloads; URI/name/description typed in Simple carry over to Power; YAML tab swaps the primary action to Create. - B4: Switching Power -> Simple with a custom preference set triggers the 3-button confirmation dialog (Keep / Discard / Cancel) with the documented semantics. Tests fail against master — implementation lands in the following commits. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add SimplePowerSwitch segmented control Replaces the previous "Advanced Mode / Simple Mode" toggle button in the page header with a two-segment control that flips between Simple and Power. The control reuses the existing .segmented CSS shared with the Sound page for visual consistency. Mode state is persisted to localStorage under `import-form-mode` so reloads land on the same view (default: simple). The boolean alias `isAdvancedMode` is retained internally to minimise diff — subsequent commits reshape the Simple and Power surfaces independently. Closes B1 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): simple mode collapsible options, power tabs, switch dialog Completes the Batch B surface in a single structural pass so Simple and Power mode can evolve independently: Simple mode - URI input + Ambiguity alert + Import button, plus a collapsible "Options" disclosure that exposes ONLY Backend, Model Name, Description. Quantizations / MMProj / Model Type / Diffusers fields / Custom Preferences are no longer rendered in Simple mode. Power mode - In-page segmented "Preferences · YAML" tab strip. Active tab persists to localStorage under `import-form-power-tab`. - Preferences tab = the full existing preferences + custom prefs panel (no progressive disclosure yet — that's Batch D). - YAML tab = the existing CodeEditor. Primary button reads "Create" here, "Import Model" everywhere else. Switch dialog - Power -> Simple with non-default prefs (advanced pref keys set, any custom-pref key non-empty, or YAML edited away from the template) opens a 3-button dialog: Keep & switch / Discard & switch / Cancel. - Keep preserves all state. Discard resets prefs + customPrefs + YAML to defaults. Cancel leaves the user in Power mode. Page subtitle reflects the current surface (Simple, Power/Preferences, Power/YAML). Estimate banner renders everywhere except Power/YAML. Closes B2/B3/B4 from the Batch B Playwright suite. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): expand Options disclosure in Batch A tests Batch B hid the Backend dropdown behind a collapsible Options disclosure in Simple mode. The Batch A tests that exercise the dropdown directly (manual-pick badge, ambiguity chip sets the selected backend, auto- install warning) now click the disclosure toggle before asserting on dropdown contents. Test intent is unchanged. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): strip decorative icons from field labels The preference panel had 12 Font Awesome icons decorating field labels (Backend, Model Name, Description, Quantizations, MMProj Quantizations, Model Type, Pipeline Type, Scheduler Type, Enable Parameters, Embeddings, CUDA, plus fa-link on Model URI). Every label screamed equally, flattening the visual hierarchy. Remove them. Keep icons where they carry meaning: page-level section headers, URI format guide entries, primary buttons, the Simple-mode Options disclosure, the ambiguity alert's fa-lightbulb, the auto-install note's fa-download, and the Estimated-requirements banner's fa-memory / fa-microchip / fa-download. No new behaviour, no layout / spacing changes beyond removing the orphaned icon margin. Playwright suite green. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): progressive disclosure of preference fields Cover the Batch D visibility matrix for Power > Preferences: Quantizations, MMProj Quantizations, and Model Type each render only for the backends that can consume them, stay visible when the backend is unset, and preserve any value the user already typed when toggled off and back on. Also pin the shrunk Description textarea at rows=2. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): progressive disclosure + shorter description textarea Gate Quantizations, MMProj Quantizations, and Model Type in the Power > Preferences tab so each field only renders for the backends that can actually consume it. Backend unset keeps everything visible. Hidden fields' state is preserved (the JSX wrapper is guarded, not the underlying prefs state) so users flipping backends back and forth don't lose input. Also shrink the Description textarea from rows=3 to rows=2 — it's shared between Simple Options and Power Preferences so the change applies to both. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): enter-to-submit in Simple mode Red test for Batch F3 — pressing Enter in the URI input must POST /models/import-uri, and Enter in the Description textarea must insert a newline without submitting the form. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): enter-to-submit in Simple mode Wrap the Simple-mode URI input + ambiguity alert + Options disclosure in a <form> whose onSubmit calls handleSimpleImport. Pressing Enter in the URI input (or any Simple-mode text input) now submits the import without having to move the mouse to the header button. The Description textarea keeps its native behaviour — Enter inserts a newline. A hidden submit button is included because the visible Import button lives outside the form in the page header; some browsers only fire implicit Enter-submit when the form contains a submit-capable element. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import,SearchableSelect,components): aria-hidden on decorative icons Every Font Awesome icon in the import form is decorative — its meaning is already conveyed by adjacent visible text. Adding aria-hidden="true" prevents screen readers from announcing the unicode glyph point as content. Covers ImportModel.jsx (all remaining <i> glyphs) and SearchableSelect.jsx (the trigger chevron). AmbiguityAlert and SimplePowerSwitch already set aria-hidden on their icons when the components landed in Batches A and B — no change needed there. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(SearchableSelect): responsive dropdown maxHeight + hover focus guard F2 — replace fixed pixel heights with min(pixel, vh) so the dropdown and its inner scroll region don't overflow short viewports. Outer container: 260px -> min(260px, 60vh); inner listbox: 200px -> min(200px, 50vh). Tall viewports still get the original pixel caps. F5 — short-circuit onMouseEnter when the hovered row is already the focused row. Avoids queueing a setFocusIndex call (and a render) for every mousemove inside the same item — the state would be identical. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * ui(import): aria-label on custom preference rows The Key / Value inputs and trash button in each Custom Preferences row previously relied on placeholder text alone. Placeholders are not accessible names — they vanish on input and screen readers do not announce them consistently. Add row-indexed aria-labels so assistive tech can distinguish "Preference key for row 1" from "row 2", and give the trash button an explicit "Remove this preference" label. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * test(ui/import): modality chip row Red tests for Batch E — a horizontal modality chip row that filters the Backend dropdown by modality. Covers visibility in Simple-mode Options and Power/Preferences (and absence in Power/YAML), filter behaviour, mismatched-backend clearing with toast, ambiguity-alert auto-selection, and radiogroup keyboard navigation. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * feat(ui/import): add ModalityChips component + filter integration Horizontal chip row (Any, Text, Speech, TTS, Image, Embeddings, Rerankers, Detection, VAD) filters the Backend dropdown options to the selected modality. Default is Any — no filter, current behaviour. - New ModalityChips component (radiogroup pattern, roving tabindex, arrow-key navigation, Home/End). - buildBackendOptions now accepts an optional modalityFilter so grouped output is narrowed before rendering. - Chips render inside Simple-mode Options disclosure and Power > Preferences tab. Power > YAML stays unaffected. - Switching the filter drops a mismatched backend selection and surfaces a toast so the auto-clear is visible. - Ambiguity alerts auto-activate the matching chip so users see only relevant backends even if they dismiss the alert. Tightens the Batch E tests' option-matching to the label <span> so the "↵" keybind hint on the focused row doesn't break accessible-name lookups. Assisted-by: Claude:claude-opus-4-7[1m] [Agent] * fix(ui/import): rename Power to Advanced + stop URI-formats toggle from submitting form The "Supported URI Formats" disclosure button inside the Simple-mode form lacked an explicit type attribute, so it defaulted to type="submit". Every click triggered the form's onSubmit and surfaced the empty-URI validation toast ("Please enter a model URI"). Marking it type="button" lets it behave as a pure toggle. While here, rename the user-visible "Power" label to "Advanced" in the mode switch (button text + tooltip) and the Power-mode tab's aria-label, matching the term users actually expect. The internal mode key stays 'power' so tests, localStorage, and data-testid selectors are untouched. Assisted-by: Claude:claude-opus-4-7 * fix(system): fall back to cpu when meta backend lacks default capability Meta backends like vllm and sglang enumerate concrete variants for nvidia/amd/intel/cpu but omit a default: catch-all entry. On a no-GPU host the reported capability is "default", so the previous Capability() returned "default" unconditionally on a miss — IsCompatibleWith then saw no "default" key and filtered the meta out of AvailableBackends. The import flow's auto-install step then failed with "no backend found with name <meta>", contradicting the UI's promise that the backend would be downloaded on demand. Try the explicit "default" key first, then fall back to "cpu" before giving up. vllm now resolves to cpu-vllm on CPU-only Linux without touching the gallery YAML. Assisted-by: Claude:claude-opus-4-7
2026-04-22 20:42:37 +00:00
// Best-effort: PipelineTag / LibraryName are advisory — some callers
// (offline tests, restricted networks) can't reach the metadata endpoint.
// Swallow errors so downstream file detection still works.
if meta, err := c.fetchModelMetadata(repoID); err == nil {
details.PipelineTag = meta.PipelineTag
details.LibraryName = meta.LibraryName
}
// Process each file
baseURL := strings.TrimSuffix(c.baseURL, "/api/models")
for _, file := range files {
fileName := filepath.Base(file.Path)
isReadme := strings.Contains(strings.ToLower(fileName), "readme")
// Extract SHA256 from LFS or use OID
sha256 := ""
if file.LFS != nil && file.LFS.Oid != "" {
sha256 = file.LFS.Oid
} else {
sha256 = file.Oid
}
// Construct the full URL for the file
// Use /resolve/main/ for downloading files (handles LFS properly)
fileURL := fmt.Sprintf("%s/%s/resolve/main/%s", baseURL, repoID, file.Path)
modelFile := ModelFile{
Path: file.Path,
Size: file.Size,
SHA256: sha256,
IsReadme: isReadme,
URL: fileURL,
}
details.Files = append(details.Files, modelFile)
// Set the readme file
if isReadme && details.ReadmeFile == nil {
details.ReadmeFile = &modelFile
}
}
return details, nil
}
// GetReadmeContent gets the content of a README file
func (c *Client) GetReadmeContent(repoID, readmePath string) (string, error) {
baseURL := strings.TrimSuffix(c.baseURL, "/api/models")
url := fmt.Sprintf("%s/%s/raw/main/%s", baseURL, repoID, readmePath)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to fetch readme content. Status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
return string(body), nil
}
// FilterFilesByQuantization filters files by quantization type
func FilterFilesByQuantization(files []ModelFile, quantization string) []ModelFile {
var filtered []ModelFile
for _, file := range files {
fileName := filepath.Base(file.Path)
if strings.Contains(strings.ToLower(fileName), strings.ToLower(quantization)) {
filtered = append(filtered, file)
}
}
return filtered
}
// shardSuffixRegex matches the `-NNNNN-of-MMMMM.gguf` suffix that llama.cpp
// uses to split large GGUF models across multiple files. Widths of 16 digits
// are accepted because shard counts seen in the wild range from single digits
// (unusual) to the common 5-digit zero-padded form (e.g. `-00001-of-00014`).
var shardSuffixRegex = regexp.MustCompile(`(?i)-(\d{1,6})-of-(\d{1,6})\.gguf$`)
// SplitShardSuffix detects llama.cpp-style sharded GGUF filenames. When the
// filename ends with `-NNNNN-of-MMMMM.gguf` it returns the base filename
// (with `.gguf` re-appended), the 1-based shard index, the total shard
// count, and ok=true. Non-sharded filenames return zero values and ok=false.
func SplitShardSuffix(fileName string) (base string, index, total int, ok bool) {
loc := shardSuffixRegex.FindStringSubmatchIndex(fileName)
if loc == nil {
return "", 0, 0, false
}
idx, err := strconv.Atoi(fileName[loc[2]:loc[3]])
if err != nil {
return "", 0, 0, false
}
tot, err := strconv.Atoi(fileName[loc[4]:loc[5]])
if err != nil {
return "", 0, 0, false
}
return fileName[:loc[0]] + ".gguf", idx, tot, true
}
// ShardGroup bundles every file that belongs to the same logical GGUF model.
// Single-file models produce a one-entry group; multi-part shard sets produce
// one group holding every part in shard-index order.
type ShardGroup struct {
// Base is the logical filename: for sharded groups this is the common
// prefix with `.gguf` re-appended; for single-file groups it equals the
// sole entry's basename.
Base string
// Sharded is true when the group represents a multi-part shard set.
Sharded bool
// Total is the declared shard count (0 when Sharded is false).
Total int
// Files are the group's entries; sharded groups are sorted by index.
Files []ModelFile
}
// GroupShards buckets ModelFile entries by their shard base. Files that do
// not match the sharded-filename pattern become one-entry groups. Group
// order follows the first appearance of each group in the input (so the
// historical "last-seen wins" fallback logic in the llama-cpp importer
// keeps producing the same group); shards within a group are sorted by
// their 1-based index so downstream consumers can rely on Files[0] being
// shard 1.
func GroupShards(files []ModelFile) []ShardGroup {
groupIdx := make(map[string]int)
var groups []ShardGroup
for _, file := range files {
name := filepath.Base(file.Path)
base, _, total, isShard := SplitShardSuffix(name)
if !isShard {
groups = append(groups, ShardGroup{
Base: name,
Files: []ModelFile{file},
})
continue
}
if idx, ok := groupIdx[base]; ok {
groups[idx].Files = append(groups[idx].Files, file)
if total > groups[idx].Total {
groups[idx].Total = total
}
continue
}
groupIdx[base] = len(groups)
groups = append(groups, ShardGroup{
Base: base,
Sharded: true,
Total: total,
Files: []ModelFile{file},
})
}
for i := range groups {
if !groups[i].Sharded {
continue
}
sort.SliceStable(groups[i].Files, func(a, b int) bool {
_, ai, _, _ := SplitShardSuffix(filepath.Base(groups[i].Files[a].Path))
_, bi, _, _ := SplitShardSuffix(filepath.Base(groups[i].Files[b].Path))
return ai < bi
})
}
return groups
}
// FindPreferredModelFile returns shard #1 of the first group whose base
// filename contains any of the quantization preferences, checking each
// preference in priority order. For single-file models this collapses to
// "the first file whose name contains the preference", preserving the
// historical behaviour while correctly pointing at shard 1 for multi-part
// GGUF models — llama.cpp's split loader needs shard 1 to walk the set.
func FindPreferredModelFile(files []ModelFile, preferences []string) *ModelFile {
groups := GroupShards(files)
for _, preference := range preferences {
lowerPref := strings.ToLower(preference)
for i := range groups {
if strings.Contains(strings.ToLower(groups[i].Base), lowerPref) {
return &groups[i].Files[0]
}
}
}
return nil
}