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
This commit is contained in:
Aleksander Grygier
2026-09-04 13:19:01 +02:00
parent a8db34b0f3
commit cebb6f38cb
7 changed files with 57 additions and 18 deletions
@@ -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 {
@@ -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;
@@ -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.
+2
View File
@@ -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';
+10
View File
@@ -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'
}
@@ -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;
+15 -11
View File
@@ -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<string, 'pause' | 'cancel'>();
private stopRequests = new SvelteMap<string, DownloadStopRequest>();
/**
* 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);