diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetails.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetails.svelte index e39d075ee9..aa5fcc01f9 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetails.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetails.svelte @@ -3,7 +3,7 @@ import ModelsDiscoverModelDetailsHeader from './ModelsDiscoverModelDetailsHeader.svelte'; import ModelsDiscoverModelDetailsReadme from './ModelsDiscoverModelDetailsReadme.svelte'; import ModelsDiscoverModelDetailsSkeleton from './ModelsDiscoverModelDetailsSkeleton.svelte'; - import { isAuxSidecar } from '$lib/constants'; + import { ModelAuxSidecar } from '$lib/enums'; import { HuggingFaceService } from '$lib/services'; import type { HfModelDetailInfo, HfModelSibling } from '$lib/types/huggingface'; import { detectThinkingSupport, detectToolUseSupport } from '$lib/utils'; @@ -37,11 +37,7 @@ // Capabilities derived from HF metadata. Vision comes from an mmproj sidecar // or a multimodal pipeline tag; tool use / reasoning from the chat template. let hasMmproj = $derived( - files.some((f) => { - const sidecar = HuggingFaceService.extractQuantMeta(f.path)?.sidecar; - - return sidecar !== null && sidecar !== undefined && isAuxSidecar(sidecar); - }) + files.some((f) => HuggingFaceService.extractQuantMeta(f.path)?.sidecar === ModelAuxSidecar.MMPROJ) ); let hasVision = $derived(hasMmproj || details?.pipeline_tag === 'image-text-to-text'); let hasTools = $derived(detectToolUseSupport(gguf?.chat_template ?? '')); @@ -57,8 +53,9 @@ for (const file of files) { const meta = HuggingFaceService.extractQuantMeta(file.path); - // mmproj sidecars are already conveyed by the Vision capability badge. - if (meta?.sidecar && isAuxSidecar(meta.sidecar)) continue; + // mmproj sidecars are already conveyed by the Vision capability badge; + // imatrix ships as a normal chip with its own badge. + if (meta?.sidecar === ModelAuxSidecar.MMPROJ) continue; const depth = meta?.quant ? HuggingFaceService.getBitDepth(meta.quant) : null; const bucket = depth ?? 99; diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetailsDownloadOptionsQuantDownloadButton.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetailsDownloadOptionsQuantDownloadButton.svelte index f413f5d2d7..cccd563600 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetailsDownloadOptionsQuantDownloadButton.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverModelDetailsDownloadOptionsQuantDownloadButton.svelte @@ -4,7 +4,6 @@ import { Check, Download, Loader2, Pause, Play, RotateCw, X } from '@lucide/svelte'; import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte'; import * as Tooltip from '$lib/components/ui/tooltip'; - import { isAuxSidecar } from '$lib/constants'; import { HuggingFaceService } from '$lib/services'; import { modelsStore } from '$lib/stores'; import type { HfModelSibling } from '$lib/types'; @@ -65,7 +64,7 @@ onclick={() => (confirmDeleteOpen = true)} type="button" > - {#if meta?.sidecar && !isAuxSidecar(meta.sidecar)} + {#if meta?.sidecar} @@ -131,7 +130,7 @@ }} type="button" > - {#if meta?.sidecar && !isAuxSidecar(meta.sidecar)} + {#if meta?.sidecar} @@ -224,7 +223,7 @@ {/if} - {#if meta?.sidecar && !isAuxSidecar(meta.sidecar)} + {#if meta?.sidecar} diff --git a/tools/ui/src/lib/components/app/models/discover/download-options.utils.ts b/tools/ui/src/lib/components/app/models/discover/download-options.utils.ts index 5aa329896c..69eb42704f 100644 --- a/tools/ui/src/lib/components/app/models/discover/download-options.utils.ts +++ b/tools/ui/src/lib/components/app/models/discover/download-options.utils.ts @@ -1,4 +1,4 @@ -import { isAuxSidecar, type ModelSidecar } from '$lib/constants'; +import { isAuxSidecar, isDraftSidecar, type ModelSidecar } from '$lib/constants'; import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums'; import { HuggingFaceService } from '$lib/services'; import type { HfModelSibling } from '$lib/types/huggingface'; @@ -43,9 +43,13 @@ export function classify(path: string): 'main' | 'draft' | 'aux' { /** Display label of a file: its quant, else the file name without the extension. */ export function labelFor(path: string): string { - const quant = HuggingFaceService.extractQuantMeta(path)?.quant; + const meta = HuggingFaceService.extractQuantMeta(path); - if (quant) return quant; + if (meta?.quant) return meta.quant; + + // Quantless sidecar files: the chip badge already carries the sidecar type, + // so the label only marks draft files; aux sidecars badge alone. + if (meta?.sidecar) return isDraftSidecar(meta.sidecar) ? 'draft' : ''; const basename = path.split('/').pop() ?? path; @@ -65,9 +69,10 @@ export function quantBitDepth(quant: string | null): number { return bit ? Number(bit) : 99; } -// LLAMA-APP-REUSE: --spec-type value for each draft sidecar +// LLAMA-APP-REUSE: --spec-type value for each draft sidecar; aux sidecars stay empty export const SPEC_TYPE: Record = { [ModelAuxSidecar.MMPROJ]: '', + [ModelAuxSidecar.IMATRIX]: '', [ModelDraftSidecar.DFLASH]: 'draft-dflash', [ModelDraftSidecar.DSPARK]: 'draft-dspark', [ModelDraftSidecar.EAGLE3]: 'eagle3', diff --git a/tools/ui/src/lib/constants/model-id.constants.ts b/tools/ui/src/lib/constants/model-id.constants.ts index 73a2c5fde4..dc29338d5f 100644 --- a/tools/ui/src/lib/constants/model-id.constants.ts +++ b/tools/ui/src/lib/constants/model-id.constants.ts @@ -75,10 +75,12 @@ export const MODEL_ID = { /** * Trailing `-` suffix marking a GGUF with an embedded draft in the * same weight file (MTP) or a sidecar download entry, e.g. - * `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. The captured prefix is the - * candidate model id; the caller decides whether it looks quantized. + * `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. An optional `-draft` tail covers + * standalone sidecar files, e.g. `Model-MTP-draft.gguf`. The captured + * prefix is the candidate model id; the caller decides whether it looks + * quantized. */ - SIDECAR_SUFFIX_REGEX: new RegExp(`^(.*)-(${SIDECAR_TOKEN_ALTERNATION})$`, 'i'), + SIDECAR_SUFFIX_REGEX: new RegExp(`^(.*)-(${SIDECAR_TOKEN_ALTERNATION})(-draft)?$`, 'i'), /** Matches a trailing weight file extension, e.g. `model.gguf` -> `model`. */ WEIGHT_EXTENSION_REGEX: /\.(gguf|ggml)$/i diff --git a/tools/ui/src/lib/enums/model.enums.ts b/tools/ui/src/lib/enums/model.enums.ts index b0b7906f08..100d1f2dc5 100644 --- a/tools/ui/src/lib/enums/model.enums.ts +++ b/tools/ui/src/lib/enums/model.enums.ts @@ -32,5 +32,7 @@ export enum ModelDraftSidecar { */ export enum ModelAuxSidecar { /** Multimodal projector: unlocks vision and/or audio input modalities. */ - MMPROJ = 'mmproj' + MMPROJ = 'mmproj', + /** Importance-matrix data used to build imatrix quants; not loaded at serve time. */ + IMATRIX = 'imatrix' } diff --git a/tools/ui/src/lib/services/huggingface.service.ts b/tools/ui/src/lib/services/huggingface.service.ts index 0550ecd64d..ee71e05131 100644 --- a/tools/ui/src/lib/services/huggingface.service.ts +++ b/tools/ui/src/lib/services/huggingface.service.ts @@ -168,8 +168,9 @@ export class HuggingFaceService { * Extract the GGUF quantization token (e.g. `Q4_K_M`) and any sidecar type * (`mtp`, `dflash`, `mmproj`, ...) from a `.gguf` filename. The sidecar token * shows up either as a sidecar prefix (`mtp-.gguf`, `dflash-.gguf`, - * `mmproj-.gguf`) or as the `-mtp` suffix when the draft model is - * embedded in the same GGUF weight file. + * `mmproj-.gguf`), as a `-mtp` suffix, or as the whole filename + * (`imatrix.gguf`); a `-draft` tail marks a standalone sidecar file + * (`Model-MTP-draft.gguf`). * * `sidecarForm` records which side of the filename the sidecar token sat * on so callers can render badges differently (e.g. prefix on the left of @@ -197,6 +198,14 @@ export class HuggingFaceService { let sidecar: ModelSidecar | null = null; let sidecarForm: SidecarForm | null = null; + // A file named just the sidecar token (`imatrix.gguf`) is the sidecar + // itself: no name or quant segments to parse. + const bareSidecar = sidecarFromFileToken(source.toLowerCase()); + + if (bareSidecar) { + return { quant: null, shared: false, sidecar: bareSidecar, sidecarForm: SidecarForm.PREFIX }; + } + const prefixMatch = source.match(MODEL_ID.SIDECAR_PREFIX_REGEX); if (prefixMatch) { @@ -207,14 +216,12 @@ export class HuggingFaceService { const suffixMatch = source.match(MODEL_ID.SIDECAR_SUFFIX_REGEX); if (suffixMatch) { - const candidate = suffixMatch[1]; - const headSeg = candidate.split(MODEL_ID.SEGMENT_SEPARATOR).pop(); - - if (headSeg && MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(headSeg)) { - sidecar = sidecarFromFileToken(suffixMatch[2].toLowerCase()); - sidecarForm = SidecarForm.SUFFIX; - source = candidate; - } + // Take the suffix sidecar even when the head carries no quant: + // embedded drafts end in one (`Hy3-IQ1_M-mtp`), standalone sidecar + // files do not (`Model-MTP-draft`, `Model-imatrix`). + sidecar = sidecarFromFileToken(suffixMatch[2].toLowerCase()); + sidecarForm = SidecarForm.SUFFIX; + source = suffixMatch[1]; } }