ui : filter sidecar-only entries from the model selector

Entries like repo:Q4_0-mtp mark a downloaded sidecar file, not a loadable model.

Assisted-by: pi:zai-org/GLM-5.3
This commit is contained in:
Aleksander Grygier
2026-09-02 12:39:09 +02:00
parent 0fa3c06edf
commit 83ef59ebff
2 changed files with 58 additions and 21 deletions
+24 -1
View File
@@ -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 `<repo>:<tag>` string expected by POST /models from a parsed
* filename quant + optional sidecar type. Used by the model download
+34 -20
View File
@@ -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). */