Match both spellings when the monitor unloads a model

My earlier fix compared the store checkpoint only against the identifier
/status reports, which is the concrete load path. For a GGUF loaded through
auto-switch or from a non-active cache the store can hold the advertised repo
id instead, so the comparison failed and the store stayed pinned to a model
that had just been freed. Match the load path and the public alias, and keep
the external guard that prompted the narrower comparison.
This commit is contained in:
danielhanchen 2026-07-29 06:12:57 +00:00
commit dc2d160c64
2 changed files with 28 additions and 1 deletions

View file

@ -21,6 +21,7 @@ import { getInferenceStatus, unloadModel } from "@/features/chat/api/chat-api";
import { resolveInferenceCheckpointId } from "@/features/chat/lib/apply-inference-status-to-store";
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
import type { ApiMonitorEntry } from "@/features/chat/types/api";
import { isExternalModelId } from "@/features/chat/external-providers";
import { modelIdsMatch } from "@/features/hub/lib/model-identity";
import { useSettingsDialogStore } from "@/features/settings";
import { getApiBase, isTauri } from "@/lib/api-base";
@ -520,9 +521,17 @@ export function ApiMonitorPage(): ReactElement {
// external provider selected while a local model stays resident, and
// clearCheckpoint calls saveLastExternalCheckpoint(null), so clearing
// unconditionally would delete a selection this button never touched.
// Both spellings: status reports the concrete load path as the identifier
// while the store may hold the advertised repo id, so matching only the
// path leaves the store pinned to a model this button just freed.
const store = useChatRuntimeStore.getState();
const selected = store.params.checkpoint;
if (selected && modelIdsMatch(selected, checkpoint)) {
const unloadedAliases = [checkpoint, status.active_model];
if (
selected &&
!isExternalModelId(selected) &&
unloadedAliases.some((alias) => modelIdsMatch(selected, alias))
) {
store.clearCheckpoint();
}
setUnloadError(null);

View file

@ -1527,3 +1527,21 @@ def test_a_standalone_gguf_has_one_settings_identity_everywhere():
bare = route.index("\n target_id,\n")
labelled = route.index('f"{target_id}:{file_variant}"')
assert bare < labelled, "the bare path must be read before the filename label"
def test_monitor_unload_clears_only_the_model_it_freed():
"""Unload targets the resident local model from /status, but the store may
hold either spelling: status reports the concrete load path while the store
can hold the advertised repo id. Matching one spelling leaves the store
pinned to a model just freed; matching none of them, or matching an external
pick, deletes a selection this button never touched, because clearCheckpoint
also drops the persisted external checkpoint."""
page = " ".join(
_read("features/api-monitor/api-monitor-page.tsx").split()
)
assert "const unloadedAliases = [checkpoint, status.active_model];" in page
assert "!isExternalModelId(selected)" in page
assert (
"unloadedAliases.some((alias) => modelIdsMatch(selected, alias))" in page
)
assert "store.clearCheckpoint();" in page