Show the Hub the settings the model is really running with

Two ways the Hub's per-model settings page could offer a resident model's
saved or default values as if they were live, and then write them back over
the running config on Apply.

ModelConfigPage seeds its editable state from loadedConfig in a useState
initializer, so it reads that prop once per mounted instance. The sidebar
entry keys its instance on a signature of the live config; the Hub's keyed on
the model and quant only. Open the page before /api/inference/status has
hydrated, or while that same target is still loading, and loadedConfig flips
from null to the live config after mount with nothing to remount on: the
editor keeps the values it seeded from and Apply reloads the model with them.
Both hosts now mount under one shared key that includes the live config, so
the arrival of that config re-seeds the editor and a repeated poll of the same
values does not.

The live config itself comes out of the chat runtime store, and landing
straight on /hub is the one entry point where nothing has applied the status
yet: useChatModelRuntime has no mount sync and the chat page is a different
route. The Hub's own status effect pinned the checkpoint and stopped there, so
the resident check passed while kv cache, speculative decoding, tensor
parallel and every GPU placement field still held their defaults. It now
applies the whole status, the same call the chat runtime's refresh makes, and
holds off when a load owns the store or an external provider is selected so it
cannot fight either.
This commit is contained in:
danielhanchen 2026-07-29 00:18:38 +00:00
commit 542f70bc47
11 changed files with 589 additions and 65 deletions

View file

