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