diff --git a/tools/ui/src/lib/services/backends.service.ts b/tools/ui/src/lib/services/backends.service.ts index 806291d75d..884ed042c7 100644 --- a/tools/ui/src/lib/services/backends.service.ts +++ b/tools/ui/src/lib/services/backends.service.ts @@ -7,6 +7,7 @@ */ import { API_MODELS, LOCAL_BACKEND_ID } from '$lib/constants'; +import { ModelsService } from '$lib/services/models.service'; import type { ApiModelsListResponse, Backend, ModelOption } from '$lib/types'; import { isAbortError } from '$lib/utils/abort'; import { apiUrl } from '$lib/utils/api-base'; @@ -130,6 +131,9 @@ function normalizeBackendModel(entry: unknown): ModelOption[] { if (!id) return []; + // a llama-compat server lists its projector and draft sidecars as models too + if (ModelsService.isSidecarEntry(id)) return []; + return [ { capabilities: [], contextLength: readModelContextLength(raw), id, model: id, name: id } ]; diff --git a/tools/ui/src/lib/services/models.service.ts b/tools/ui/src/lib/services/models.service.ts index b141551d1c..13601e0c8f 100644 --- a/tools/ui/src/lib/services/models.service.ts +++ b/tools/ui/src/lib/services/models.service.ts @@ -131,13 +131,23 @@ export class ModelsService { static isSidecarEntry(modelId: string): boolean { const idx = modelId.indexOf(MODEL_ID.QUANTIZATION_SEPARATOR); - if (idx === MODEL_ID.NOT_FOUND) return false; + if (idx !== MODEL_ID.NOT_FOUND) { + const tag = modelId.slice(idx + 1).toLowerCase(); + const dash = tag.lastIndexOf(MODEL_ID.SEGMENT_SEPARATOR); + const token = dash === -1 ? tag : tag.slice(dash + 1); - const tag = modelId.slice(idx + 1).toLowerCase(); - const dash = tag.lastIndexOf(MODEL_ID.SEGMENT_SEPARATOR); - const token = dash === -1 ? tag : tag.slice(dash + 1); + if (SIDECAR_TOKENS.includes(token)) return true; + } - return SIDECAR_TOKENS.includes(token); + // the router also lists projector and draft files by filename, e.g. + // `org/model-mmproj-F16.gguf` or `mmproj-model.gguf` + const name = modelId.split('/').pop() ?? modelId; + + return ( + MODEL_ID.SIDECAR_INFIX_REGEX.test(name) || + MODEL_ID.SIDECAR_PREFIX_REGEX.test(name) || + MODEL_ID.SIDECAR_SUFFIX_REGEX.test(name) + ); } /** diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts index 455ff83fe4..21695259f6 100644 --- a/tools/ui/src/lib/stores/models/index.svelte.ts +++ b/tools/ui/src/lib/stores/models/index.svelte.ts @@ -668,8 +668,13 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { return ( entries - // sidecar entries mark downloaded sidecar files, not loadable models - .filter(({ item }) => !ModelsService.isSidecarEntry(item.id)) + // sidecar entries mark downloaded sidecar files, not loadable models; + // the router also lists projector files by filename, which carry the + // sidecar token inside the id rather than after a colon + .filter( + ({ item }) => + !ModelsService.isSidecarEntry(item.id) && !ModelsService.parseModelId(item.id).sidecar + ) // in-flight downloads are not usable models yet; the selector tracks // them in its "Download in progress" section instead .filter(({ item }) => item.status?.value !== ServerModelStatus.DOWNLOADING)