diff --git a/studio/frontend/src/features/model-picker/components/model-config-page.tsx b/studio/frontend/src/features/model-picker/components/model-config-page.tsx index d290e516bb..b4100295a0 100644 --- a/studio/frontend/src/features/model-picker/components/model-config-page.tsx +++ b/studio/frontend/src/features/model-picker/components/model-config-page.tsx @@ -53,6 +53,7 @@ import { floorMaxSeqLength, isDefaultConfig, normalizeMaxSeqLength, + normalizePerModelConfig, resolveInitialConfig, savePerModelConfig, } from "../model-config/per-model-config"; @@ -871,14 +872,23 @@ export function ModelConfigPage({ const effectiveAtBaseline = perModelConfigsEqual(effectiveConfig, baseline); const effectivePersistenceOnly = isActiveModel && effectiveAtBaseline && rememberChanged; - const defaultConfig = isDefaultConfig(effectiveRuntimeConfig); + // Storage's own shape, because savePerModelConfig judges the normalized + // object: the runtime hands this page Speculative Decoding "auto", which + // canonicalizes to null, so the raw one looked non-default. Remember then + // reported saved while the local write had dropped the entry as default, and + // the mirror below sent the server a "auto" override the browser did not + // have, which is exactly the disagreement that mirror must not create. + const normalizedRuntimeConfig = normalizePerModelConfig( + effectiveRuntimeConfig, + ); + const defaultConfig = isDefaultConfig(normalizedRuntimeConfig); let saveFailed = false; const evicted: { modelId: string; ggufVariant: string | null }[] = []; if (remember) { saveFailed = !savePerModelConfig( configId, target.ggufVariant, - effectiveRuntimeConfig, + normalizedRuntimeConfig, evicted, ); } else { @@ -896,7 +906,7 @@ export function ModelConfigPage({ syncModelOverride( configId, target.ggufVariant, - remember ? effectiveRuntimeConfig : null, + remember ? normalizedRuntimeConfig : null, ); } // Saving can push the local map over budget and drop other models, whose server diff --git a/studio/frontend/src/features/model-picker/model-config/per-model-config.ts b/studio/frontend/src/features/model-picker/model-config/per-model-config.ts index 911b79e121..04ce96021c 100644 --- a/studio/frontend/src/features/model-picker/model-config/per-model-config.ts +++ b/studio/frontend/src/features/model-picker/model-config/per-model-config.ts @@ -479,6 +479,18 @@ function normalizeV1(partial: RawConfig): PerModelConfig { }; } +/** + * A config in the exact shape storage keeps it in. + * + * Callers that decide anything from a config a user is still editing have to run + * it through this first: the UI carries sentinels storage does not, notably + * Speculative Decoding "auto", which canonicalizes to null. Judging the raw + * object calls a default config non-default. + */ +export function normalizePerModelConfig(raw: unknown): PerModelConfig { + return normalize(raw); +} + function normalize(raw: unknown): PerModelConfig { if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return normalizeV1({}); diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index 255949cb92..c6bb7c71ff 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -912,3 +912,28 @@ def test_the_detail_card_also_gates_ollama_out_of_the_api_promise(): assert hub.count("LOCAL_MODEL_SOURCE.OLLAMA") == 2 assert "selectedModel.localSource !== LOCAL_MODEL_SOURCE.OLLAMA" in hub assert "row.source !== LOCAL_MODEL_SOURCE.OLLAMA" in hub + + +def test_the_settings_page_judges_the_config_storage_actually_keeps(): + """savePerModelConfig normalizes before deciding, and the runtime hands this + page Speculative Decoding "auto", which canonicalizes to null. Judging the raw + object called a default config non-default, so Remember reported saved while + the local write had dropped the entry, and the mirror sent the server an + "auto" override the browser did not have.""" + src = " ".join( + _read("features/model-picker/components/model-config-page.tsx").split() + ) + assert ( + "const normalizedRuntimeConfig = normalizePerModelConfig( effectiveRuntimeConfig, );" + in src + ) + assert "const defaultConfig = isDefaultConfig(normalizedRuntimeConfig);" in src + # The same object goes to storage and to the server, or they disagree again. + assert "target.ggufVariant, normalizedRuntimeConfig, evicted," in src + assert "remember ? normalizedRuntimeConfig : null," in src + assert "isDefaultConfig(effectiveRuntimeConfig)" not in src + + store = " ".join(_read("features/model-picker/model-config/per-model-config.ts").split()) + assert "export function normalizePerModelConfig(" in store + assert "const normalized = normalize(config);" in store + assert "if (isDefaultConfig(normalized)) {" in store, "the rule this mirrors"