diff --git a/tools/ui/src/lib/services/models.service.ts b/tools/ui/src/lib/services/models.service.ts index 0b337775f4..96902a0b9d 100644 --- a/tools/ui/src/lib/services/models.service.ts +++ b/tools/ui/src/lib/services/models.service.ts @@ -7,7 +7,7 @@ */ import { base } from '$app/paths'; -import { API_MODELS, MODEL_ID, type ModelSidecar, SIDECAR_TOKENS } from '$lib/constants'; +import { API_MODELS, MODEL_ID, type ModelSidecar } from '$lib/constants'; import { ServerModelStatus } from '$lib/enums'; import type { ParsedModelId } from '$lib/types/models'; import { @@ -17,6 +17,7 @@ import { extractSseDataPayload, normalizeModelName, sidecarFromFileToken, + sidecarFromTag, splitSseRecords } from '$lib/utils'; import { getAuthHeaders } from '$lib/utils/api-headers'; @@ -94,11 +95,7 @@ export class ModelsService { if (idx === MODEL_ID.NOT_FOUND) return false; - const tag = modelId.slice(idx + 1).toLowerCase(); - const dash = tag.lastIndexOf(MODEL_ID.SEGMENT_SEPARATOR); - const token = dash === -1 ? tag : tag.slice(dash + 1); - - return SIDECAR_TOKENS.includes(token); + return sidecarFromTag(modelId.slice(idx + 1)) !== null; } /** diff --git a/tools/ui/src/lib/stores/models/status.svelte.ts b/tools/ui/src/lib/stores/models/status.svelte.ts index ab237e5d8d..b0ac083664 100644 --- a/tools/ui/src/lib/stores/models/status.svelte.ts +++ b/tools/ui/src/lib/stores/models/status.svelte.ts @@ -169,8 +169,11 @@ export class ModelStatusManager { // localStorage; the progress snapshot is stale after a reload and stays null try { const raw = localStorage.getItem(PAUSED_MODEL_DOWNLOADS_LOCALSTORAGE_KEY); + const parsed: unknown = JSON.parse(raw ?? '[]'); - for (const repoWithTag of JSON.parse(raw ?? '[]') as string[]) { + if (!Array.isArray(parsed)) return; + + for (const repoWithTag of parsed.filter((id): id is string => typeof id === 'string')) { this.pausedDownloads.set(repoWithTag, null); } } catch { diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index 988a7a237e..21d84dc8d6 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -108,7 +108,7 @@ export { export { normalizeModelName, isValidModelName } from './model-names'; // Sidecar token utilities -export { isAuxSidecar, isDraftSidecar, sidecarFromFileToken } from './sidecars'; +export { isAuxSidecar, isDraftSidecar, sidecarFromFileToken, sidecarFromTag } from './sidecars'; // Portal utilities export { portalToBody } from './portal-to-body'; diff --git a/tools/ui/src/lib/utils/sidecars.ts b/tools/ui/src/lib/utils/sidecars.ts index a3eb3841fd..52532b135a 100644 --- a/tools/ui/src/lib/utils/sidecars.ts +++ b/tools/ui/src/lib/utils/sidecars.ts @@ -1,4 +1,4 @@ -import { type ModelSidecar, SIDECAR_TOKENS } from '$lib/constants'; +import { MODEL_ID, type ModelSidecar, SIDECAR_TOKENS } from '$lib/constants'; import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums'; const SIDECAR_TOKEN_SET = new Set(SIDECAR_TOKENS); @@ -10,6 +10,17 @@ export function sidecarFromFileToken(token: string): ModelSidecar | null { return SIDECAR_TOKEN_SET.has(token) ? (token as ModelSidecar) : null; } +/** + * Sidecar a download tag points at: the segment after the last dash, + * e.g. `q4_0-mtp` -> `mtp`, `mmproj` -> `mmproj`. Returns null for quant-only + * tags and tags whose tail is not a sidecar token. + */ +export function sidecarFromTag(tag: string): ModelSidecar | null { + const token = tag.toLowerCase().split(MODEL_ID.SEGMENT_SEPARATOR).pop() ?? ''; + + return sidecarFromFileToken(token); +} + export function isDraftSidecar(sidecar: ModelSidecar): sidecar is ModelDraftSidecar { return DRAFT_SIDECAR_SET.has(sidecar); } diff --git a/tools/ui/tests/unit/model-sidecar-grammar.test.ts b/tools/ui/tests/unit/model-sidecar-grammar.test.ts index eda2b5d62d..1ad59171a9 100644 --- a/tools/ui/tests/unit/model-sidecar-grammar.test.ts +++ b/tools/ui/tests/unit/model-sidecar-grammar.test.ts @@ -1,6 +1,7 @@ import { ModelAuxSidecar, ModelDraftSidecar, SidecarForm } from '$lib/enums'; import { HuggingFaceService } from '$lib/services/huggingface.service'; import { ModelsService } from '$lib/services/models.service'; +import { sidecarFromTag } from '$lib/utils'; import { describe, expect, it } from 'vitest'; const { buildDownloadTag, isSidecarEntry } = ModelsService; @@ -125,3 +126,15 @@ describe('isSidecarEntry', () => { expect(isSidecarEntry('org/repo')).toBe(false); }); }); + +describe('sidecarFromTag', () => { + it('reads the token after the last dash', () => { + expect(sidecarFromTag('Q4_0-mtp')).toBe(ModelDraftSidecar.MTP); + expect(sidecarFromTag('mmproj')).toBe(ModelAuxSidecar.MMPROJ); + }); + + it('returns null for quant-only and unrelated tags', () => { + expect(sidecarFromTag('Q4_K_XL')).toBeNull(); + expect(sidecarFromTag('UD-Q4_K_XL')).toBeNull(); + }); +});