Skip to content

ui: don't reload a loading model on select, prefer it in pre-selection - #31326

Merged
ggerganov merged 3 commits into
masterfrom
allozaur/fix/model-selector-loading-state
Nov 2, 2026
Merged

ggerganov merged 3 commits into
masterfrom
allozaur/fix/model-selector-loading-state

Conversation

@allozaur

@allozaur allozaur commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Picking a model that is already loading no longer starts a second load, and a chat without a model of its own pre-selects the model that is loading. Automatic selection also stops picking models hidden from the selector.

Demo

Demo

Recorded at 3a8c7ad; click the preview for the full video.

Additional information

The demo doesn't cover hidden models.

Cause
  • The selector only checked that a model wasn't loaded before loading it. status.load() skips loads this client already started, but not a model the server reports as loading (started from another tab, the manager, or before a reload), so picking it sent another load request.
  • Pre-selection went from the last reply's model straight to the first loaded model, so a model being loaded lost to one already up.
  • Automatic selection only skipped models flagged ui: false by the server, not the ones hidden in the manager, so it could land on a model the selector doesn't list.
What changed
  • modelsStore.isModelLoading() covers both the server's loading status and a load this client started. handleSelect in use-models-selector only loads a model that is neither loaded nor loading.
  • ensureFirstModelSelected() (new chat) tries a loading model right after the last reply's model, before loaded models, favorites and the first available model.
  • getLoadingModelToPreselect() lets a chat without a model in its replies swap its selection for a loading model, unless the selection is loaded or loading itself. A new chat and selectModelFromLastAssistantResponse() (opening a chat) use it, and the chat form's fallback tries a loading model before a loaded one.
  • getVisibleModels() also skips models hidden in the manager, and the chat form's fallback uses it instead of every model. A conversation whose replies name a hidden model still selects it.
Testing
  • Type-check, lint and unit tests pass.
  • Recorded the demo against a router: picking a loading model again sent no second load request, and after a reload the loading model was pre-selected over the loaded one, with the script checking the server still reported it as loading at both points.
Demo script
test('demo', async ({ demo, page }) => {
	const LOADING = 'InternScience/Sage-A1.5-Q8_0-GGUF:Q8_0';
	let loadRequests = 0;
	page.on('request', (request) => {
		if (request.method() === 'POST' && request.url().endsWith('/models/load')) loadRequests++;
	});

	const status = async () => {
		const response = await page.request.get('http://localhost:9931/models');
		const { data } = await response.json();

		return data.find((m: { id: string }) => m.id === LOADING)?.status?.value;
	};
	const trigger = (name: RegExp) => page.getByRole('button', { name }).first();
	const pickSage = async () => {
		await demo.click(trigger(/Groot|Sage/), 300);
		await demo.type(page.getByPlaceholder('Search models...'), 'Sage', 200);
		await demo.click(page.getByRole('option', { name: /Sage/ }).first(), 300);
	};

	await expect(trigger(/Groot/)).toBeVisible({ timeout: 30_000 });
	await demo.caption('A new chat starts on the model that is already loaded', 1500);

	// pick an unloaded model, then pick it again while it loads
	await pickSage();
	await pickSage();
	expect(await status()).toBe('loading');
	const requests = loadRequests;

	// a reload drops the pick: the loading model wins over the loaded one
	await page.reload();
	await expect(trigger(/Sage/)).toBeVisible({ timeout: 30_000 });
	expect(await status()).toBe('loading');
	await demo.caption('After a reload, the loading model is pre-selected over the loaded one', 2200);
	await demo.caption(`Picking it twice while it loaded sent ${requests} load request`, 2200);
});

Requirements

@ahoplock

Copy link
Copy Markdown

This is unrelated to this change but the "first loaded model" logic will select a model even when it's hidden from the UI.
image

@allozaur

Copy link
Copy Markdown
Contributor Author

This is unrelated to this change but the "first loaded model" logic will select a model even when it's hidden from the UI. image

could be addressed in this PR :) thanks for the heads up!

Assisted-by: Claude (Opus 5.5 Medium)
@allozaur allozaur added the video demo PR features a video demo recorded with the UI demo pipeline label Oct 29, 2026
@allozaur
allozaur marked this pull request as ready for review October 29, 2026 12:35
@allozaur
allozaur requested a review from a team as a code owner October 29, 2026 12:35
Copilot AI balanced review requested due to automatic review settings October 29, 2026 12:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@ServeurpersoCom ServeurpersoCom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small thing: isModelLoading() also returns true while this client is unloading a model, since unload() sets the same loadingStates flag, so a model being unloaded can be pre-selected as the loading one. Checking this._status.isOperationInProgress(modelId) && !this.isModelLoaded(modelId) would keep only loads, as the status stays LOADED until UNLOADED.

Assisted-by: Claude (Opus 5.5 Medium)
@allozaur allozaur added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Nov 2, 2026
@ggerganov
ggerganov merged commit b12b405 into master Nov 2, 2026
7 checks passed
@ahoplock

ahoplock commented Nov 2, 2026

Copy link
Copy Markdown

could be addressed in this PR :) thanks for the heads up!

Thanks, works as expected now!

edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Nov 6, 2026
ggml-org#31326)

* ui: don't reload a loading model on select, prefer it in pre-selection

Assisted-by: Claude (Opus 5.5 Medium)

* ui: skip hidden models in automatic model selection

Assisted-by: Claude (Opus 5.5 Medium)

* ui: don't count a model being unloaded as loading

Assisted-by: Claude (Opus 5.5 Medium)
(cherry picked from commit b12b405)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. server/ui video demo PR features a video demo recorded with the UI demo pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants