ui : extract discover pipeline tags, memory and serve-command constants

Move remaining magic strings in the discover UI into $lib/constants:
- HF modalities: HF_MODALITY_PIPELINE_TAGS (vision/audio/video pipeline_tag
  groups) and HF_MMPROJ_FILENAME_TOKEN; reuse MODEL_ID.ORG_SEPARATOR for the
  org split.
- Bit-depth row: OTHER_BIT_DEPTH_LABEL, BIT_DEPTH_LABEL_SUFFIX and the
  existing GIGABYTE_LABEL / OTHER_BIT_DEPTH instead of 'Other', '-bit', 'GB'.
- Serve command preview: a SERVE_COMMAND constants object (llama/serve/-hf/
  -hfd/--spec-type) plus MODEL_ID.QUANTIZATION_SEPARATOR for the ':' join.

Assisted-by: llama-ui:Qwen3.8-Flash-Next
This commit is contained in:
Aleksander Grygier
2026-09-04 20:07:37 +02:00
parent cebb6f38cb
commit 7144b70eb5
5 changed files with 99 additions and 26 deletions
@@ -2,7 +2,13 @@
import { quantBitDepth } from './download-options.utils';
import { Check, Copy, Plus, X } from '@lucide/svelte';
import * as Select from '$lib/components/ui/select';
import { DEFAULT_BASE_BIT_DEPTH, type ModelSidecar,SPEC_TYPE } from '$lib/constants';
import {
DEFAULT_BASE_BIT_DEPTH,
MODEL_ID,
type ModelSidecar,
SERVE_COMMAND,
SPEC_TYPE
} from '$lib/constants';
import { HuggingFaceService } from '$lib/services';
import type { QuantOption } from '$lib/types';
import { copyToClipboard } from '$lib/utils';
@@ -91,10 +97,20 @@
/** The llama serve command, composed from the inline picks. */
let command = $derived.by(() => {
const parts = ['llama', 'serve', '-hf', mainQuant ? `${modelId}:${mainQuant}` : modelId];
const parts = [
SERVE_COMMAND.BIN,
SERVE_COMMAND.SUBCOMMAND,
SERVE_COMMAND.MODEL_FLAG,
mainQuant ? `${modelId}${MODEL_ID.QUANTIZATION_SEPARATOR}${mainQuant}` : modelId
];
if (draftOption && draftQuant) {
parts.push('-hfd', `${modelId}:${draftQuant}`, '--spec-type', specType ?? '');
parts.push(
SERVE_COMMAND.DRAFT_FLAG,
`${modelId}${MODEL_ID.QUANTIZATION_SEPARATOR}${draftQuant}`,
SERVE_COMMAND.SPEC_TYPE_FLAG,
specType ?? ''
);
}
return parts.join(' ');
@@ -124,13 +140,15 @@
<div
class="flex min-w-0 flex-1 items-center gap-x-2 overflow-x-auto py-0.5 font-mono text-xs whitespace-nowrap text-foreground/90"
>
<span class="shrink-0">llama</span>
<span class="shrink-0">{SERVE_COMMAND.BIN}</span>
<span class="shrink-0">serve</span>
<span class="shrink-0">{SERVE_COMMAND.SUBCOMMAND}</span>
<span class="shrink-0">-hf</span>
<span class="shrink-0">{SERVE_COMMAND.MODEL_FLAG}</span>
<span class="shrink-0">{modelId}{mainQuant ? ':' : ''}</span>
<span class="shrink-0">
{modelId}{mainQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''}
</span>
<!-- Base quant: always part of the command, the 4-bit file by default. -->
{#if baseOption}
@@ -172,9 +190,11 @@
it only appears while hovering the segment, or directly on touch -->
{#if draftOption}
<span class="group/draft inline-flex shrink-0 items-center gap-x-2">
<span>-hfd</span>
<span>{SERVE_COMMAND.DRAFT_FLAG}</span>
<span class="shrink-0">{modelId}{draftQuant ? ':' : ''}</span>
<span class="shrink-0">
{modelId}{draftQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''}
</span>
<Select.Root
onValueChange={(v) => v && (draftPick = v)}
@@ -199,7 +219,7 @@
</Select.Root>
{#if draftType}
<span>--spec-type</span>
<span>{SERVE_COMMAND.SPEC_TYPE_FLAG}</span>
<!-- the select only earns its chrome when there is a real choice to make -->
{#if specTypes.length > 1}
@@ -1,5 +1,11 @@
<script lang="ts">
import ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton from './ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte';
import {
BIT_DEPTH_LABEL_SUFFIX,
GIGABYTE_LABEL,
OTHER_BIT_DEPTH,
OTHER_BIT_DEPTH_LABEL
} from '$lib/constants';
import { SelectableFileKind } from '$lib/enums';
import type { DownloadEntryState, SelectableFile } from '$lib/types';
import { minMemoryTierGb } from '$lib/utils';
@@ -22,15 +28,17 @@
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-3">
<div class="pt-1 text-sm tabular-nums text-muted-foreground">
{#if bitDepth === 99}
Other
{#if bitDepth === OTHER_BIT_DEPTH}
{OTHER_BIT_DEPTH_LABEL}
{:else}
{bitDepth}-bit
{bitDepth}{BIT_DEPTH_LABEL_SUFFIX}
{/if}
{#if mainMemGb}
<span class="block text-[10px] whitespace-nowrap text-muted-foreground/60">
needs at least {mainMemGb}GB{draftMemGb ? ` + ${draftMemGb}GB` : ''}+ memory
needs at least {mainMemGb}{GIGABYTE_LABEL}{draftMemGb
? ` + ${draftMemGb}${GIGABYTE_LABEL}`
: ''}+ memory
</span>
{/if}
</div>
@@ -1,7 +1,13 @@
<script lang="ts">
import ModelId from '../../ModelId.svelte';
import ModelsDiscoverAvatar from '../ModelsDiscoverAvatar.svelte';
import { isAuxSidecar, type ModelSidecar } from '$lib/constants';
import {
HF_MMPROJ_FILENAME_TOKEN,
HF_MODALITY_PIPELINE_TAGS,
isAuxSidecar,
MODEL_ID,
type ModelSidecar
} from '$lib/constants';
import { HuggingFaceService, ModelsService } from '$lib/services';
import { modelsHubStore } from '$lib/stores';
import type { ModelsHubSizeRange } from '$lib/stores/models-hub/index.svelte';
@@ -20,7 +26,7 @@
let { active = false, model, onSelect, showBaseModelAvatar = false }: Props = $props();
let org = $derived(model.id.split('/')[0] ?? model.id);
let org = $derived(model.id.split(MODEL_ID.ORG_SEPARATOR)[0] ?? model.id);
// Org whose avatar is shown: the base model's org when showBaseModelAvatar
// (e.g. the Qwen logo for ggml-org/Qwen3.8-27B-GGUF), else the repo's org.
@@ -74,16 +80,12 @@
let modalities = $derived.by<ModelModalities>(() => {
const tag = model.pipeline_tag ?? '';
const vision =
['image-text-to-text', 'image-to-text', 'text-to-image', 'image-to-video'].includes(tag) ||
Boolean(model.siblings?.some((s) => s.rfilename.toLowerCase().includes('mmproj')));
const audio = [
'audio-classification',
'audio-to-audio',
'automatic-speech-recognition',
'text-to-speech',
'voice-activity-detection'
].includes(tag);
const video = ['text-to-video', 'image-to-video', 'video-to-video'].includes(tag);
HF_MODALITY_PIPELINE_TAGS.vision.includes(tag) ||
Boolean(
model.siblings?.some((s) => s.rfilename.toLowerCase().includes(HF_MMPROJ_FILENAME_TOKEN))
);
const audio = HF_MODALITY_PIPELINE_TAGS.audio.includes(tag);
const video = HF_MODALITY_PIPELINE_TAGS.video.includes(tag);
return { audio, video, vision };
});
@@ -100,6 +100,28 @@ export const HF_SAFETENSORS_TAG = 'safetensors';
// Pipeline tasks (logic use only - matching `pipeline_tag` values against tags)
/**
* `pipeline_tag` values grouped by the input/output modality they imply, used
* to derive a discover row's modality icons. A tag in more than one group (e.g.
* `image-to-video`) lights up each modality it belongs to.
*/
export const HF_MODALITY_PIPELINE_TAGS: Readonly<
Record<'audio' | 'video' | 'vision', readonly string[]>
> = {
audio: [
'audio-classification',
'audio-to-audio',
'automatic-speech-recognition',
'text-to-speech',
'voice-activity-detection'
],
video: ['text-to-video', 'image-to-video', 'video-to-video'],
vision: ['image-text-to-text', 'image-to-text', 'text-to-image', 'image-to-video']
};
/** Filename token marking an mmproj sidecar sibling (unlocks vision / audio). */
export const HF_MMPROJ_FILENAME_TOKEN = 'mmproj';
export const HF_TASK_TAGS: readonly string[] = [
'audio-classification',
'audio-to-audio',
@@ -27,6 +27,27 @@ export const SPEC_TYPE: Record<ModelSidecar, string> = {
/** Bit-depth bucket for files that carry no quant token; rendered as "Other". */
export const OTHER_BIT_DEPTH = 99;
/** Label for the OTHER_BIT_DEPTH bucket. */
export const OTHER_BIT_DEPTH_LABEL = 'Other';
/** Suffix appended after a bit-depth number to label a row, e.g. `4-bit`. */
export const BIT_DEPTH_LABEL_SUFFIX = '-bit';
/**
* Fixed tokens of the standalone `llama serve` command preview, so the command
* builder and its rendered spans share one source of the CLI spelling.
*/
export const SERVE_COMMAND = {
BIN: 'llama',
/** Draft model repo:tag argument (download sidecar / draft weights). */
DRAFT_FLAG: '-hfd',
/** Main model repo:tag argument. */
MODEL_FLAG: '-hf',
/** Speculative-decoding type argument for the draft. */
SPEC_TYPE_FLAG: '--spec-type',
SUBCOMMAND: 'serve'
} as const;
/** Bit depth preferred for the command's default base quant. */
export const DEFAULT_BASE_BIT_DEPTH = 4;