@ -35,7 +35,10 @@ export type {
GgufVariantDetail,
InferenceStatusResponse,
} from "./types/api";
export { resolveInferenceCheckpointId } from "./lib/apply-inference-status-to-store";
export {
applyActiveModelStatusToStore,
resolveInferenceCheckpointId,
} from "./lib/apply-inference-status-to-store";
export {
ChatSettingsPanel,
ParamSlider,

View file

@ -7,7 +7,11 @@
// place to work through every knob. This gives them a page and says plainly that
// what is saved here is what an API load uses, mirrored by ModelConfigPage.
import { ModelConfigPage, type ModelPickTarget } from "@/features/model-picker";
import {
ModelConfigPage,
type ModelPickTarget,
modelConfigInstanceKey,
} from "@/features/model-picker";
import type { PerModelConfig } from "@/features/model-picker";
import { cn } from "@/lib/utils";
import { ArrowLeft01Icon, Globe02Icon } from "@hugeicons/core-free-icons";
@ -122,7 +126,18 @@ export function HubModelSettingsView({
<div className="rounded-xl border border-border/60 bg-card px-4 py-4">
<ModelConfigPage
key={`${target.id}::${target.ggufVariant ?? ""}`}
// Keyed on the live config too, like the sidebar entry.
// ModelConfigPage reads loadedConfig once, in its useState
// initializer, so opening this page before /api/inference/status
// has hydrated (or while the target is still loading) would
// otherwise leave the editor on saved/default values for a model
// that is running with something else, and Apply would write them
// back over it.
key={modelConfigInstanceKey(
target.id,
target.ggufVariant,
loadedConfig,
)}
target={target}
onRun={onRun}
loadedConfig={loadedConfig}

View file

@ -3,6 +3,7 @@
import { usePlatformStore } from "@/config/env";
import {
applyActiveModelStatusToStore,
getInferenceStatus,
isExternalModelId,
listGgufVariants,
@ -72,6 +73,7 @@ import { useModelsSelection } from "./hooks/use-models-selection";
import { useHubInventory } from "./inventory";
import { LOCAL_MODEL_SOURCE } from "./inventory/constants";
import { settingsGgufVariantForRow } from "./inventory/settings-identity";
import { adoptResidentModelStatus } from "./lib/adopt-inference-status";
import {
CHANNEL_TO_SECTION,
type ChannelId,
@ -384,25 +386,40 @@ export function ModelsPage() {
let cancelled = false;
void getInferenceStatus()
.then((status) => {
if (cancelled || !status.active_model) return;
// The loadable identifier, as every other status reader records it: a GGUF
// from a non-active HF cache or straight off disk loads by path, while
// active_model is the clean public id (an HF snapshot's repo id, any other
// file's filename stem). Two files that share a stem collapse onto one id,
// so storing that would make the catalog row for one of them look loaded.
const checkpointId = resolveInferenceCheckpointId(status);
if (!checkpointId) return;
if (cancelled) return;
const store = useChatRuntimeStore.getState();
if (
!isExternalModelId(store.params.checkpoint) &&
(!modelIdsMatch(store.params.checkpoint, checkpointId) ||
!ggufVariantsMatch(
store.activeGgufVariant,
status.gguf_variant ?? null,
))
) {
store.setCheckpoint(checkpointId, status.gguf_variant ?? null);
}
adoptResidentModelStatus(
{
// The loadable identifier, as every other status reader records it: a
// GGUF from a non-active HF cache or straight off disk loads by path,
// while active_model is the clean public id (an HF snapshot's repo id,
// any other file's filename stem). Two files that share a stem collapse
// onto one id, so storing that would make the catalog row for one of
// them look loaded.
checkpointId: resolveInferenceCheckpointId(status),
ggufVariant: status.gguf_variant ?? null,
},
{
checkpoint: store.params.checkpoint,
checkpointIsExternal: isExternalModelId(store.params.checkpoint),
activeGgufVariant: store.activeGgufVariant,
modelLoading: store.modelLoading,
},
{
setCheckpoint: (checkpointId, ggufVariant) => {
store.setCheckpoint(checkpointId, ggufVariant);
},
// Landing here is the one entry point that has applied no status yet,
// so the settings page would read this model's live config off a store
// still holding defaults. Same call the chat runtime's refresh makes.
applyStatus: (previous) => {
applyActiveModelStatusToStore(status, {
previousCheckpoint: previous.checkpoint ?? undefined,
previousGgufVariant: previous.ggufVariant,
});
},
},
);
})
.catch(() => undefined);
return () => {

View file

@ -0,0 +1,92 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
// Adopting the resident model into the chat runtime store from the Hub.
//
// Landing straight on /hub (a reload, or a deep link after an OpenAI-compatible
// auto-switch loaded something else) is the one entry point where nothing has
// applied /api/inference/status yet: useChatModelRuntime has no mount sync and
// the chat page is a different route. Pinning only the checkpoint leaves every
// other field useActiveModelConfig reads at its default, so the Hub's settings
// page passes those defaults on as the resident model's live config and Apply
// reloads the model with them. Adoption therefore has to apply the whole status,
// exactly as the chat runtime's refresh does.
import { ggufVariantsMatch, modelIdsMatch } from "./model-identity.ts";
/** The parts of the chat runtime store adoption has to look at. */
export interface ResidentAdoptionState {
/** ``params.checkpoint``. */
checkpoint: string | null;
/** Whether that checkpoint names an external provider's model. */
checkpointIsExternal: boolean;
/** ``activeGgufVariant``. */
activeGgufVariant: string | null;
/** ``modelLoading``: a load this tab started still owns the store. */
modelLoading: boolean;
}
/** What ``/api/inference/status`` says is resident, already resolved. */
export interface ResidentStatusFacts {
/** ``resolveInferenceCheckpointId(status)``; null when nothing is loaded. */
checkpointId: string | null;
/** ``status.gguf_variant``. */
ggufVariant: string | null;
}
export interface ResidentAdoptionActions {
/** Re-pin ``params.checkpoint`` onto the resident model. */
setCheckpoint: (checkpointId: string, ggufVariant: string | null) => void;
/**
* Apply the rest of the status. Receives the store values from BEFORE
* ``setCheckpoint`` ran, which is what applyActiveModelStatusToStore needs to
* tell a hydration from steady state.
*/
applyStatus: (previous: {
checkpoint: string | null;
ggufVariant: string | null;
}) => void;
}
/**
* Adopt the resident model reported by ``/api/inference/status``.
*
* Returns whether anything was adopted. Never loads or unloads a model: it only
* mirrors what the server already has.
*/
export function adoptResidentModelStatus(
status: ResidentStatusFacts,
state: ResidentAdoptionState,
actions: ResidentAdoptionActions,
): boolean {
const { checkpointId } = status;
if (!checkpointId) {
return false;
}
// An external-provider selection has no local mirror, so stamping the resident
// GGUF's capabilities and launch settings onto it would describe a model the
// user is not talking to.
if (state.checkpointIsExternal) {
return false;
}
// A load this tab started applies its own status when it settles, and the load
// dialog owns the params meanwhile. Adopting underneath it would fight both.
if (state.modelLoading) {
return false;
}
const previous = {
checkpoint: state.checkpoint,
ggufVariant: state.activeGgufVariant,
};
const alreadyPinned =
modelIdsMatch(previous.checkpoint, checkpointId) &&
ggufVariantsMatch(previous.ggufVariant, status.ggufVariant);
if (!alreadyPinned) {
actions.setCheckpoint(checkpointId, status.ggufVariant);
}
// Unconditional, even when the checkpoint already matched: a persisted
// checkpoint rehydrates from localStorage on its own, with none of the fields
// that say how the model was actually launched.
actions.applyStatus(previous);
return true;
}

View file

@ -2,7 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { useMemo } from "react";
import { gpuFieldsSignature } from "../model-config/apply-per-model-config";
import { modelConfigInstanceKey } from "../model-config/config-signature";
import { isOllamaLinkPath } from "../model-config/model-identity";
import type { PerModelConfig } from "../model-config/per-model-config";
import { ModelConfigPage } from "./model-config-page";
@ -29,29 +29,6 @@ function leafName(id: string): string {
return separator >= 0 ? trimmed.slice(separator + 1) : trimmed;
}
function hashString(value: string): number {
let hash = 5381;
for (let i = 0; i < value.length; i += 1) {
hash = (Math.imul(hash, 33) ^ value.charCodeAt(i)) >>> 0;
}
return hash;
}
function configSignature(config: PerModelConfig): string {
return [
config.customContextLength ?? "",
config.maxSeqLength ?? "",
config.kvCacheDtype ?? "",
config.speculativeType ?? "",
config.specDraftNMax ?? "",
config.tensorParallel ? "1" : "0",
config.chatTemplateOverride == null
? ""
: `${config.chatTemplateOverride.length}:${hashString(config.chatTemplateOverride)}`,
gpuFieldsSignature(config),
].join("|");
}
export function SidebarModelConfig({
modelId,
ggufVariant,
@ -84,7 +61,7 @@ export function SidebarModelConfig({
return (
<ModelConfigPage
key={`${modelId}::${ggufVariant ?? ""}::${configSignature(loadedConfig)}`}
key={modelConfigInstanceKey(modelId, ggufVariant, loadedConfig)}
target={target}
onRun={onReload}
loadedConfig={loadedConfig}

View file

@ -36,6 +36,7 @@ export type {
ModelOption,
ModelSelectorChangeMeta,
} from "./components/model-selector";
export { modelConfigInstanceKey } from "./model-config/config-signature";
export {
applyModelLoadConfigToRuntime,
applyPerModelConfigToRuntime,

View file

@ -10,12 +10,17 @@ import {
reconcilePersistedGpuIds,
useChatRuntimeStore,
} from "@/features/chat";
// Lives in its own module so hosts that only need the signature (the editor keys)
// can import it without pulling the chat runtime store in with it.
import { gpuFieldsSignature } from "./config-signature";
import {
DEFAULT_PER_MODEL_CONFIG,
type PerModelConfig,
normalizeMaxSeqLength,
} from "./per-model-config";
export { gpuFieldsSignature };
function cleanTemplate(value: string | null | undefined): string | null {
return value?.trim() ? value : null;
}
@ -108,20 +113,6 @@ export function perModelConfigsEqual(
);
}
// Serialize the per-model GPU knobs with the same "absent == default"
// coalescing the store applies: mode auto/absent, gpuLayers Auto (< 0) /
// absent, nCpuMoe 0 / absent, and the GPU pick (null / absent = all GPUs).
export function gpuFieldsSignature(config: PerModelConfig): string {
return [
config.gpuMemoryMode ?? "auto",
config.gpuLayers == null || config.gpuLayers < 0 ? -1 : config.gpuLayers,
config.nCpuMoe ?? 0,
config.selectedGpuIds == null
? "all"
: [...config.selectedGpuIds].sort((a, b) => a - b).join(","),
].join("|");
}
function gpuFieldsEqual(a: PerModelConfig, b: PerModelConfig): boolean {
return gpuFieldsSignature(a) === gpuFieldsSignature(b);
}

View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
// Identity of one ModelConfigPage editor instance.
//
// ModelConfigPage seeds its editable state from `loadedConfig` in a useState
// initializer, so it reads that prop exactly once per mounted instance. A host
// that opens the page before /api/inference/status has hydrated, or while the
// target is still loading, gets `loadedConfig` null first and the live config a
// moment later; without the config in the React key the same instance survives
// that flip and keeps showing the saved/default values for a model that is
// running with something else, which Apply then writes back over it.
import type { PerModelConfig } from "./per-model-config";
// Serialize the per-model GPU knobs with the same "absent == default"
// coalescing the store applies: mode auto/absent, gpuLayers Auto (< 0) /
// absent, nCpuMoe 0 / absent, and the GPU pick (null / absent = all GPUs).
export function gpuFieldsSignature(config: PerModelConfig): string {
return [
config.gpuMemoryMode ?? "auto",
config.gpuLayers == null || config.gpuLayers < 0 ? -1 : config.gpuLayers,
config.nCpuMoe ?? 0,
config.selectedGpuIds == null
? "all"
: [...config.selectedGpuIds].sort((a, b) => a - b).join(","),
].join("|");
}
function hashString(value: string): number {
let hash = 5381;
for (let i = 0; i < value.length; i += 1) {
hash = (Math.imul(hash, 33) ^ value.charCodeAt(i)) >>> 0;
}
return hash;
}
/**
* Signature of the live config an editor was seeded from.
*
* `null` (no live config, because the model is not resident or status has not
* answered yet) is deliberately its own value, distinct from every real config:
* the arrival of the live config is exactly the transition that has to remount.
*/
export function loadedConfigSignature(
config: PerModelConfig | null | undefined,
): string {
if (!config) {
return "none";
}
return [
config.customContextLength ?? "",
config.maxSeqLength ?? "",
config.kvCacheDtype ?? "",
config.speculativeType ?? "",
config.specDraftNMax ?? "",
config.tensorParallel ? "1" : "0",
config.chatTemplateOverride == null
? ""
: `${config.chatTemplateOverride.length}:${hashString(config.chatTemplateOverride)}`,
gpuFieldsSignature(config),
].join("|");
}
/**
* React key for one ModelConfigPage instance. Every host mounts it under this so
* they agree on when the editor is re-seeded: on a different model, a different
* quant, or a change in the live config it is meant to be showing.
*/
export function modelConfigInstanceKey(
modelId: string,
ggufVariant: string | null | undefined,
loadedConfig: PerModelConfig | null | undefined,
): string {
return `${modelId}::${ggufVariant ?? ""}::${loadedConfigSignature(loadedConfig)}`;
}

View file

@ -0,0 +1,151 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import assert from "node:assert/strict";
import test from "node:test";
import {
type ResidentAdoptionState,
adoptResidentModelStatus,
} from "../src/features/hub/lib/adopt-inference-status.ts";
const RESIDENT = {
checkpointId: "unsloth/Qwen3-8B-GGUF",
ggufVariant: "Q4_K_M",
};
function emptyStore(
overrides: Partial<ResidentAdoptionState> = {},
): ResidentAdoptionState {
return {
checkpoint: null,
checkpointIsExternal: false,
activeGgufVariant: null,
modelLoading: false,
...overrides,
};
}
function spies() {
const calls: string[] = [];
const previouslySeen: { checkpoint: string | null; ggufVariant: string | null }[] =
[];
return {
calls,
previouslySeen,
actions: {
setCheckpoint(checkpointId: string, ggufVariant: string | null) {
calls.push(`setCheckpoint:${checkpointId}:${ggufVariant ?? ""}`);
},
applyStatus(previous: {
checkpoint: string | null;
ggufVariant: string | null;
}) {
calls.push("applyStatus");
previouslySeen.push(previous);
},
},
};
}
test("landing on the Hub applies the whole status, not just the checkpoint", () => {
// Nothing else on /hub hydrates the runtime store: useChatModelRuntime has no
// mount sync and the chat page is a different route. Pinning only the
// checkpoint leaves every field useActiveModelConfig reads at its default, so
// the settings page offers those defaults as the resident model's live config.
const { calls, actions } = spies();
const adopted = adoptResidentModelStatus(RESIDENT, emptyStore(), actions);
assert.equal(adopted, true);
assert.deepEqual(calls, [
"setCheckpoint:unsloth/Qwen3-8B-GGUF:Q4_K_M",
"applyStatus",
]);
});
test("a checkpoint that already matches is still hydrated", () => {
// A reload rehydrates params.checkpoint from localStorage on its own, with
// none of the fields that say how the model was actually launched.
const { calls, actions } = spies();
adoptResidentModelStatus(
RESIDENT,
emptyStore({
checkpoint: "unsloth/Qwen3-8B-GGUF",
activeGgufVariant: "Q4_K_M",
}),
actions,
);
assert.deepEqual(calls, ["applyStatus"]);
});
test("an API auto-switch under the tab re-pins the model and the quant", () => {
for (const stale of [
{ checkpoint: "unsloth/Llama-3.1-8B-GGUF", activeGgufVariant: "Q4_K_M" },
{ checkpoint: "unsloth/Qwen3-8B-GGUF", activeGgufVariant: "Q8_0" },
]) {
const { calls, actions } = spies();
adoptResidentModelStatus(RESIDENT, emptyStore(stale), actions);
assert.deepEqual(calls, [
"setCheckpoint:unsloth/Qwen3-8B-GGUF:Q4_K_M",
"applyStatus",
]);
}
});
test("the status applied is the one from before the checkpoint moved", () => {
// applyActiveModelStatusToStore tells a hydration from steady state by the
// previous checkpoint/quant, so it has to be read before setCheckpoint syncs
// them, or a variant-only switch reads as steady state and keeps the old
// quant's baselines.
const { previouslySeen, actions } = spies();
adoptResidentModelStatus(
RESIDENT,
emptyStore({
checkpoint: "unsloth/Qwen3-8B-GGUF",
activeGgufVariant: "Q8_0",
}),
actions,
);
assert.deepEqual(previouslySeen, [
{ checkpoint: "unsloth/Qwen3-8B-GGUF", ggufVariant: "Q8_0" },
]);
});
test("nothing is adopted when no model is loaded", () => {
const { calls, actions } = spies();
const adopted = adoptResidentModelStatus(
{ checkpointId: null, ggufVariant: null },
emptyStore(),
actions,
);
assert.equal(adopted, false);
assert.deepEqual(calls, []);
});
test("an external-provider selection is left alone", () => {
// It has no local mirror, so stamping the resident GGUF's launch settings onto
// it would describe a model the user is not talking to.
const { calls, actions } = spies();
const adopted = adoptResidentModelStatus(
RESIDENT,
emptyStore({
checkpoint: "openai/gpt-5",
checkpointIsExternal: true,
}),
actions,
);
assert.equal(adopted, false);
assert.deepEqual(calls, []);
});
test("a load in flight is not fought", () => {
// The load applies its own status when it settles, and the load dialog owns
// the params meanwhile.
const { calls, actions } = spies();
const adopted = adoptResidentModelStatus(
RESIDENT,
emptyStore({ modelLoading: true }),
actions,
);
assert.equal(adopted, false);
assert.deepEqual(calls, []);
});

View file

@ -0,0 +1,129 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import assert from "node:assert/strict";
import test from "node:test";
import { modelConfigInstanceKey } from "../src/features/model-picker/model-config/config-signature.ts";
import type { PerModelConfig } from "../src/features/model-picker/model-config/per-model-config.ts";
const MODEL = "unsloth/Qwen3-8B-GGUF";
const VARIANT = "Q4_K_M";
// What the model is actually running with, as useActiveModelConfig reports it.
const LIVE: PerModelConfig = {
customContextLength: 16384,
maxSeqLength: null,
kvCacheDtype: "q8_0",
speculativeType: "ngram",
specDraftNMax: 6,
tensorParallel: true,
chatTemplateOverride: null,
gpuMemoryMode: "manual",
gpuLayers: 24,
nCpuMoe: 3,
selectedGpuIds: [0, 1],
};
// What ModelConfigPage would fall back to before the live config lands.
const SAVED: PerModelConfig = {
customContextLength: null,
maxSeqLength: null,
kvCacheDtype: null,
speculativeType: "auto",
specDraftNMax: null,
tensorParallel: false,
chatTemplateOverride: null,
gpuMemoryMode: "auto",
gpuLayers: -1,
nCpuMoe: 0,
selectedGpuIds: null,
};
/**
* ModelConfigPage reads `loadedConfig` in a useState initializer, so it seeds its
* editable state once per MOUNTED instance; React keeps that instance for as long
* as the key is unchanged. This is that rule, and nothing else.
*/
function renderEditor(
previous: { key: string; editing: PerModelConfig } | null,
key: string,
loadedConfig: PerModelConfig | null,
): { key: string; editing: PerModelConfig } {
if (previous && previous.key === key) {
return previous;
}
return { key, editing: loadedConfig ?? SAVED };
}
test("the settings editor re-seeds when the live config arrives after mount", () => {
// Opened before /api/inference/status answered, or while the target was still
// loading: loadedConfig is null on the first render and live on the next.
let editor = renderEditor(
null,
modelConfigInstanceKey(MODEL, VARIANT, null),
null,
);
assert.deepEqual(editor.editing, SAVED);
editor = renderEditor(
editor,
modelConfigInstanceKey(MODEL, VARIANT, LIVE),
LIVE,
);
// Without the live config in the key the editor would still hold SAVED, and
// Apply would reload the model with it over what it is running with.
assert.deepEqual(editor.editing, LIVE);
});
test("a repeated status poll keeps the same editor instance", () => {
const first = renderEditor(
null,
modelConfigInstanceKey(MODEL, VARIANT, LIVE),
LIVE,
);
// A structurally equal config from the next poll must not remount and throw
// away whatever the user has typed since.
const again = renderEditor(
first,
modelConfigInstanceKey(MODEL, VARIANT, { ...LIVE }),
LIVE,
);
assert.equal(again, first);
});
test("every mirrored setting moves the instance key", () => {
const base = modelConfigInstanceKey(MODEL, VARIANT, LIVE);
const changes: PerModelConfig[] = [
{ ...LIVE, customContextLength: 8192 },
{ ...LIVE, maxSeqLength: 4096 },
{ ...LIVE, kvCacheDtype: "f16" },
{ ...LIVE, speculativeType: "off" },
{ ...LIVE, specDraftNMax: 4 },
{ ...LIVE, tensorParallel: false },
{ ...LIVE, chatTemplateOverride: "{{ bos_token }}" },
{ ...LIVE, gpuMemoryMode: "auto" },
{ ...LIVE, gpuLayers: 20 },
{ ...LIVE, nCpuMoe: 0 },
{ ...LIVE, selectedGpuIds: [0] },
];
for (const changed of changes) {
assert.notEqual(modelConfigInstanceKey(MODEL, VARIANT, changed), base);
}
// The GPU pick is a set, not an order.
assert.equal(
modelConfigInstanceKey(MODEL, VARIANT, { ...LIVE, selectedGpuIds: [1, 0] }),
base,
);
});
test("the model and its quant still key the editor", () => {
const base = modelConfigInstanceKey(MODEL, VARIANT, LIVE);
assert.notEqual(modelConfigInstanceKey("unsloth/Other-GGUF", VARIANT, LIVE), base);
assert.notEqual(modelConfigInstanceKey(MODEL, "Q8_0", LIVE), base);
// A loose .gguf carries no quant; null and undefined are the same absence.
assert.equal(
modelConfigInstanceKey(MODEL, null, LIVE),
modelConfigInstanceKey(MODEL, undefined, LIVE),
);
});

View file

@ -196,10 +196,21 @@ def test_active_model_config_round_trips_gpu_fields():
"features/hub/catalog/sampling-settings-dialog.tsx",
):
assert "useActiveModelConfig(" in _read(rel), rel
signature = _read("features/model-picker/components/sidebar-model-config.tsx")
assert "gpuFieldsSignature(config)" in signature
shared = _read("features/model-picker/model-config/apply-per-model-config.ts")
# The GPU knobs are part of the editor's instance key, so a reload that lands
# on different placement re-seeds the editor instead of leaving it on the old
# values. Shared by every host that mounts ModelConfigPage.
shared = _read("features/model-picker/model-config/config-signature.ts")
assert "export function gpuFieldsSignature" in shared
assert "gpuFieldsSignature(config)," in shared
assert "export function modelConfigInstanceKey" in shared
for rel in (
"features/model-picker/components/sidebar-model-config.tsx",
"features/hub/catalog/hub-model-settings-view.tsx",
):
assert "modelConfigInstanceKey(" in _read(rel), rel
# apply-per-model-config re-exports it, so its own callers are unchanged.
reexport = _read("features/model-picker/model-config/apply-per-model-config.ts")
assert "export { gpuFieldsSignature };" in reexport
def test_gpu_picker_round_trips_requested_pool_not_fitted_subset():
@ -1012,8 +1023,7 @@ def test_the_hub_settings_page_matches_a_resident_path_loaded_model():
# The loadable identifier, as every other status reader records it. active_model
# is the clean public id, and two files sharing a filename collapse onto one, so
# storing it would let the wrong catalog row look loaded.
assert "const checkpointId = resolveInferenceCheckpointId(status);" in hub
assert "store.setCheckpoint(checkpointId, status.gguf_variant ?? null);" in hub
assert "checkpointId: resolveInferenceCheckpointId(status)," in hub
assert "setCheckpoint(status.active_model" not in hub
chat = " ".join(_read("features/chat/lib/apply-inference-status-to-store.ts").split())
assert "return status.model_identifier ?? status.active_model;" in chat, "the rule this mirrors"
@ -1030,6 +1040,68 @@ def test_the_hub_settings_page_matches_a_resident_path_loaded_model():
assert "def public_model_id(" in backend, "the rule this mirrors"
def test_the_hub_hydrates_the_live_settings_before_it_offers_them():
"""The Hub builds activeModelConfig out of the chat runtime store, and landing
straight on /hub is the one entry point where nothing has applied
/api/inference/status yet: useChatModelRuntime has no mount sync and the chat
page is a different route. Pinning only the checkpoint left every other field
at its default, so the settings page passed those defaults on as the resident
model's live config and Apply reloaded the model with them."""
hub = " ".join(_read("features/hub/hub-page.tsx").split())
assert "adoptResidentModelStatus(" in hub
assert "applyActiveModelStatusToStore(status, {" in hub
assert "previousCheckpoint: previous.checkpoint ?? undefined," in hub
assert "previousGgufVariant: previous.ggufVariant," in hub
assert "modelLoading: store.modelLoading," in hub
adopt = " ".join(_read("features/hub/lib/adopt-inference-status.ts").split())
# Unconditional: a persisted checkpoint rehydrates from localStorage on its
# own, carrying none of the fields that say how the model was launched.
assert "actions.applyStatus(previous); return true;" in adopt
# Never fight the load that owns the store, and never describe an external
# provider's model with the resident GGUF's launch settings.
assert "if (state.checkpointIsExternal) { return false; }" in adopt
assert "if (state.modelLoading) { return false; }" in adopt
# Same call the chat runtime's own refresh makes, which is the rule this mirrors.
runtime = " ".join(_read("features/chat/hooks/use-chat-model-runtime.ts").split())
assert "applyActiveModelStatusToStore(statusRes, {" in runtime
def test_the_hub_settings_editor_reseeds_when_the_live_config_lands():
"""ModelConfigPage reads loadedConfig in a useState initializer, so it seeds
once per mounted instance. Opening the Hub's settings page before
/api/inference/status has hydrated (or while the target is still loading)
flips loadedConfig from null to the live config after mount, and without the
config in the React key the editor kept the saved/default values for a model
running with something else, which Apply then wrote back over it."""
view = " ".join(_read("features/hub/catalog/hub-model-settings-view.tsx").split())
assert "key={modelConfigInstanceKey( target.id, target.ggufVariant, loadedConfig, )}" in view
# Same key the sidebar entry uses; that parity is the point.
sidebar = " ".join(_read("features/model-picker/components/sidebar-model-config.tsx").split())
assert "key={modelConfigInstanceKey(modelId, ggufVariant, loadedConfig)}" in sidebar
signature = " ".join(
_read("features/model-picker/model-config/config-signature.ts").split()
)
# "No live config yet" has to be its own value: that transition is exactly
# the one that must remount.
assert 'if (!config) { return "none"; }' in signature
for field in (
"config.customContextLength",
"config.maxSeqLength",
"config.kvCacheDtype",
"config.speculativeType",
"config.specDraftNMax",
"config.tensorParallel",
"config.chatTemplateOverride",
):
assert field in signature, field
page = " ".join(_read("features/model-picker/components/model-config-page.tsx").split())
assert "const [initial] = useState(resolveInitial);" in page, "the rule this mirrors"
def test_a_standalone_gguf_has_one_settings_key():
"""The inventory labels a single scanned .gguf from its filename, so the Hub row
menu keyed its settings to `<path>:Q4_K_M` while the Chat picker, the detail