From 60203ed16ca0e2a774d262316097635114167de4 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 28 Jul 2026 18:56:12 +0000 Subject: [PATCH] Judge the config storage actually keeps, not the one on screen savePerModelConfig normalizes before deciding whether a config is default, and the runtime hands the settings page Speculative Decoding "auto", which canonicalizes to null. The page judged the raw object instead, so a model sitting at defaults looked non-default: turning on "Remember for this model" reported saved while the local write had dropped the entry, reopening showed it as not remembered, and the mirror sent the server a speculative_type "auto" override the browser did not have. That disagreement between the two is the one thing the mirror is written to avoid. The page now normalizes once and uses that object for the default check, the local write and the server mirror alike. Driving the real module under node: the raw object reads as non-default, storage stores nothing, and only the normalized reading agrees with storage. --- .../components/model-config-page.tsx | 16 +++++++++--- .../model-config/per-model-config.ts | 12 +++++++++ tests/studio/test_model_picker_contracts.py | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) 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"