diff --git a/tools/ui/src/lib/services/models.service.ts b/tools/ui/src/lib/services/models.service.ts index bccda35c2b..3157d9145b 100644 --- a/tools/ui/src/lib/services/models.service.ts +++ b/tools/ui/src/lib/services/models.service.ts @@ -8,7 +8,7 @@ import { base } from '$app/paths'; import { API_MODELS, MODEL_ID, type ModelSidecar, sidecarFromFileToken } from '$lib/constants'; -import { ServerModelStatus } from '$lib/enums'; +import { ModelAuxSidecar, ModelDraftSidecar, ServerModelStatus } from '$lib/enums'; import type { ParsedModelId } from '$lib/types/models'; import { apiDelete, @@ -20,9 +20,32 @@ import { } from '$lib/utils'; import { getAuthHeaders } from '$lib/utils/api-headers'; +/** Sidecar filename tokens, mirrored from the enums for entry-tag checks. */ +const SIDECAR_TOKENS: readonly string[] = [ + ...Object.values(ModelDraftSidecar), + ...Object.values(ModelAuxSidecar) +]; + export class ModelsService { private static readonly SSE_RECONNECT_MS = 1000; + /** + * True when a router entry id is a sidecar-only entry, e.g. `org/model:Q4_0-mtp` + * or `org/model:mmproj`. Such entries mark a downloaded sidecar file, not a + * loadable model, so the selector skips them. + */ + static isSidecarEntry(modelId: string): boolean { + const idx = modelId.indexOf(MODEL_ID.QUANTIZATION_SEPARATOR); + + 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); + } + /** * Build the `:` string expected by POST /models from a parsed * filename quant + optional sidecar type. Used by the model download diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts index 095b343b9f..b95014168a 100644 --- a/tools/ui/src/lib/stores/models/index.svelte.ts +++ b/tools/ui/src/lib/stores/models/index.svelte.ts @@ -359,27 +359,41 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { * they differ only in which endpoint is called. */ private buildModelOptions(response: ApiModelsListResponse): ModelOption[] { - return response.data.map((item: ApiModelDataEntry, index: number) => { - const details = response.models?.[index]; - const rawCapabilities = Array.isArray(details?.capabilities) ? details?.capabilities : []; - const displayNameSource = - details?.name && details.name.trim().length > 0 ? details.name : item.id; - const modelId = details?.model || item.id; + const entries: { + details?: ApiModelsListResponse['models'][number]; + item: ApiModelDataEntry; + }[] = response.data.map((item: ApiModelDataEntry, index: number) => ({ + details: response.models?.[index], + item + })); - return { - aliases: item.aliases ?? [], - capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value)), - description: details?.description, - details: details?.details, - id: item.id, - meta: item.meta ?? null, - modalities: this.props.buildArchitectureModalities(item.architecture), - model: modelId, - name: this.toDisplayName(displayNameSource), - parsedId: ModelsService.parseModelId(modelId), - tags: item.tags ?? [] - }; - }); + return ( + entries + // sidecar entries mark downloaded sidecar files, not loadable models + .filter(({ item }) => !ModelsService.isSidecarEntry(item.id)) + .map(({ details, item }) => { + const rawCapabilities = Array.isArray(details?.capabilities) ? details?.capabilities : []; + const displayNameSource = + details?.name && details.name.trim().length > 0 ? details.name : item.id; + const modelId = details?.model || item.id; + + return { + aliases: item.aliases ?? [], + capabilities: rawCapabilities.filter((value: unknown): value is string => + Boolean(value) + ), + description: details?.description, + details: details?.details, + id: item.id, + meta: item.meta ?? null, + modalities: this.props.buildArchitectureModalities(item.architecture), + model: modelId, + name: this.toDisplayName(displayNameSource), + parsedId: ModelsService.parseModelId(modelId), + tags: item.tags ?? [] + }; + }) + ); } /** Fetch models in MODEL mode (single model, standard OpenAI-compatible). */