From f39fd0e408f7c7cb8bff75198c32cdee15212dad Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 28 Jul 2026 13:16:39 +0000 Subject: [PATCH] Resolve the override key on save and match standalone gguf settings The remove branch already resolved the key a load would use, but the save branch wrote payload.model_id literally. The browser normalizes casing before storing, so a backfilled key and a later UI save left two entries for one model; with two equivalent keys present resolve_model_override_key finds no unique match, so any third casing resolved to no override at all and the model silently loaded with defaults. A standalone .gguf gets variants=() from the resolver, so variant is None and only the bare ids were tried. The picker keys the same file by the quant label it derives from the filename, which is never empty, so those settings lived under :LABEL and nothing reached them. Try the filename-derived key after the variant-qualified ones and before the bare ones, so older bare entries still work. --- studio/backend/routes/inference.py | 12 ++++++++++++ studio/backend/routes/settings.py | 8 +++++++- .../backend/tests/test_openai_auto_switch.py | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 4bd3774ff7..5e56880c3d 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -4291,10 +4291,22 @@ async def _maybe_auto_switch_model( # different configs), then the bare ids. Both the advertised # repo id and the concrete load path are tried: a local folder # or a non-active HF cache is configured against its path. + # A standalone .gguf needs no quant sub-selection, so the + # resolver reports variant=None for it. The picker still + # keys its config by the quant label it derives from the + # filename (LocalModelInfo.format_variant), which is never + # empty, so those settings live under ":LABEL" and no + # bare key would ever reach them. + file_variant = None + if not variant and target_id.lower().endswith(".gguf"): + from hub.utils.gguf import extract_quant_label + + file_variant = extract_quant_label(os.path.basename(target_id)) override = {} for override_key in ( f"{override_id}:{variant}" if variant else None, f"{target_id}:{variant}" if variant else None, + f"{target_id}:{file_variant}" if file_variant else None, override_id, target_id, ): diff --git a/studio/backend/routes/settings.py b/studio/backend/routes/settings.py index 052c643519..cc8844ad4d 100644 --- a/studio/backend/routes/settings.py +++ b/studio/backend/routes/settings.py @@ -411,8 +411,14 @@ def update_openai_auto_switch_override( target_id = resolve_model_override_key(payload.model_id) or payload.model_id set_model_override(target_id, llama_extra_args = [], max_seq_length = None) else: + # Save under the key a load would resolve to, for the same reason the + # removal branch does. The browser normalizes casing before storing, + # so saving the literal id leaves a second entry for one model, and + # two equivalent keys make every other casing ambiguous: the lookup + # then matches neither and the model silently loses its settings. + target_id = resolve_model_override_key(payload.model_id) or payload.model_id set_model_override( - payload.model_id, + target_id, llama_extra_args = extra_args, max_seq_length = payload.max_seq_length, custom_context_length = payload.custom_context_length, diff --git a/studio/backend/tests/test_openai_auto_switch.py b/studio/backend/tests/test_openai_auto_switch.py index bf4f41c818..55484e67d3 100644 --- a/studio/backend/tests/test_openai_auto_switch.py +++ b/studio/backend/tests/test_openai_auto_switch.py @@ -4748,6 +4748,25 @@ def test_removal_clears_the_entry_a_load_would_actually_resolve(monkeypatch): assert settings.get_model_override("unsloth/B-GGUF:Q4_K_M") == {} +def test_save_updates_the_existing_case_variant_instead_of_forking_it(monkeypatch): + # The backfill stores normalized (lowercase) keys while a later UI save carries + # the catalog's casing. Writing that literally leaves two keys for one model, + # and with two equivalent keys present any third casing resolves ambiguously, + # so the model silently loses every saved setting on the API path. + import routes.settings as settings_route + + _mock_override_store(monkeypatch) + settings.set_model_override("unsloth/b-gguf:q4_k_m", max_seq_length = 8192) + settings_route.update_openai_auto_switch_override( + settings_route.ModelOverridePayload( + model_id = "unsloth/B-GGUF:Q4_K_M", max_seq_length = 4096 + ), + "tester", + ) + assert list(settings.get_model_overrides()) == ["unsloth/b-gguf:q4_k_m"] + assert settings.get_model_override("Unsloth/B-GGUF:Q4_K_M")["max_seq_length"] == 4096 + + def test_removal_of_a_path_still_only_touches_the_exact_key(monkeypatch): import routes.settings as settings_route