From cebb6f38cb2cbb3463938f0fe1c6cbdfd018310d Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Fri, 4 Sep 2026 13:19:01 +0200 Subject: [PATCH] ui : replace magic strings in hf service and model status store Extract remaining magic values into named constants/enums: - HF service: reuse PATH_SEPARATOR for the '/' split, new HF_SHARED_DRAFT_TOKEN and HF_SIZE_STRING_REGEX constants. - Model status store: reuse MODEL_ID.QUANTIZATION_SEPARATOR for ':' and PATH_SEPARATOR for the sidecar cache key; new CLI_FLAGS entries for --model-draft / -md / --mmproj; a new DownloadStopRequest enum replacing the 'pause' | 'cancel' string union. - Reuse the shared PATH_SEPARATOR in download-options.utils. Assisted-by: llama-ui:Qwen3.8-Flash-Next --- .../download-options.utils.ts | 10 ++++--- .../src/lib/constants/cli-flags.constants.ts | 6 +++++ .../lib/constants/huggingface.constants.ts | 12 +++++++++ tools/ui/src/lib/enums/index.ts | 2 ++ tools/ui/src/lib/enums/model.enums.ts | 10 +++++++ .../src/lib/services/huggingface.service.ts | 9 ++++--- .../ui/src/lib/stores/models/status.svelte.ts | 26 +++++++++++-------- 7 files changed, 57 insertions(+), 18 deletions(-) diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts index 19866eae4c..bd7439b373 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts @@ -1,4 +1,10 @@ -import { DRAFT_FILE_LABEL, isAuxSidecar, isDraftSidecar, OTHER_BIT_DEPTH } from '$lib/constants'; +import { + DRAFT_FILE_LABEL, + isAuxSidecar, + isDraftSidecar, + OTHER_BIT_DEPTH, + PATH_SEPARATOR +} from '$lib/constants'; import { SelectableFileKind } from '$lib/enums'; import { HuggingFaceService } from '$lib/services'; @@ -9,8 +15,6 @@ const UD_QUANT_PREFIX_REGEX = /^UD-(?=.)/i; const QUANT_BIT_DEPTH_REGEX = /(?:I?Q|F)(\d+)/i; /** Trailing weight extension, stripped from a quantless file's label. */ const WEIGHT_EXTENSION_LABEL_REGEX = /\.gguf$/i; -/** Path separator between path segments. */ -const PATH_SEPARATOR = '/'; /** Kind of a file path: the main weights, a draft sidecar, or an aux sidecar (mmproj). */ export function classify(path: string): SelectableFileKind { diff --git a/tools/ui/src/lib/constants/cli-flags.constants.ts b/tools/ui/src/lib/constants/cli-flags.constants.ts index c4af2b6f46..0b6b2af86e 100644 --- a/tools/ui/src/lib/constants/cli-flags.constants.ts +++ b/tools/ui/src/lib/constants/cli-flags.constants.ts @@ -2,6 +2,12 @@ export const CLI_FLAGS = { AGENT: '--agent', API_KEY: '--api-key', MCP_PROXY: '--ui-mcp-proxy', + /** Multimodal projector path; unlocks vision/audio for the model. */ + MMPROJ: '--mmproj', + /** Draft model weights path (long form); the router records it per model. */ + MODEL_DRAFT: '--model-draft', + /** Draft model weights path (short form). */ + MODEL_DRAFT_SHORT: '-md', SLOTS: '--slots', TOOLS: '--tools' } as const; diff --git a/tools/ui/src/lib/constants/huggingface.constants.ts b/tools/ui/src/lib/constants/huggingface.constants.ts index bf67b39a7b..686f9bf989 100644 --- a/tools/ui/src/lib/constants/huggingface.constants.ts +++ b/tools/ui/src/lib/constants/huggingface.constants.ts @@ -78,6 +78,11 @@ export const HF_SHARD_PAD_WIDTH = 5; /** `UD-` (Unsloth Dynamic) custom quantization prefix, e.g. `UD-Q4_K_XL`. */ export const HF_UD_QUANT_PREFIX = 'UD'; export const HF_UD_QUANT_PREFIX_REGEX = /^UD-/i; +/** + * Segment marking an Unsloth `shared-` draft head that borrows the target + * model's embedding/output weights, e.g. `...-shared-Q4_K_M.gguf`. + */ +export const HF_SHARED_DRAFT_TOKEN = 'shared'; /** * Extracts the leading precision digits from a quant token, e.g. * `Q4_K_XL` -> 4, `IQ2_XXS` -> 2, `TQ1_0` -> 1, `BF16` -> 16. @@ -136,6 +141,13 @@ export const MEGABYTE = 1_000_000; export const GIGABYTE = 1_000_000_000; export const TERABYTE = 1_000_000_000_000; +/** + * Matches a human size string (`177GB`, `1.2 TB`, `500MB`), capturing the + * numeric value and its unit suffix. Used by `parseSizeBytes`. + */ +// LLAMA-APP-REUSE: catalog size string parsing +export const HF_SIZE_STRING_REGEX = /^\s*([\d.]+)\s*([a-z]+)\s*$/i; + /** * Byte multiplier for a size suffix (`k` kilobyte, `m` megabyte, ...) as used by * the llama.app catalog `size` strings, whose suffix is lowercase. diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts index cedb514f5c..3f16626acd 100644 --- a/tools/ui/src/lib/enums/index.ts +++ b/tools/ui/src/lib/enums/index.ts @@ -77,6 +77,8 @@ export { SelectableFileKind } from './model.enums'; +export { DownloadStopRequest } from './model.enums'; + export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './server.enums'; export { ParameterSource, SyncableParameterType, SettingsFieldType } from './settings.enums'; diff --git a/tools/ui/src/lib/enums/model.enums.ts b/tools/ui/src/lib/enums/model.enums.ts index 19d2d83714..d781a778f9 100644 --- a/tools/ui/src/lib/enums/model.enums.ts +++ b/tools/ui/src/lib/enums/model.enums.ts @@ -46,3 +46,13 @@ export enum SelectableFileKind { DRAFT = 'draft', MAIN = 'main' } + +/** + * Why an in-flight download is being stopped, so the terminal `download_failed` + * feed event can be attributed: a user pause (resumable) or a user cancel + * (discard). Distinguishes these from a genuine download failure. + */ +export enum DownloadStopRequest { + CANCEL = 'cancel', + PAUSE = 'pause' +} diff --git a/tools/ui/src/lib/services/huggingface.service.ts b/tools/ui/src/lib/services/huggingface.service.ts index ee71e05131..3afa1af105 100644 --- a/tools/ui/src/lib/services/huggingface.service.ts +++ b/tools/ui/src/lib/services/huggingface.service.ts @@ -39,6 +39,8 @@ import { HF_SAFETENSORS_TAG, HF_SHARD_PAD_WIDTH, HF_SHARD_REGEX, + HF_SHARED_DRAFT_TOKEN, + HF_SIZE_STRING_REGEX, HF_SIZE_SUFFIX_BYTES, HF_TASK_TAGS, HF_TREE_PATH, @@ -191,7 +193,7 @@ export class HuggingFaceService { // HF repos may nest sidecars in a folder (e.g. `MTP/mtp-Model-Q4_0.gguf`); // parse the file name only, the folder adds no quant information. - let source = (filename.split('/').pop() ?? filename).replace( + let source = (filename.split(PATH_SEPARATOR).pop() ?? filename).replace( MODEL_ID.WEIGHT_EXTENSION_REGEX, '' ); @@ -233,7 +235,7 @@ export class HuggingFaceService { const quantIdx = segments.findIndex((seg) => MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(seg)); // Unsloth ships draft heads in two layouts: `shared-` files borrow the // embedding/output weights from the target model, others are self-contained. - const shared = segments.some((seg) => seg.toLowerCase() === 'shared'); + const shared = segments.some((seg) => seg.toLowerCase() === HF_SHARED_DRAFT_TOKEN); let quant = quantIdx >= 0 ? segments[quantIdx].toUpperCase() : null; @@ -623,9 +625,8 @@ export class HuggingFaceService { * null when it carries no number or no known suffix, so callers can fall * back to another source instead of showing a wrong size. */ - // LLAMA-APP-REUSE: catalog size string parsing static parseSizeBytes(size: string): number | null { - const match = /^\s*([\d.]+)\s*([a-z]+)\s*$/i.exec(size); + const match = HF_SIZE_STRING_REGEX.exec(size); if (!match) return null; diff --git a/tools/ui/src/lib/stores/models/status.svelte.ts b/tools/ui/src/lib/stores/models/status.svelte.ts index 634f23d35d..228e3b1e4a 100644 --- a/tools/ui/src/lib/stores/models/status.svelte.ts +++ b/tools/ui/src/lib/stores/models/status.svelte.ts @@ -7,8 +7,8 @@ * modelsStore; the host owns the router model rows the feed updates. */ -import { HF_UD_QUANT_PREFIX_REGEX } from '$lib/constants'; -import { ServerModelsSseEventType, ServerModelStatus } from '$lib/enums'; +import { CLI_FLAGS, HF_UD_QUANT_PREFIX_REGEX, MODEL_ID, PATH_SEPARATOR } from '$lib/constants'; +import { DownloadStopRequest, ServerModelsSseEventType, ServerModelStatus } from '$lib/enums'; import { HuggingFaceService } from '$lib/services/huggingface.service'; import { ModelsService } from '$lib/services/models.service'; import type { ModelPropsManager } from '$lib/stores/models/props.svelte'; @@ -41,7 +41,7 @@ export interface ModelStatusHost { * must compare equal. */ function downloadIdKey(repoWithTag: string): string { - const idx = repoWithTag.indexOf(':'); + const idx = repoWithTag.indexOf(MODEL_ID.QUANTIZATION_SEPARATOR); const repo = idx === -1 ? repoWithTag : repoWithTag.slice(0, idx); const tag = idx === -1 ? '' : repoWithTag.slice(idx + 1); @@ -64,13 +64,17 @@ export class ModelStatusManager { if (!args) continue; for (let i = 0; i < args.length - 1; i++) { - if (args[i] !== '--model-draft' && args[i] !== '-md' && args[i] !== '--mmproj') { + if ( + args[i] !== CLI_FLAGS.MODEL_DRAFT && + args[i] !== CLI_FLAGS.MODEL_DRAFT_SHORT && + args[i] !== CLI_FLAGS.MMPROJ + ) { continue; } const parsed = HuggingFaceService.parseCachePath(args[i + 1]); - if (parsed) result.add(`${parsed.repo}/${parsed.file}`); + if (parsed) result.add(`${parsed.repo}${PATH_SEPARATOR}${parsed.file}`); } } @@ -91,7 +95,7 @@ export class ModelStatusManager { { target: ServerModelStatus; resolve: () => void; reject: (e: Error) => void } >(); /** Tags the user asked to stop (pause or cancel); the download_failed the stop triggers is intentional, not a failure. */ - private stopRequests = new SvelteMap(); + private stopRequests = new SvelteMap(); /** * Cancel an in-flight download or remove a previously downloaded/failed model @@ -110,7 +114,7 @@ export class ModelStatusManager { // in-flight: the kill triggers download_failed over the feed; mark it as a // user cancel so it settles silently instead of toasting a failure if (this.downloadProgress.has(repoWithTag)) { - this.stopRequests.set(repoWithTag, 'cancel'); + this.stopRequests.set(repoWithTag, DownloadStopRequest.CANCEL); } // a downloaded model registers under the name the router derived from the @@ -342,7 +346,7 @@ export class ModelStatusManager { this.subscribe(); - this.stopRequests.set(repoWithTag, 'pause'); + this.stopRequests.set(repoWithTag, DownloadStopRequest.PAUSE); try { await ModelsService.unload(repoWithTag); @@ -416,7 +420,7 @@ export class ModelStatusManager { * delete-and-retry path. */ private applyDownloadFinished(event: ApiModelsSseEvent): void { - let request: 'pause' | 'cancel' | undefined; + let request: DownloadStopRequest | undefined; if (event.event === ServerModelsSseEventType.DOWNLOAD_FAILED) { request = this.stopRequests.get(event.model); @@ -427,7 +431,7 @@ export class ModelStatusManager { this.downloadProgress.delete(event.model); - if (request === 'cancel') { + if (request === DownloadStopRequest.CANCEL) { // user cancel: settle silently, the feed's model_remove cleans up the entry this.failedDownloads.delete(event.model); this.pausedDownloads.delete(event.model); @@ -435,7 +439,7 @@ export class ModelStatusManager { return; } - if (request === 'pause') { + if (request === DownloadStopRequest.PAUSE) { this.pausedDownloads.set(event.model, progress); this.failedDownloads.delete(event.model);