diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte index bc97f7370e..63b8ac6647 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte @@ -12,7 +12,7 @@ } from '@lucide/svelte'; import { ActionIcon, ModelId } from '$lib/components/app'; import { ICON_CLASS_DEFAULT } from '$lib/constants'; - import { ServerModelStatus } from '$lib/enums'; + import { ModelCapability, ServerModelStatus } from '$lib/enums'; import { modelsStore } from '$lib/stores'; import type { ModelOption } from '$lib/types/models'; import { modelLoadFraction, modelLoadProgressText } from '$lib/utils'; @@ -61,7 +61,7 @@ let modalities = $derived(option.modalities); let capabilities = $derived.by(() => ({ reasoning: modelsStore.props.checkModelSupportsThinking(option.model), - tools: option.capabilities.includes('tools') + tools: option.capabilities.includes(ModelCapability.TOOL_USE) })); diff --git a/tools/ui/src/lib/constants/model-id.constants.ts b/tools/ui/src/lib/constants/model-id.constants.ts index b4c9e50daf..9fe87d1cc4 100644 --- a/tools/ui/src/lib/constants/model-id.constants.ts +++ b/tools/ui/src/lib/constants/model-id.constants.ts @@ -7,22 +7,16 @@ import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums'; /** Any sidecar file type: a draft variant or an auxiliary sidecar like mmproj. */ export type ModelSidecar = ModelDraftSidecar | ModelAuxSidecar; -/** Lowercase filename token for each sidecar, e.g. `mtp-Q4_0.gguf`, `mmproj-F16.gguf`. */ -export const SIDECAR_FILE_TOKENS: Record = { - [ModelAuxSidecar.MMPROJ]: 'mmproj', - [ModelDraftSidecar.DFLASH]: 'dflash', - [ModelDraftSidecar.DSPARK]: 'dspark', - [ModelDraftSidecar.EAGLE3]: 'eagle3', - [ModelDraftSidecar.MTP]: 'mtp' -}; - -const SIDECARS_BY_FILE_TOKEN = Object.fromEntries( - Object.entries(SIDECAR_FILE_TOKENS).map(([sidecar, token]) => [token, sidecar]) -) as Record; +/** All sidecar filename tokens. Enum values are the lowercase filename tokens, e.g. `mtp-Q4_0.gguf`, `mmproj-F16.gguf`. */ +const SIDECAR_TOKENS: string[] = [ + ...Object.values(ModelDraftSidecar), + ...Object.values(ModelAuxSidecar) +]; +const SIDECAR_TOKEN_SET = new Set(SIDECAR_TOKENS); /** Map a lowercase filename token (e.g. `mtp`) to its sidecar enum value. */ export function sidecarFromFileToken(token: string): ModelSidecar | null { - return SIDECARS_BY_FILE_TOKEN[token] ?? null; + return SIDECAR_TOKEN_SET.has(token) ? (token as ModelSidecar) : null; } export function isDraftSidecar(sidecar: ModelSidecar): sidecar is ModelDraftSidecar { @@ -33,15 +27,17 @@ export function isAuxSidecar(sidecar: ModelSidecar): sidecar is ModelAuxSidecar return (Object.values(ModelAuxSidecar) as string[]).includes(sidecar); } +const SIDECAR_TOKEN_ALTERNATION = SIDECAR_TOKENS.join('|'); + export const MODEL_ID = { /** * Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`. * The leading `A`/`a` distinguishes it from a regular params segment. */ - ACTIVATED_PARAMS_RE: /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/, + ACTIVATED_PARAMS_REGEX: /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/, /** Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`. */ - CUSTOM_QUANTIZATION_PREFIX_RE: /^UD$/i, + CUSTOM_QUANTIZATION_PREFIX_REGEX: /^UD$/i, /** Container format segments to exclude from tags (every model uses these). */ IGNORED_SEGMENTS: new Set(['GGUF', 'GGML']), /** Sentinel value returned by `indexOf` when a substring is not found. */ @@ -53,13 +49,13 @@ export const MODEL_ID = { * The optional leading `E` covers effective-parameter sizes, e.g. Gemma's * `E2B`/`E4B` (MatFormer models sized by resident params). */ - PARAMS_RE: /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/, + PARAMS_REGEX: /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/, /** * Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`. * Case-insensitive to handle both uppercase and lowercase inputs. */ - QUANTIZATION_SEGMENT_RE: /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i, + QUANTIZATION_SEGMENT_REGEX: /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i, /** Separates the model path from the quantization tag, e.g. `model:Q4_K_M`. */ QUANTIZATION_SEPARATOR: ':', @@ -73,7 +69,7 @@ export const MODEL_ID = { * `eagle3-.gguf`, `mmproj-.gguf`. Captures the bare type * token for typed lookup. */ - SIDECAR_PREFIX_RE: /^(mtp|dflash|dspark|eagle3|mmproj)-(.*)$/i, + SIDECAR_PREFIX_REGEX: new RegExp(`^(${SIDECAR_TOKEN_ALTERNATION})-(.*)$`, 'i'), /** * Trailing `-` suffix marking a GGUF with an embedded draft in the @@ -81,8 +77,8 @@ export const MODEL_ID = { * `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. The captured prefix is the * candidate model id; the caller decides whether it looks quantized. */ - SIDECAR_SUFFIX_RE: /^(.*)-(mtp|dflash|dspark|eagle3)$/i, + SIDECAR_SUFFIX_REGEX: new RegExp(`^(.*)-(${SIDECAR_TOKEN_ALTERNATION})$`, 'i'), /** Matches a trailing weight file extension, e.g. `model.gguf` -> `model`. */ - WEIGHT_EXTENSION_RE: /\.(gguf|ggml)$/i + WEIGHT_EXTENSION_REGEX: /\.(gguf|ggml)$/i }; diff --git a/tools/ui/src/lib/enums/model.enums.ts b/tools/ui/src/lib/enums/model.enums.ts index b6b97ab2b5..b76d4a2494 100644 --- a/tools/ui/src/lib/enums/model.enums.ts +++ b/tools/ui/src/lib/enums/model.enums.ts @@ -7,7 +7,7 @@ export enum ModelModality { export enum ModelCapability { REASONING = 'REASONING', - TOOL_USE = 'TOOL_USE' + TOOL_USE = 'tools' } /** @@ -16,13 +16,13 @@ export enum ModelCapability { */ export enum ModelDraftSidecar { /** DFlash block-diffusion draft (spec-type draft-dflash). */ - DFLASH = 'DFLASH', + DFLASH = 'dflash', /** DSpark block-diffusion draft (spec-type draft-dspark). */ - DSPARK = 'DSPARK', + DSPARK = 'dspark', /** EAGLE-3 speculative draft (spec-type draft-eagle3). */ - EAGLE3 = 'EAGLE3', + EAGLE3 = 'eagle3', /** Multi-token-prediction draft head (spec-type draft-mtp). */ - MTP = 'MTP' + MTP = 'mtp' } /** @@ -31,5 +31,5 @@ export enum ModelDraftSidecar { */ export enum ModelAuxSidecar { /** Multimodal projector: unlocks vision and/or audio input modalities. */ - MMPROJ = 'MMPROJ' + MMPROJ = 'mmproj' } diff --git a/tools/ui/src/lib/services/models.service.ts b/tools/ui/src/lib/services/models.service.ts index 5bebbbb9fe..8d81d3f49a 100644 --- a/tools/ui/src/lib/services/models.service.ts +++ b/tools/ui/src/lib/services/models.service.ts @@ -127,11 +127,11 @@ export class ModelsService { // strip directory path and weight extension so a bare `-m /path/file.gguf` // parses like a clean repo id; the HF `org/model` form is preserved - let source = normalizeModelName(modelId).replace(MODEL_ID.WEIGHT_EXTENSION_RE, ''); + let source = normalizeModelName(modelId).replace(MODEL_ID.WEIGHT_EXTENSION_REGEX, ''); // 0. Detect sidecar prefix (mtp-, dflash-, mmproj-) before any other // splitting so the inner id parses cleanly. - const prefixMatch = source.match(MODEL_ID.SIDECAR_PREFIX_RE); + const prefixMatch = source.match(MODEL_ID.SIDECAR_PREFIX_REGEX); if (prefixMatch) { result.sidecar = sidecarFromFileToken(prefixMatch[1].toLowerCase()); @@ -139,7 +139,7 @@ export class ModelsService { // a sidecar filename's remainder may be just the quant token, // e.g. `mtp-Q4_0.gguf` or `mmproj-F16.gguf` - if (MODEL_ID.QUANTIZATION_SEGMENT_RE.test(source)) { + if (MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(source)) { result.quantization = source.toUpperCase(); source = ''; } @@ -148,13 +148,13 @@ export class ModelsService { // Only strip it when the segment preceding it looks like a real quant // token, so a model literally named `MyModel-mtp` is not mistaken for a // draft one. - const suffixMatch = source.match(MODEL_ID.SIDECAR_SUFFIX_RE); + const suffixMatch = source.match(MODEL_ID.SIDECAR_SUFFIX_REGEX); if (suffixMatch) { const candidate = suffixMatch[1]; const headSeg = candidate.split(MODEL_ID.SEGMENT_SEPARATOR).pop(); - if (headSeg && MODEL_ID.QUANTIZATION_SEGMENT_RE.test(headSeg)) { + if (headSeg && MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(headSeg)) { result.sidecar = sidecarFromFileToken(suffixMatch[2].toLowerCase()); source = candidate; } @@ -191,7 +191,7 @@ export class ModelsService { if (dotIdx !== MODEL_ID.NOT_FOUND && !result.quantization) { const afterDot = modelStr.slice(dotIdx + 1); - if (MODEL_ID.QUANTIZATION_SEGMENT_RE.test(afterDot)) { + if (MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(afterDot)) { result.quantization = afterDot; modelStr = modelStr.slice(0, dotIdx); } @@ -206,8 +206,8 @@ export class ModelsService { const last = segments[segments.length - 1]; const secondLast = segments.length > 2 ? segments[segments.length - 2] : null; - if (MODEL_ID.QUANTIZATION_SEGMENT_RE.test(last)) { - if (secondLast && MODEL_ID.CUSTOM_QUANTIZATION_PREFIX_RE.test(secondLast)) { + if (MODEL_ID.QUANTIZATION_SEGMENT_REGEX.test(last)) { + if (secondLast && MODEL_ID.CUSTOM_QUANTIZATION_PREFIX_REGEX.test(secondLast)) { result.quantization = `${secondLast}-${last}`; segments.splice(segments.length - 2, 2); } else { @@ -224,10 +224,10 @@ export class ModelsService { for (let i = 0; i < segments.length; i++) { const seg = segments[i]; - if (paramsIdx === MODEL_ID.NOT_FOUND && MODEL_ID.PARAMS_RE.test(seg)) { + if (paramsIdx === MODEL_ID.NOT_FOUND && MODEL_ID.PARAMS_REGEX.test(seg)) { paramsIdx = i; result.params = seg.toUpperCase(); - } else if (paramsIdx !== MODEL_ID.NOT_FOUND && MODEL_ID.ACTIVATED_PARAMS_RE.test(seg)) { + } else if (paramsIdx !== MODEL_ID.NOT_FOUND && MODEL_ID.ACTIVATED_PARAMS_REGEX.test(seg)) { activatedParamsIdx = i; result.activatedParams = seg.toUpperCase(); } diff --git a/tools/ui/tests/unit/model-id-parser.test.ts b/tools/ui/tests/unit/model-id-parser.test.ts index 82009acb9d..bf68cdec66 100644 --- a/tools/ui/tests/unit/model-id-parser.test.ts +++ b/tools/ui/tests/unit/model-id-parser.test.ts @@ -328,24 +328,24 @@ describe('parseModelId', () => { // sidecar prefix: bare filename or multi-slash path reduces to the filename expect(parseModelId('mtp-Q4_0.gguf')).toMatchObject({ quantization: 'Q4_0', - sidecar: 'MTP' + sidecar: 'mtp' }); expect(parseModelId('ggml-org/Model-GGUF/mtp-Q4_0.gguf')).toMatchObject({ quantization: 'Q4_0', - sidecar: 'MTP' + sidecar: 'mtp' }); expect(parseModelId('ggml-org/Model-GGUF/mmproj-F16.gguf')).toMatchObject({ quantization: 'F16', - sidecar: 'MMPROJ' + sidecar: 'mmproj' }); // embedded-draft suffix: - only strips when preceded by a quant expect(parseModelId('ggml-org/Hy3-IQ1_M-mtp')).toMatchObject({ modelName: 'Hy3', quantization: 'IQ1_M', - sidecar: 'MTP' + sidecar: 'mtp' }); // a model literally named MyModel-mtp is not a draft