webui: Improve model parsing logic + add unit tests (#20749)
* add tests for model id parser * add test case having activated params * add structured tests for model id parser * add ToDo * feat: Improve model parsing logic + tests * chore: update webui build output --------- Co-authored-by: bluemoehre <bluemoehre@gmx.de>
This commit is contained in:
committed by
GitHub
parent
b486c17b3e
commit
512bba6ee0
@@ -28,6 +28,11 @@
|
||||
|
||||
let parsed = $derived(ModelsService.parseModelId(modelId));
|
||||
let resolvedShowRaw = $derived(showRaw ?? (config().showRawModelNames as boolean) ?? false);
|
||||
let displayName = $derived(
|
||||
aliases && aliases.length > 0 ? aliases[0] : (parsed.modelName ?? modelId)
|
||||
);
|
||||
let remainingAliases = $derived(aliases && aliases.length > 1 ? aliases.slice(1) : []);
|
||||
let allTags = $derived([...(parsed.tags ?? []), ...(tags ?? [])]);
|
||||
</script>
|
||||
|
||||
{#if resolvedShowRaw}
|
||||
@@ -35,7 +40,7 @@
|
||||
{:else}
|
||||
<span class="flex min-w-0 flex-wrap items-center gap-1 {className}">
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{#if showOrgName && parsed.orgName}{parsed.orgName}/{/if}{parsed.modelName ?? modelId}
|
||||
{#if showOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName}
|
||||
</span>
|
||||
|
||||
{#if parsed.params}
|
||||
@@ -50,14 +55,14 @@
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if aliases && aliases.length > 0}
|
||||
{#each aliases as alias (alias)}
|
||||
{#if remainingAliases.length > 0}
|
||||
{#each remainingAliases as alias (alias)}
|
||||
<span class={badgeClass}>{alias}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if tags && tags.length > 0}
|
||||
{#each tags as tag (tag)}
|
||||
{#if allTags.length > 0}
|
||||
{#each allTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -11,10 +11,16 @@ export const MODEL_ID_SEGMENT_SEPARATOR = '-';
|
||||
export const MODEL_ID_QUANTIZATION_SEPARATOR = ':';
|
||||
|
||||
/**
|
||||
* Matches a trailing ALL-CAPS format segment, e.g. `GGUF`, `BF16`, `Q4_K_M`.
|
||||
* Must be at least 2 uppercase letters, optionally followed by uppercase letters or digits.
|
||||
* Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`.
|
||||
* Case-insensitive to handle both uppercase and lowercase inputs.
|
||||
*/
|
||||
export const MODEL_FORMAT_SEGMENT_RE = /^[A-Z]{2,}[A-Z0-9]*$/;
|
||||
export const MODEL_QUANTIZATION_SEGMENT_RE =
|
||||
/^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i;
|
||||
|
||||
/**
|
||||
* Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`.
|
||||
*/
|
||||
export const MODEL_CUSTOM_QUANTIZATION_PREFIX_RE = /^UD$/i;
|
||||
|
||||
/**
|
||||
* Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
|
||||
@@ -22,7 +28,12 @@ export const MODEL_FORMAT_SEGMENT_RE = /^[A-Z]{2,}[A-Z0-9]*$/;
|
||||
export const MODEL_PARAMS_RE = /^\d+(\.\d+)?[BbMmKkTt]$/;
|
||||
|
||||
/**
|
||||
* Matches an activated-parameter-count segment, e.g. `A10B`, `A2.4b`.
|
||||
* The leading `A` distinguishes it from a regular params segment.
|
||||
* Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
|
||||
* The leading `A`/`a` distinguishes it from a regular params segment.
|
||||
*/
|
||||
export const MODEL_ACTIVATED_PARAMS_RE = /^A\d+(\.\d+)?[BbMmKkTt]$/;
|
||||
export const MODEL_ACTIVATED_PARAMS_RE = /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/;
|
||||
|
||||
/**
|
||||
* Container format segments to exclude from tags (every model uses these).
|
||||
*/
|
||||
export const MODEL_IGNORED_SEGMENTS = new Set(['GGUF', 'GGML']);
|
||||
|
||||
@@ -2,9 +2,11 @@ import { ServerModelStatus } from '$lib/enums';
|
||||
import { apiFetch, apiPost } from '$lib/utils';
|
||||
import type { ParsedModelId } from '$lib/types/models';
|
||||
import {
|
||||
MODEL_FORMAT_SEGMENT_RE,
|
||||
MODEL_QUANTIZATION_SEGMENT_RE,
|
||||
MODEL_CUSTOM_QUANTIZATION_PREFIX_RE,
|
||||
MODEL_PARAMS_RE,
|
||||
MODEL_ACTIVATED_PARAMS_RE,
|
||||
MODEL_IGNORED_SEGMENTS,
|
||||
MODEL_ID_NOT_FOUND,
|
||||
MODEL_ID_ORG_SEPARATOR,
|
||||
MODEL_ID_SEGMENT_SEPARATOR,
|
||||
@@ -119,8 +121,9 @@ export class ModelsService {
|
||||
/**
|
||||
* Parse a model ID string into its structured components.
|
||||
*
|
||||
* Handles the convention:
|
||||
* `<org>/<ModelName>-<Parameters>(-<ActivatedParameters>)-<Format>:<QuantizationType>`
|
||||
* Handles conventions like:
|
||||
* `<org>/<ModelName>-<Parameters>(-<ActivatedParameters>)(-<Tags>)(-<Quantization>):<Quantization>`
|
||||
* `<ModelName>.<Quantization>` (dot-separated quantization, e.g. `model.Q4_K_M`)
|
||||
*
|
||||
* @param modelId - Raw model identifier string
|
||||
* @returns Structured {@link ParsedModelId} with all detected fields
|
||||
@@ -132,11 +135,11 @@ export class ModelsService {
|
||||
modelName: null,
|
||||
params: null,
|
||||
activatedParams: null,
|
||||
format: null,
|
||||
quantization: null,
|
||||
tags: []
|
||||
};
|
||||
|
||||
// 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`)
|
||||
const colonIdx = modelId.indexOf(MODEL_ID_QUANTIZATION_SEPARATOR);
|
||||
let modelPath: string;
|
||||
|
||||
@@ -147,6 +150,7 @@ export class ModelsService {
|
||||
modelPath = modelId;
|
||||
}
|
||||
|
||||
// 2. Extract org name (e.g. `org/model` -> org = "org")
|
||||
const slashIdx = modelPath.indexOf(MODEL_ID_ORG_SEPARATOR);
|
||||
let modelStr: string;
|
||||
|
||||
@@ -157,37 +161,66 @@ export class ModelsService {
|
||||
modelStr = modelPath;
|
||||
}
|
||||
|
||||
const segments = modelStr.split(MODEL_ID_SEGMENT_SEPARATOR);
|
||||
// 3. Handle dot-separated quantization (e.g. `model-name.Q4_K_M`)
|
||||
const dotIdx = modelStr.lastIndexOf('.');
|
||||
|
||||
if (segments.length > 0 && MODEL_FORMAT_SEGMENT_RE.test(segments[segments.length - 1])) {
|
||||
result.format = segments.pop()!;
|
||||
if (dotIdx !== MODEL_ID_NOT_FOUND && !result.quantization) {
|
||||
const afterDot = modelStr.slice(dotIdx + 1);
|
||||
|
||||
if (MODEL_QUANTIZATION_SEGMENT_RE.test(afterDot)) {
|
||||
result.quantization = afterDot;
|
||||
modelStr = modelStr.slice(0, dotIdx);
|
||||
}
|
||||
}
|
||||
|
||||
const paramsRe = MODEL_PARAMS_RE;
|
||||
const activatedParamsRe = MODEL_ACTIVATED_PARAMS_RE;
|
||||
const segments = modelStr.split(MODEL_ID_SEGMENT_SEPARATOR);
|
||||
|
||||
// 4. Detect trailing quantization from dash-separated segments
|
||||
// Handle UD-prefixed quantization (e.g. `UD-Q8_K_XL`) and
|
||||
// standalone quantization (e.g. `Q4_K_M`, `BF16`, `F16`, `MXFP4`)
|
||||
if (!result.quantization && segments.length > 1) {
|
||||
const last = segments[segments.length - 1];
|
||||
const secondLast = segments.length > 2 ? segments[segments.length - 2] : null;
|
||||
|
||||
if (MODEL_QUANTIZATION_SEGMENT_RE.test(last)) {
|
||||
if (secondLast && MODEL_CUSTOM_QUANTIZATION_PREFIX_RE.test(secondLast)) {
|
||||
result.quantization = `${secondLast}-${last}`;
|
||||
segments.splice(segments.length - 2, 2);
|
||||
} else {
|
||||
result.quantization = last;
|
||||
segments.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Find params and activated params
|
||||
let paramsIdx = MODEL_ID_NOT_FOUND;
|
||||
let activatedParamsIdx = MODEL_ID_NOT_FOUND;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const seg = segments[i];
|
||||
if (paramsIdx === -1 && paramsRe.test(seg)) {
|
||||
|
||||
if (paramsIdx === MODEL_ID_NOT_FOUND && MODEL_PARAMS_RE.test(seg)) {
|
||||
paramsIdx = i;
|
||||
result.params = seg.toUpperCase();
|
||||
} else if (activatedParamsRe.test(seg)) {
|
||||
} else if (paramsIdx !== MODEL_ID_NOT_FOUND && MODEL_ACTIVATED_PARAMS_RE.test(seg)) {
|
||||
activatedParamsIdx = i;
|
||||
result.activatedParams = seg.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Model name = segments before params; tags = remaining segments after params
|
||||
const pivotIdx = paramsIdx !== MODEL_ID_NOT_FOUND ? paramsIdx : segments.length;
|
||||
|
||||
result.modelName = segments.slice(0, pivotIdx).join(MODEL_ID_SEGMENT_SEPARATOR) || null;
|
||||
|
||||
if (paramsIdx !== MODEL_ID_NOT_FOUND) {
|
||||
result.tags = segments
|
||||
.slice(paramsIdx + 1)
|
||||
.filter((_, relIdx) => paramsIdx + 1 + relIdx !== activatedParamsIdx);
|
||||
result.tags = segments.slice(paramsIdx + 1).filter((_, relIdx) => {
|
||||
const absIdx = paramsIdx + 1 + relIdx;
|
||||
if (absIdx === activatedParamsIdx) return false;
|
||||
|
||||
return !MODEL_IGNORED_SEGMENTS.has(segments[absIdx].toUpperCase());
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface ParsedModelId {
|
||||
modelName: string | null;
|
||||
params: string | null;
|
||||
activatedParams: string | null;
|
||||
format: string | null;
|
||||
quantization: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user