feat(studio): presets include load settings (#7347) (#7352)

* feat(studio): save load settings in chat presets

Presets previously stored only sampling params (temperature, top_p, etc.).
Extend them with an optional loadConfig blob that captures context length,
KV cache dtype, speculative decoding, and GPU layer knobs from the current
runtime when saving.

- Apply loadConfig when switching presets or hydrating on startup
- Show a short summary under the preset controls
- Prompt to reload when a model is already loaded

Fixes #7347

* fix(studio): persist preset loadConfig and capture GGUF context

Add ChatPresetLoadConfig to the chat settings API schema so presets with
load settings no longer 400 on save. Capture effective GGUF context from
ggufContextLength when customContextLength is cleared after auto-mode load.

* fix(studio): address Codex review on preset load settings

Coalesce default maxSeqLength/speculative/gpu knobs when capturing presets,
no-op apply for legacy presets without loadConfig, preserve GPU pin on apply,
and stop replaying stale loadConfig during settings hydration.

* Remove unused getOrderedPresets import

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
Souravrajvi0 2026-07-24 14:53:43 +05:30 committed by GitHub
commit 434fac6ffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 467 additions and 15 deletions

View file

@ -0,0 +1,70 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Contract coverage for preset load settings (#7347)."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def _read(relative: str) -> str:
path = ROOT / relative
if not path.exists():
path = ROOT / "unsloth_repo" / relative
return path.read_text(encoding = "utf-8")
def test_preset_interface_includes_load_config():
policy = _read("studio/frontend/src/features/chat/presets/preset-policy.ts")
assert "loadConfig?: PresetLoadConfig" in policy
def test_preset_save_captures_load_config():
sheet = _read("studio/frontend/src/features/chat/chat-settings-sheet.tsx")
assert "capturePresetLoadConfig()" in sheet
assert "applyPresetLoadConfig" in sheet
def test_preset_apply_restores_load_config():
sheet = _read("studio/frontend/src/features/chat/chat-settings-sheet.tsx")
assert "if (p.loadConfig)" in sheet
assert "applyPresetLoadConfig(p.loadConfig)" in sheet
def test_persisted_preset_serializes_load_config():
storage = _read("studio/frontend/src/features/chat/utils/chat-settings-storage.ts")
assert "normalizePresetLoadConfig(item.loadConfig)" in storage
api = _read("studio/frontend/src/features/chat/api/chat-settings-api.ts")
assert "loadConfig?: Record<string, unknown>" in api
def test_capture_reads_gguf_loaded_context():
source = _read("studio/frontend/src/features/chat/presets/preset-load-config.ts")
assert "store.ggufContextLength" in source
assert "effectiveContextLength" in source
def test_apply_skips_missing_load_config():
source = _read("studio/frontend/src/features/chat/presets/preset-load-config.ts")
assert "if (config == null)" in source
assert "selectedGpuIds: store.selectedGpuIds" in source
sheet = _read("studio/frontend/src/features/chat/chat-settings-sheet.tsx")
assert "if (p.loadConfig)" in sheet
def test_hydration_does_not_replay_preset_load_config():
store = _read("studio/frontend/src/features/chat/stores/chat-runtime-store.ts")
assert "applyPresetLoadConfig(activeDefinition.loadConfig)" not in store
def test_capture_coalesces_default_load_knobs():
source = _read("studio/frontend/src/features/chat/presets/preset-load-config.ts")
assert "coalesceDefaultLoadKnobs" in source
assert "DEFAULT_MAX_SEQ_LENGTH" in source
def test_backend_chat_preset_accepts_load_config():
routes = _read("studio/backend/routes/chat_history.py")
assert "class ChatPresetLoadConfig" in routes
assert "loadConfig: Optional[ChatPresetLoadConfig]" in routes