diff --git a/studio/backend/tests/test_openai_auto_switch.py b/studio/backend/tests/test_openai_auto_switch.py index 954d4b2474..25bee8b98b 100644 --- a/studio/backend/tests/test_openai_auto_switch.py +++ b/studio/backend/tests/test_openai_auto_switch.py @@ -566,11 +566,8 @@ def test_idle_loop_deletes_saved_kv_when_unload_fails(monkeypatch, tmp_path): async def _drive(): task = asyncio.create_task(kw.idle_unload_loop(poll_seconds = 0.01)) - # A wall-clock deadline, not an iteration count: Windows timers round a - # 10 ms sleep up to the ~15.6 ms scheduler tick, on both this loop and the - # one under test, so a fixed count of short sleeps gave the unload far - # less real time there than the number suggests and the test failed for - # being slow rather than wrong. + # Wall clock, not an iteration count: Windows rounds a 10 ms sleep up to the + # ~15.6 ms tick on both loops, so a fixed count failed for being slow. deadline = time.monotonic() + 15.0 while time.monotonic() < deadline: await asyncio.sleep(0.01) diff --git a/studio/frontend/src/features/model-picker/api/migrate-model-overrides.ts b/studio/frontend/src/features/model-picker/api/migrate-model-overrides.ts index 6b26c424cc..71a22dcd2f 100644 --- a/studio/frontend/src/features/model-picker/api/migrate-model-overrides.ts +++ b/studio/frontend/src/features/model-picker/api/migrate-model-overrides.ts @@ -44,13 +44,10 @@ function markRan(): void { /** * A server key under the same identity this browser stores. * - * `app_settings` has no schema version and holds whatever id was current when the - * row was written, so an old install has keys like `Unsloth/Repo-GGUF:Q4_K_M` while - * this browser only produces the folded form. The backend resolves both to one - * model, so an exact lookup would report "not on the server" and let the backfill - * overwrite it. The split is quant-aware like the backend's: a repo id folds and a - * POSIX path deliberately does not, and an ordinary colon inside a filename, or a - * Windows drive letter, is not a separator at all. + * `app_settings` has no schema version, so an old install holds keys like + * `Unsloth/Repo-GGUF:Q4_K_M` that the backend resolves to the same model as this + * browser's folded form; an exact lookup would call it missing and let the backfill + * overwrite it. The quant-aware split folds repo ids and leaves POSIX paths alone. */ function normalizedOverrideKey(key: string): string { const split = splitQuantSuffix(key); 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 b4100295a0..93a2c2e63f 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 @@ -872,12 +872,9 @@ export function ModelConfigPage({ const effectiveAtBaseline = perModelConfigsEqual(effectiveConfig, baseline); const effectivePersistenceOnly = isActiveModel && effectiveAtBaseline && rememberChanged; - // 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. + // Judge what storage keeps: savePerModelConfig normalizes first, and the runtime's + // Speculative Decoding "auto" canonicalizes to null, so judging the raw object + // reported saved while the write dropped it, and mirrored an override nothing held. const normalizedRuntimeConfig = normalizePerModelConfig( effectiveRuntimeConfig, ); diff --git a/studio/frontend/src/features/model-picker/model-config/model-identity.ts b/studio/frontend/src/features/model-picker/model-config/model-identity.ts index 5848ba6921..ff2e510e82 100644 --- a/studio/frontend/src/features/model-picker/model-config/model-identity.ts +++ b/studio/frontend/src/features/model-picker/model-config/model-identity.ts @@ -69,8 +69,7 @@ export function ggufVariantFromStorageKey(key: string): string | null { } // Mirrors split_quant_suffix in studio/backend/utils/openai_auto_switch_settings.py. -// A quant label may carry a bits-per-weight modifier ("IQ4_XS-3.53bpw"), and the -// two backend label helpers disagree on whether to keep it, so both forms parse. +// The bpw modifier ("IQ4_XS-3.53bpw") is optional: the backend label helpers disagree. const BPW_SUFFIX = /-[0-9]+(?:\.[0-9]+)?bpw$/i; const KNOWN_QUANT = /^(UD-)?(MXFP[0-9]+(?:_[A-Z0-9]+)*|IQ[0-9]+_[A-Z]+(?:_[A-Z0-9]+)?|TQ[0-9]+_[0-9]+|Q[0-9]+_K_[A-Z]+|Q[0-9]+_[0-9]+|Q[0-9]+_K|BF16|F16|F32)$/i; @@ -79,9 +78,8 @@ const MAX_QUANT_SUFFIX_LEN = 64; /** * `[head, quant]` for a `head:QUANT` key, or null when the colon is not one. * - * The suffix has to look like a real quant, so an ordinary colon inside a POSIX - * filename is left alone ("/models/foo:bar.gguf" is one valid filename) and a - * Windows drive letter is never mistaken for a model id. + * The suffix must look like a real quant, so an ordinary colon in a POSIX filename + * ("/models/foo:bar.gguf") and a Windows drive letter are left alone. */ export function splitQuantSuffix(value: string): [string, string] | null { const separator = value.lastIndexOf(":"); @@ -99,9 +97,7 @@ export function splitQuantSuffix(value: string): [string, string] | null { ) { return [head, tail]; } - // A .gguf with no recognizable quant token is labelled by its stem, so keys - // like "/models/CustomModel.gguf:custommodel" exist. Requiring the head to be - // a .gguf keeps an ordinary colon out: "/models/foo:bar.gguf" splits to a head - // that is not one. + // A .gguf with no recognizable quant is labelled by its stem, so + // "/models/CustomModel.gguf:custommodel" exists; a non-.gguf head is a plain colon. return head.toLowerCase().endsWith(".gguf") ? [head, tail] : null; } 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 04ce96021c..268a7e068e 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 @@ -480,12 +480,9 @@ 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. + * A config in the exact shape storage keeps it in: the UI carries sentinels storage + * does not, notably Speculative Decoding "auto" which canonicalizes to null, so a + * config still being edited reads as non-default when it is not. */ export function normalizePerModelConfig(raw: unknown): PerModelConfig { return normalize(raw); diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index 616b1e79ff..3abb8e3925 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -697,8 +697,7 @@ def test_backfill_compares_server_keys_by_normalized_identity(): # Folded on both sides: the older `id::variant` local keys are not. assert "const known = new Set(Object.keys(existing).map(normalizedOverrideKey));" in src assert "if (known.has(key)) { continue; }" in src - # A variant never holds a colon, so the last one splits the key; the first would - # cut the drive letter off every Windows path id. + # A quant-aware split, so a Windows drive letter is not read as a separator. assert "const split = splitQuantSuffix(key);" in src # Repo ids fold and POSIX paths do not, which is what these do. assert "normalizeModelIdentity(" in src and "normalizeGgufVariantIdentity(" in src @@ -895,8 +894,7 @@ def test_backfill_splits_a_quant_suffix_the_way_the_backend_does(): ).read_text(encoding = "utf-8") assert "def split_quant_suffix(" in backend, "the rule this mirrors" assert "_BPW_SUFFIX" in backend and "bpw" in identity - # Both sides accept the same quant vocabulary. The regex itself lives with - # the loader that reads the filenames. + # Both sides accept the same quant vocabulary; the regex lives with the loader. quants = (WORKDIR / "studio" / "backend" / "core" / "inference" / "llama_cpp.py").read_text( encoding = "utf-8" )