mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-06 17:57:44 +02:00
ui : address review on model id parsing
Use lowercase values for the sidecar enums so the value doubles as the filename token, derive the sidecar regexes from the enum values, and rename the MODEL_ID regex keys to the _REGEX suffix used by the rest of the constants files. Replace the tools capability magic string with ModelCapability.TOOL_USE. Assisted-by: pi
This commit is contained in:
@@ -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)
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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<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>;
|
||||
/** 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<string>(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-<name>.gguf`, `mmproj-<name>.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 `-<type>` 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
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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: -<type> 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
|
||||
|
||||
Reference in New Issue
Block a user