Commit a6b83d426 for llama.cpp

commit a6b83d426f90d65be8f0c44145437cf42d10e442
Author: Aleksander Grygier <aleksander.grygier@gmail.com>
Date:   Thu Oct 22 07:11:45 2026 +0200

    ui : key draft sidecar badges by model instead of repo (#30919)

    Two quants of one repo pulled each other's draft: the badge read every
    --model-draft arg of the repo, so each row showed both drafts.

    Assisted-by: DeepSeek-V4.1-Flash-1017

diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts
index 38d734f4e..3008eb521 100644
--- a/tools/ui/src/lib/stores/models/index.svelte.ts
+++ b/tools/ui/src/lib/stores/models/index.svelte.ts
@@ -578,7 +578,6 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 					const displayNameSource =
 						details?.name && details.name.trim().length > 0 ? details.name : item.id;
 					const modelId = details?.model || item.id;
-					const repo = repoOf(modelId);

 					return {
 						aliases: item.aliases ?? [],
@@ -594,7 +593,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 								: undefined),
 						description: details?.description,
 						details: details?.details,
-						draftSidecars: this._status.getDraftSidecars(repo),
+						draftSidecars: this._status.getDraftSidecars(modelId),
 						id: item.id,
 						meta: item.meta ?? null,
 						modalities: this.props.buildArchitectureModalities(item.architecture),
diff --git a/tools/ui/src/lib/stores/models/status.svelte.ts b/tools/ui/src/lib/stores/models/status.svelte.ts
index 4c00adbd7..a2e3b2752 100644
--- a/tools/ui/src/lib/stores/models/status.svelte.ts
+++ b/tools/ui/src/lib/stores/models/status.svelte.ts
@@ -97,10 +97,11 @@ export class ModelStatusManager {
 	/** Sidecar kinds each in-flight or paused download pulls with its model. */
 	private downloadSidecars = new SvelteMap<string, ModelSidecar[]>();
 	/**
-	 * Draft sidecars the registered models pull with them, keyed by the repo of the model
-	 * that pulls them: the router records them in the model's `--model-draft` args.
+	 * Draft sidecars the registered models pull with them, keyed by the model that pulls
+	 * them: the router records them in the model's `--model-draft` args, and each quant
+	 * of a repo can pull a different draft.
 	 */
-	private draftSidecarsByRepo = $derived.by(() => {
+	private draftSidecarsByModel = $derived.by(() => {
 		const result = new SvelteMap<string, ModelSidecarBadge[]>();

 		for (const m of this.host.routerModels) {
@@ -108,8 +109,6 @@ export class ModelStatusManager {

 			if (!args) continue;

-			const baseRepo = repoOf(m.id);
-
 			for (let i = 0; i < args.length - 1; i++) {
 				if (args[i] !== CLI_FLAGS.MODEL_DRAFT && args[i] !== CLI_FLAGS.MODEL_DRAFT_SHORT) {
 					continue;
@@ -128,7 +127,7 @@ export class ModelStatusManager {
 					quant: meta.quant,
 					repo: parsed.repo
 				};
-				const kinds = result.get(baseRepo);
+				const kinds = result.get(m.id);

 				if (kinds) {
 					if (
@@ -139,7 +138,7 @@ export class ModelStatusManager {
 						kinds.push(badge);
 					}
 				} else {
-					result.set(baseRepo, [badge]);
+					result.set(m.id, [badge]);
 				}
 			}
 		}
@@ -404,17 +403,19 @@ export class ModelStatusManager {
 	}

 	/**
-	 * Draft sidecars a repo pulls with its model, e.g. `mtp` at `Q8_0`: the
-	 * router records loaded models' sidecars in their `--model-draft` args, and
-	 * lists the sidecars each cached model has on disk.
+	 * Draft sidecars a model pulls with it, e.g. `mtp` at `Q8_0`: the router
+	 * records each model's sidecars in its `--model-draft` args, and lists the
+	 * sidecars each cached repo has on disk.
 	 */
-	getDraftSidecars(repoId: string): ModelSidecarBadge[] {
-		// one badge per kind: the args know the quant, the listing carries kinds only,
-		// and a draft still coming down is pending rather than on disk
-		const merged = [...(this.draftSidecarsByRepo.get(repoId) ?? [])];
+	getDraftSidecars(modelId: string): ModelSidecarBadge[] {
+		const repo = repoOf(modelId);
+		// one badge per kind: the args know the quant of this model's draft, the
+		// listing carries repo-wide kinds only, and a draft still coming down is
+		// pending rather than on disk
+		const merged = [...(this.draftSidecarsByModel.get(modelId) ?? [])];

 		for (const source of [this.listedSidecarsByRepo, this.pendingSidecarsByRepo]) {
-			for (const badge of source.get(repoId) ?? []) {
+			for (const badge of source.get(repo) ?? []) {
 				if (!merged.some((b) => b.kind === badge.kind && b.repo === badge.repo)) {
 					merged.push(badge);
 				}
diff --git a/tools/ui/tests/client/models-sidecar-badges.svelte.test.ts b/tools/ui/tests/client/models-sidecar-badges.svelte.test.ts
index a5754abc1..03a402b74 100644
--- a/tools/ui/tests/client/models-sidecar-badges.svelte.test.ts
+++ b/tools/ui/tests/client/models-sidecar-badges.svelte.test.ts
@@ -1,6 +1,7 @@
-// Guards the draft badges a repo row shows: a loaded model records its draft in
-// the `--model-draft` args, which know the file's quant, and the listing reports
-// the kinds of every cached model, which do not. The row shows each kind once.
+// Guards the draft badges a model row shows: a registered model records its draft
+// in its `--model-draft` args, which know the file's quant, and the listing reports
+// the kinds every cached model of the repo has on disk, which do not. The row shows
+// each kind once, and two quants of one repo keep their own drafts apart.

 import { ModelAuxSidecar, ModelDraftSidecar, ServerModelStatus } from '$lib/enums';
 import { modelsStore } from '$lib/stores';
@@ -9,6 +10,11 @@ import { beforeEach, describe, expect, it } from 'vitest';

 const REPO = 'ggml-org/Qwen3.8-Flash-Next-GGUF';

+/** Draft file of a cached model, as a preset records it in `--model-draft`. */
+function draftPath(quant: string): string {
+	return `/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-${quant}.gguf`;
+}
+
 /** A router entry; `args` stand in for the args of a loaded instance. */
 function entry(
 	id: string,
@@ -36,18 +42,28 @@ describe('draft sidecar badges', () => {
 	it('shows one badge for a draft the args and the listing both report', () => {
 		modelsStore.routerModels = [
 			entry(
-				`${REPO}:IQ4_NL`,
-				[],
-				[
-					'--model-draft',
-					`/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-Q8_0.gguf`
-				]
-			),
-			entry(`${REPO}:Q8_0`, [ModelAuxSidecar.MMPROJ, ModelDraftSidecar.MTP])
+				`${REPO}:Q8_0`,
+				[ModelAuxSidecar.MMPROJ, ModelDraftSidecar.MTP],
+				['--model-draft', draftPath('Q8_0')]
+			)
 		];

 		// the args badge wins: it knows the quant the listing does not
-		expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
+			{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
+		]);
+	});
+
+	it('gives each quant of a repo the draft it pulls itself', () => {
+		modelsStore.routerModels = [
+			entry(`${REPO}:IQ4_NL`, [], ['--model-draft', draftPath('Q4_0')]),
+			entry(`${REPO}:Q8_0`, [], ['--model-draft', draftPath('Q8_0')])
+		];
+
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:IQ4_NL`)).toEqual([
+			{ kind: ModelDraftSidecar.MTP, quant: 'Q4_0', repo: REPO }
+		]);
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
 			{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
 		]);
 	});
@@ -58,7 +74,7 @@ describe('draft sidecar badges', () => {
 			entry(`${REPO}:Q8_0`, [ModelDraftSidecar.MTP])
 		];

-		expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:IQ4_NL`)).toEqual([
 			{ kind: ModelDraftSidecar.MTP, quant: null, repo: REPO }
 		]);
 	});
@@ -74,7 +90,7 @@ describe('draft sidecar badges', () => {
 		];

 		// the kinds are not on disk yet, so the row reads them as in flight
-		expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:Q4_0`)).toEqual([
 			{ kind: ModelDraftSidecar.MTP, pending: true, quant: null, repo: REPO }
 		]);
 	});
@@ -82,17 +98,10 @@ describe('draft sidecar badges', () => {
 	it('keeps the on-disk badge of a kind a download also pulls', () => {
 		modelsStore.routerModels = [
 			entry(`${REPO}:IQ4_NL`, [ModelDraftSidecar.MTP], [], ServerModelStatus.INCOMPLETE),
-			entry(
-				`${REPO}:Q8_0`,
-				[],
-				[
-					'--model-draft',
-					`/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-Q8_0.gguf`
-				]
-			)
+			entry(`${REPO}:Q8_0`, [], ['--model-draft', draftPath('Q8_0')])
 		];

-		expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
+		expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
 			{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
 		]);
 	});