mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-06 17:57:44 +02:00
ui : parse sidecar types in model ids
Add ModelDraftSidecar / ModelAuxSidecar enums with a ModelSidecar union type; mmproj is the only auxiliary sidecar (single member, covers vision and audio input). Add SIDECAR_PREFIX/SUFFIX_RE regex matching the server's filename conventions, and type guards + enum-file-token helpers in model-id.constants.ts. Extend parseModelId to detect sidecar filename tokens (mtp-, mmproj-, etc) and expose isDraftSidecar / isAuxSidecar / sidecarFromFileToken helpers. Add ModelCapability.TOOL_USE with icon/label/flag mappings. Assisted-by: pi
This commit is contained in:
Vendored
-2
@@ -20,13 +20,11 @@ import type {
|
||||
ApiModelDataEntry,
|
||||
ApiModelLoadStage,
|
||||
ApiModelsListResponse,
|
||||
ApiModelsLoadRequest,
|
||||
ApiModelsLoadResponse,
|
||||
ApiModelsSseData,
|
||||
ApiModelsSseEvent,
|
||||
ApiModelsSseProgress,
|
||||
ApiModelsStatusResponse,
|
||||
ApiModelsUnloadRequest,
|
||||
ApiModelsUnloadResponse,
|
||||
ApiProcessingState,
|
||||
ChatAttachmentDisplayItem,
|
||||
|
||||
@@ -60,7 +60,8 @@
|
||||
let loadTitle = $derived(modelLoadProgressText(loadProgress));
|
||||
let modalities = $derived(option.modalities);
|
||||
let capabilities = $derived.by(() => ({
|
||||
reasoning: modelsStore.props.checkModelSupportsThinking(option.model)
|
||||
reasoning: modelsStore.props.checkModelSupportsThinking(option.model),
|
||||
tools: option.capabilities.includes('tools')
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
Image as ImageIcon,
|
||||
Lightbulb as ReasoningIcon,
|
||||
Mic as AudioIcon,
|
||||
Video as VideoIcon
|
||||
Video as VideoIcon,
|
||||
Wrench as ToolUseIcon
|
||||
} from '@lucide/svelte';
|
||||
import { FileTypeCategory, ModelCapability, ModelModality } from '$lib/enums';
|
||||
import type { ModelCapabilities, ModelModalities } from '$lib/types/models';
|
||||
@@ -49,16 +50,19 @@ export const MODALITY_FLAG_KEYS: Record<
|
||||
};
|
||||
|
||||
export const CAPABILITY_ICONS: Record<ModelCapability, Component> = {
|
||||
[ModelCapability.REASONING]: ReasoningIcon
|
||||
[ModelCapability.REASONING]: ReasoningIcon,
|
||||
[ModelCapability.TOOL_USE]: ToolUseIcon
|
||||
} as const;
|
||||
|
||||
export const CAPABILITY_LABELS: Record<ModelCapability, string> = {
|
||||
[ModelCapability.REASONING]: 'Reasoning'
|
||||
[ModelCapability.REASONING]: 'Reasoning',
|
||||
[ModelCapability.TOOL_USE]: 'Tool use'
|
||||
} as const;
|
||||
|
||||
/** Maps a ModelCapability to the boolean flag it drives on the ModelCapabilities type */
|
||||
export const CAPABILITY_FLAG_KEYS: Record<ModelCapability, keyof ModelCapabilities> = {
|
||||
[ModelCapability.REASONING]: 'reasoning'
|
||||
[ModelCapability.REASONING]: 'reasoning',
|
||||
[ModelCapability.TOOL_USE]: 'tools'
|
||||
};
|
||||
|
||||
// Shared SVG icon strings for copy and preview buttons
|
||||
|
||||
@@ -2,6 +2,37 @@
|
||||
* Parsing of `org/ModelName[-tag][:quant]` style model IDs.
|
||||
*/
|
||||
|
||||
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<ModelSidecar, string> = {
|
||||
[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<string, ModelSidecar>;
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export function isDraftSidecar(sidecar: ModelSidecar): sidecar is ModelDraftSidecar {
|
||||
return (Object.values(ModelDraftSidecar) as string[]).includes(sidecar);
|
||||
}
|
||||
|
||||
export function isAuxSidecar(sidecar: ModelSidecar): sidecar is ModelAuxSidecar {
|
||||
return (Object.values(ModelAuxSidecar) as string[]).includes(sidecar);
|
||||
}
|
||||
|
||||
export const MODEL_ID = {
|
||||
/**
|
||||
* Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
|
||||
@@ -15,10 +46,8 @@ export const MODEL_ID = {
|
||||
IGNORED_SEGMENTS: new Set(['GGUF', 'GGML']),
|
||||
/** Sentinel value returned by `indexOf` when a substring is not found. */
|
||||
NOT_FOUND: -1,
|
||||
|
||||
/** Separates `<org>` from `<model>` in a model ID, e.g. `org/ModelName`. */
|
||||
ORG_SEPARATOR: '/',
|
||||
|
||||
/**
|
||||
* Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
|
||||
* The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
|
||||
@@ -38,6 +67,22 @@ export const MODEL_ID = {
|
||||
/** Separates named segments within the model path, e.g. `ModelName-7B-GGUF`. */
|
||||
SEGMENT_SEPARATOR: '-',
|
||||
|
||||
/**
|
||||
* Sidecar prefix that wraps a model id with a sidecar type, e.g.
|
||||
* `mtp-<name>.gguf`, `dflash-<name>.gguf`, `dspark-<name>.gguf`,
|
||||
* `eagle3-<name>.gguf`, `mmproj-<name>.gguf`. Captures the bare type
|
||||
* token for typed lookup.
|
||||
*/
|
||||
SIDECAR_PREFIX_RE: /^(mtp|dflash|dspark|eagle3|mmproj)-(.*)$/i,
|
||||
|
||||
/**
|
||||
* Trailing `-<type>` suffix marking a GGUF with an embedded draft in the
|
||||
* same weight file (MTP) or a sidecar download entry, e.g.
|
||||
* `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,
|
||||
|
||||
/** Matches a trailing weight file extension, e.g. `model.gguf` -> `model`. */
|
||||
WEIGHT_EXTENSION_RE: /\.(gguf|ggml)$/i
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export {
|
||||
JsonSchemaType
|
||||
} from './mcp.enums';
|
||||
|
||||
export { ModelCapability, ModelModality } from './model.enums';
|
||||
export { ModelAuxSidecar, ModelCapability, ModelDraftSidecar, ModelModality } from './model.enums';
|
||||
|
||||
export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './server.enums';
|
||||
|
||||
|
||||
@@ -6,5 +6,30 @@ export enum ModelModality {
|
||||
}
|
||||
|
||||
export enum ModelCapability {
|
||||
REASONING = 'REASONING'
|
||||
REASONING = 'REASONING',
|
||||
TOOL_USE = 'TOOL_USE'
|
||||
}
|
||||
|
||||
/**
|
||||
* Speculative-decoding draft sidecars (server spec-type draft-*).
|
||||
* Filenames use the lowercase token, e.g. `mtp-<name>.gguf` or `-mtp` suffix.
|
||||
*/
|
||||
export enum ModelDraftSidecar {
|
||||
/** DFlash block-diffusion draft (spec-type draft-dflash). */
|
||||
DFLASH = 'DFLASH',
|
||||
/** DSpark block-diffusion draft (spec-type draft-dspark). */
|
||||
DSPARK = 'DSPARK',
|
||||
/** EAGLE-3 speculative draft (spec-type draft-eagle3). */
|
||||
EAGLE3 = 'EAGLE3',
|
||||
/** Multi-token-prediction draft head (spec-type draft-mtp). */
|
||||
MTP = 'MTP'
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-draft sidecar file types. A sidecar is any auxiliary GGUF file
|
||||
* accompanying the main model weights.
|
||||
*/
|
||||
export enum ModelAuxSidecar {
|
||||
/** Multimodal projector: unlocks vision and/or audio input modalities. */
|
||||
MMPROJ = 'MMPROJ'
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { base } from '$app/paths';
|
||||
import { API_MODELS, MODEL_ID } from '$lib/constants';
|
||||
import { API_MODELS, MODEL_ID, type ModelSidecar, sidecarFromFileToken } from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import type { ParsedModelId } from '$lib/types/models';
|
||||
import {
|
||||
@@ -22,6 +22,30 @@ import { getAuthHeaders } from '$lib/utils/api-headers';
|
||||
export class ModelsService {
|
||||
private static readonly SSE_RECONNECT_MS = 1000;
|
||||
|
||||
/**
|
||||
* Build the `<repo>:<tag>` string expected by POST /models from a parsed
|
||||
* filename quant + optional sidecar type. Used by the model download
|
||||
* dialog so callers don't have to know about the tag conventions.
|
||||
*
|
||||
* @param repoId - HuggingFace repo id (e.g. `ggml-org/gemma-3-4b-it-GGUF`)
|
||||
* @param quant - Quantization token, e.g. `Q4_K_M`
|
||||
* @param sidecar - Sidecar type, uppercased into the tag (e.g. `MTP`)
|
||||
* @returns Repo id possibly suffixed with `:tag`
|
||||
*/
|
||||
static buildDownloadTag(
|
||||
repoId: string,
|
||||
quant: string | null,
|
||||
sidecar: ModelSidecar | null
|
||||
): string {
|
||||
if (!quant && !sidecar) return repoId;
|
||||
|
||||
if (!quant) return `${repoId}:${sidecar}`;
|
||||
|
||||
const tag = sidecar ? `${quant}-${sidecar}` : quant;
|
||||
|
||||
return `${repoId}:${tag}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model is loaded based on its metadata.
|
||||
*
|
||||
@@ -97,11 +121,46 @@ export class ModelsService {
|
||||
params: null,
|
||||
quantization: null,
|
||||
raw: modelId,
|
||||
sidecar: null,
|
||||
tags: []
|
||||
};
|
||||
|
||||
// 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
|
||||
const source = normalizeModelName(modelId).replace(MODEL_ID.WEIGHT_EXTENSION_RE, '');
|
||||
let source = normalizeModelName(modelId).replace(MODEL_ID.WEIGHT_EXTENSION_RE, '');
|
||||
|
||||
// 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);
|
||||
|
||||
if (prefixMatch) {
|
||||
result.sidecar = sidecarFromFileToken(prefixMatch[1].toLowerCase());
|
||||
source = prefixMatch[2];
|
||||
|
||||
// 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)) {
|
||||
result.quantization = source.toUpperCase();
|
||||
source = '';
|
||||
}
|
||||
} else {
|
||||
// 0b. Detect `-<type>` suffix (`-mtp`, `-dflash`, `-dspark`, `-eagle3`).
|
||||
// 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);
|
||||
|
||||
if (suffixMatch) {
|
||||
const candidate = suffixMatch[1];
|
||||
const headSeg = candidate.split(MODEL_ID.SEGMENT_SEPARATOR).pop();
|
||||
|
||||
if (headSeg && MODEL_ID.QUANTIZATION_SEGMENT_RE.test(headSeg)) {
|
||||
result.sidecar = sidecarFromFileToken(suffixMatch[2].toLowerCase());
|
||||
source = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`)
|
||||
const colonIdx = source.indexOf(MODEL_ID.QUANTIZATION_SEPARATOR);
|
||||
|
||||
|
||||
Vendored
+3
@@ -1,3 +1,4 @@
|
||||
import type { ModelSidecar } from '$lib/constants/model-id.constants';
|
||||
import type { ApiModelDataEntry, ApiModelDetails, ApiModelLoadStage } from '$lib/types/api';
|
||||
|
||||
export interface ModelModalities {
|
||||
@@ -8,6 +9,7 @@ export interface ModelModalities {
|
||||
|
||||
export interface ModelCapabilities {
|
||||
reasoning: boolean;
|
||||
tools: boolean;
|
||||
}
|
||||
|
||||
export interface ModelOption {
|
||||
@@ -42,6 +44,7 @@ export interface ParsedModelId {
|
||||
params: string | null;
|
||||
activatedParams: string | null;
|
||||
quantization: string | null;
|
||||
sidecar: ModelSidecar | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: null,
|
||||
raw: 'model-name-1',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -22,6 +23,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: null,
|
||||
raw: 'org/model-name-2',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
});
|
||||
@@ -105,6 +107,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'Q2_K_XL',
|
||||
raw: 'unsloth/DeepSeek-V4-Flash-0731-GGUF:Q2_K_XL',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -115,6 +118,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'Q4_K_XL',
|
||||
raw: 'unsloth/Laguna-S-2.1-GGUF:Q4_K_XL',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -125,6 +129,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: null,
|
||||
raw: 'org/Model-Name-GGUF',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
});
|
||||
@@ -137,6 +142,7 @@ describe('parseModelId', () => {
|
||||
params: '8B',
|
||||
quantization: null,
|
||||
raw: 'meta-llama/Llama-3.1-8B',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -147,6 +153,7 @@ describe('parseModelId', () => {
|
||||
params: '120B',
|
||||
quantization: 'MXFP4',
|
||||
raw: 'openai/gpt-oss-120b-MXFP4',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -157,6 +164,7 @@ describe('parseModelId', () => {
|
||||
params: '20B',
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'openai/gpt-oss-20b:Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -167,6 +175,7 @@ describe('parseModelId', () => {
|
||||
params: '30B',
|
||||
quantization: 'BF16',
|
||||
raw: 'Qwen/Qwen3-Coder-30B-A3B-Instruct-1M-BF16',
|
||||
sidecar: null,
|
||||
tags: ['Instruct', '1M']
|
||||
});
|
||||
});
|
||||
@@ -179,6 +188,7 @@ describe('parseModelId', () => {
|
||||
params: '17B',
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'meta-llama/Llama-4-Scout-17B-16E-Instruct-Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: ['16E', 'Instruct']
|
||||
});
|
||||
|
||||
@@ -189,6 +199,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'IQ4_XS',
|
||||
raw: 'MiniMaxAI/MiniMax-M2-IQ4_XS',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -199,6 +210,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'UD-Q3_K_XL',
|
||||
raw: 'MiniMaxAI/MiniMax-M2-UD-Q3_K_XL',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -209,6 +221,7 @@ describe('parseModelId', () => {
|
||||
params: '123B',
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'mistralai/Devstral-2-123B-Instruct-2512-Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: ['Instruct', '2512']
|
||||
});
|
||||
|
||||
@@ -219,6 +232,7 @@ describe('parseModelId', () => {
|
||||
params: '24B',
|
||||
quantization: 'Q8_0',
|
||||
raw: 'mistralai/Devstral-Small-2-24B-Instruct-2512-Q8_0',
|
||||
sidecar: null,
|
||||
tags: ['Instruct', '2512']
|
||||
});
|
||||
|
||||
@@ -229,6 +243,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'MXFP4_MOE',
|
||||
raw: 'noctrex/GLM-4.7-Flash-MXFP4_MOE',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -239,6 +254,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'Qwen/Qwen3-Coder-Next-Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -249,6 +265,7 @@ describe('parseModelId', () => {
|
||||
params: '120B',
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'openai/gpt-oss-120b-Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -259,6 +276,7 @@ describe('parseModelId', () => {
|
||||
params: '20B',
|
||||
quantization: 'F16',
|
||||
raw: 'openai/gpt-oss-20b-F16',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
|
||||
@@ -269,6 +287,7 @@ describe('parseModelId', () => {
|
||||
params: null,
|
||||
quantization: 'Q4_K_M',
|
||||
raw: 'nomic-embed-text-v2-moe.Q4_K_M',
|
||||
sidecar: null,
|
||||
tags: []
|
||||
});
|
||||
});
|
||||
@@ -304,4 +323,41 @@ describe('parseModelId', () => {
|
||||
tags: ['it']
|
||||
});
|
||||
});
|
||||
|
||||
it('parses sidecar file tokens', () => {
|
||||
// sidecar prefix: bare filename or multi-slash path reduces to the filename
|
||||
expect(parseModelId('mtp-Q4_0.gguf')).toMatchObject({
|
||||
quantization: 'Q4_0',
|
||||
sidecar: 'MTP'
|
||||
});
|
||||
|
||||
expect(parseModelId('ggml-org/Model-GGUF/mtp-Q4_0.gguf')).toMatchObject({
|
||||
quantization: 'Q4_0',
|
||||
sidecar: 'MTP'
|
||||
});
|
||||
|
||||
expect(parseModelId('ggml-org/Model-GGUF/mmproj-F16.gguf')).toMatchObject({
|
||||
quantization: 'F16',
|
||||
sidecar: 'MMPROJ'
|
||||
});
|
||||
|
||||
// embedded-draft suffix: -<type> only strips when preceded by a quant
|
||||
expect(parseModelId('ggml-org/Hy3-IQ1_M-mtp')).toMatchObject({
|
||||
modelName: 'Hy3',
|
||||
quantization: 'IQ1_M',
|
||||
sidecar: 'MTP'
|
||||
});
|
||||
|
||||
// a model literally named MyModel-mtp is not a draft
|
||||
expect(parseModelId('ggml-org/MyModel-mtp')).toMatchObject({
|
||||
modelName: 'MyModel-mtp',
|
||||
sidecar: null
|
||||
});
|
||||
|
||||
// no sidecar
|
||||
expect(parseModelId('ggml-org/model-Q4_K_M')).toMatchObject({
|
||||
quantization: 'Q4_K_M',
|
||||
sidecar: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user