From 6c52697ad09682e1f342aa546eeb40ceeedd72df Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Tue, 19 May 2026 19:03:00 +0100 Subject: [PATCH] Studio: persist chat toggles and preserve custom sampling (#5587) * fix: persist chat toggles and preserve custom sampling * fix: keep Qwen reload sampling aligned with Think state * fix: avoid persisting Qwen reasoning defaults * studio: keep Kimi search off when thinking defaults on * fix: avoid persisting Kimi-enforced chat toggles --- .../frontend/src/features/chat/chat-page.tsx | 35 +++++++++++-- .../chat/hooks/use-chat-model-runtime.ts | 45 +++++++++++++--- .../src/features/chat/shared-composer.tsx | 6 +-- .../chat/stores/chat-runtime-store.ts | 52 +++++++++++++++---- .../src/features/chat/utils/qwen-params.ts | 5 +- 5 files changed, 115 insertions(+), 28 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 9744b0b017..81a8c8818e 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -67,7 +67,12 @@ import { RegisterCompareHandle, SharedComposer, } from "./shared-composer"; -import { useChatRuntimeStore } from "./stores/chat-runtime-store"; +import { + CHAT_CODE_TOOLS_ENABLED_KEY, + CHAT_TOOLS_ENABLED_KEY, + loadOptionalBool, + useChatRuntimeStore, +} from "./stores/chat-runtime-store"; import { useExternalProvidersStore } from "./stores/external-providers-store"; import { buildChatTourSteps } from "./tour"; import type { ChatView, MessageRecord } from "./types"; @@ -740,6 +745,13 @@ export function ChatPage(): ReactElement { supportsBuiltinWebSearch && (provider?.providerType === "anthropic" || provider?.providerType === "openai"); + const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY); + const storedCodeToolsEnabled = loadOptionalBool(CHAT_CODE_TOOLS_ENABLED_KEY); + const nextToolsEnabled = supportsBuiltinWebSearch + ? isKimi + ? false + : (storedToolsEnabled ?? searchOnByDefault) + : false; useChatRuntimeStore.setState({ supportsReasoning: reasoningCaps.supportsReasoning, reasoningAlwaysOn: reasoningCaps.reasoningAlwaysOn, @@ -765,8 +777,10 @@ export function ChatPage(): ReactElement { supportsTools: false, supportsBuiltinWebSearch, supportsBuiltinCodeExecution, - toolsEnabled: searchOnByDefault, - codeToolsEnabled: false, + toolsEnabled: nextToolsEnabled, + codeToolsEnabled: supportsBuiltinCodeExecution + ? (storedCodeToolsEnabled ?? false) + : false, }); }, [externalProviders, inferenceParams.checkpoint]); const canCompare = useMemo(() => { @@ -940,6 +954,15 @@ export function ChatPage(): ReactElement { supportsBuiltinWebSearch && (selectedProvider?.providerType === "anthropic" || selectedProvider?.providerType === "openai"); + const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY); + const storedCodeToolsEnabled = loadOptionalBool( + CHAT_CODE_TOOLS_ENABLED_KEY, + ); + const nextToolsEnabled = supportsBuiltinWebSearch + ? isKimi + ? false + : (storedToolsEnabled ?? searchOnByDefault) + : false; useChatRuntimeStore.setState({ activeGgufVariant: null, ggufContextLength: null, @@ -969,8 +992,10 @@ export function ChatPage(): ReactElement { supportsTools: false, supportsBuiltinWebSearch, supportsBuiltinCodeExecution, - toolsEnabled: searchOnByDefault, - codeToolsEnabled: false, + toolsEnabled: nextToolsEnabled, + codeToolsEnabled: supportsBuiltinCodeExecution + ? (storedCodeToolsEnabled ?? false) + : false, ...(stillOnOpenRouterFree ? {} : { lastOpenRouterChosenModel: null }), }); return; diff --git a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts index 3f1060edf7..a02c83dbba 100644 --- a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts +++ b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts @@ -24,6 +24,8 @@ import { } from "../api/chat-api"; import { formatEta, formatRate } from "../utils/format-transfer"; import { + CHAT_REASONING_ENABLED_KEY, + loadOptionalBool, type ReasoningEffort, useChatRuntimeStore, } from "../stores/chat-runtime-store"; @@ -273,6 +275,10 @@ export function useChatModelRuntime() { } // Restore reasoning/tools support flags and context length + const hydratingExistingModel = + selectedCheckpoint !== statusRes.active_model || + useChatRuntimeStore.getState().activeGgufVariant !== + (statusRes.gguf_variant ?? null); const supportsReasoning = statusRes.supports_reasoning ?? false; const reasoningAlwaysOn = statusRes.reasoning_always_on ?? false; const reasoningStyle = statusRes.reasoning_style ?? "enable_thinking"; @@ -282,6 +288,9 @@ export function useChatModelRuntime() { : (["low", "medium", "high"] as const); const supportsPreserveThinking = statusRes.supports_preserve_thinking ?? false; const supportsTools = statusRes.supports_tools ?? false; + const storedReasoningEnabled = loadOptionalBool( + CHAT_REASONING_ENABLED_KEY, + ); const currentGgufContextLength = statusRes.is_gguf ? (statusRes.context_length ?? null) : null; @@ -363,7 +372,11 @@ export function useChatModelRuntime() { }); // Set reasoning default for Qwen3.5/3.6 small models - if (supportsReasoning) { + if ( + supportsReasoning && + hydratingExistingModel && + storedReasoningEnabled === null + ) { let reasoningDefault = true; const mid = statusRes.active_model.toLowerCase(); if (mid.includes("qwen3.5") || mid.includes("qwen3.6")) { @@ -372,7 +385,7 @@ export function useChatModelRuntime() { reasoningDefault = false; } } - useChatRuntimeStore.getState().setReasoningEnabled(reasoningDefault); + useChatRuntimeStore.setState({ reasoningEnabled: reasoningDefault }); } } else if (!statusRes.active_model && !isExternalSelectionActive) { useChatRuntimeStore.setState({ @@ -466,6 +479,9 @@ export function useChatModelRuntime() { const previousCheckpoint = currentCheckpoint; const previousVariant = useChatRuntimeStore.getState().activeGgufVariant ?? null; + const reloadingSameModel = + previousCheckpoint === modelId && + (ggufVariant ?? null) === (previousVariant ?? null); const previousModel = previousCheckpoint ? models.find((entry) => entry.id === previousCheckpoint) : undefined; @@ -651,6 +667,8 @@ export function useChatModelRuntime() { const keepCustomCtx = null; const reasoningAlwaysOn = loadResponse.reasoning_always_on ?? false; const reasoningStyle = loadResponse.reasoning_style ?? "enable_thinking"; + const supportsReasoning = loadResponse.supports_reasoning ?? false; + const supportsTools = loadResponse.supports_tools ?? false; const reasoningEffortLevels = reasoningStyle === "reasoning_effort" ? (["low", "medium", "high"] as const) @@ -660,23 +678,34 @@ export function useChatModelRuntime() { existingReasoningEffort, ); const ggufMaxContextLength = reportedMaxCtx; + const nextReasoningEnabled = reasoningAlwaysOn + ? true + : reloadingSameModel && supportsReasoning + ? stateBeforeUnload.reasoningEnabled + : reasoningDefault; useChatRuntimeStore.setState({ ggufContextLength: nativeCtx, ggufMaxContextLength, ggufNativeContextLength: reportedNativeCtx, modelRequiresTrustRemoteCode: loadResponse.requires_trust_remote_code ?? false, - supportsReasoning: loadResponse.supports_reasoning ?? false, + supportsReasoning, reasoningAlwaysOn, - reasoningEnabled: reasoningAlwaysOn ? true : reasoningDefault, + reasoningEnabled: nextReasoningEnabled, reasoningStyle, supportsReasoningOff: reasoningStyle !== "reasoning_effort", reasoningEffortLevels, reasoningEffort: clampedReasoningEffort, supportsPreserveThinking: loadResponse.supports_preserve_thinking ?? false, - supportsTools: loadResponse.supports_tools ?? false, - toolsEnabled: loadResponse.supports_tools ?? false, - codeToolsEnabled: loadResponse.supports_tools ?? false, + supportsTools, + toolsEnabled: + reloadingSameModel && supportsTools + ? stateBeforeUnload.toolsEnabled + : supportsTools, + codeToolsEnabled: + reloadingSameModel && supportsTools + ? stateBeforeUnload.codeToolsEnabled + : supportsTools, kvCacheDtype: loadedKv, loadedKvCacheDtype: loadedKv, speculativeType: loadedSpec, @@ -700,7 +729,7 @@ export function useChatModelRuntime() { const mid = modelId.toLowerCase(); const needsPresencePenalty = mid.includes("qwen3.5") || mid.includes("qwen3.6"); - const p = reasoningDefault + const p = nextReasoningEnabled ? { temperature: 0.6, topP: 0.95, diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index 11a73ee486..016260238c 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -944,7 +944,7 @@ export function SharedComposer({ // Mutual exclusion: turning thinking on for a // Kimi model forces the web_search builtin off. if (isKimiExternal && toolsEnabled) { - setToolsEnabled(false); + setToolsEnabled(false, { persist: false }); } }} > @@ -973,7 +973,7 @@ export function SharedComposer({ // requires thinking off, so turning thinking on flips // the Search pill off (and vice versa). if (isKimiExternal && next && toolsEnabled) { - setToolsEnabled(false); + setToolsEnabled(false, { persist: false }); } }} className={cn( @@ -1041,7 +1041,7 @@ export function SharedComposer({ // back on when Search goes off — mutual exclusion that // mirrors what the backend enforces. if (isKimiExternal) { - setReasoningEnabled(!next); + setReasoningEnabled(!next, { persist: false }); applyQwenThinkingParams(!next); } }} diff --git a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts index e55a25d08d..457f39ac9e 100644 --- a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts +++ b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts @@ -22,8 +22,11 @@ const HF_TOKEN_KEY = "unsloth_hf_token"; const INFERENCE_PARAMS_KEY = "unsloth_chat_inference_params"; const CHAT_ACTIVE_PRESET_KEY = "unsloth_chat_active_preset"; const CHAT_ACTIVE_PRESET_SOURCE_KEY = "unsloth_chat_active_preset_source"; +export const CHAT_REASONING_ENABLED_KEY = "unsloth_chat_reasoning_enabled"; const REASONING_EFFORT_KEY = "unsloth_reasoning_effort"; const PRESERVE_THINKING_KEY = "unsloth_preserve_thinking"; +export const CHAT_TOOLS_ENABLED_KEY = "unsloth_chat_tools_enabled"; +export const CHAT_CODE_TOOLS_ENABLED_KEY = "unsloth_chat_code_tools_enabled"; export type ReasoningStyle = "enable_thinking" | "reasoning_effort"; export type ReasoningEffort = @@ -62,13 +65,18 @@ function canUseStorage(): boolean { } function loadBool(key: string, fallback: boolean): boolean { - if (!canUseStorage()) return fallback; + const raw = loadOptionalBool(key); + return raw ?? fallback; +} + +export function loadOptionalBool(key: string): boolean | null { + if (!canUseStorage()) return null; try { const raw = localStorage.getItem(key); - if (raw === null) return fallback; + if (raw === null) return null; return raw === "true"; } catch { - return fallback; + return null; } } @@ -294,12 +302,18 @@ type ChatRuntimeStore = { setActiveThreadId: (threadId: string | null) => void; setSettingsPanelOpen: (open: boolean) => void; clearCheckpoint: () => void; - setReasoningEnabled: (enabled: boolean) => void; + setReasoningEnabled: ( + enabled: boolean, + options?: { persist?: boolean }, + ) => void; setLastOpenRouterChosenModel: (chosen: string | null) => void; setReasoningStyle: (style: ReasoningStyle) => void; setReasoningEffort: (effort: ReasoningEffort) => void; setPreserveThinking: (value: boolean) => void; - setToolsEnabled: (enabled: boolean) => void; + setToolsEnabled: ( + enabled: boolean, + options?: { persist?: boolean }, + ) => void; setCodeToolsEnabled: (enabled: boolean) => void; setToolStatus: (status: string | null) => void; setGeneratingStatus: (status: string | null) => void; @@ -333,7 +347,7 @@ export const useChatRuntimeStore = create((set) => ({ modelRequiresTrustRemoteCode: false, supportsReasoning: false, reasoningAlwaysOn: false, - reasoningEnabled: true, + reasoningEnabled: loadBool(CHAT_REASONING_ENABLED_KEY, true), reasoningStyle: "enable_thinking", reasoningEffort: loadReasoningEffort("medium"), supportsReasoningOff: false, @@ -344,8 +358,8 @@ export const useChatRuntimeStore = create((set) => ({ supportsTools: false, supportsBuiltinWebSearch: false, supportsBuiltinCodeExecution: false, - toolsEnabled: false, - codeToolsEnabled: false, + toolsEnabled: loadBool(CHAT_TOOLS_ENABLED_KEY, false), + codeToolsEnabled: loadBool(CHAT_CODE_TOOLS_ENABLED_KEY, false), toolStatus: null, generatingStatus: null, autoHealToolCalls: loadBool(AUTO_HEAL_TOOL_CALLS_KEY, true), @@ -473,7 +487,13 @@ export const useChatRuntimeStore = create((set) => ({ chatTemplateOverride: null, loadedChatTemplateOverride: null, })), - setReasoningEnabled: (reasoningEnabled) => set({ reasoningEnabled }), + setReasoningEnabled: (reasoningEnabled, options) => + set(() => { + if (options?.persist !== false) { + saveBool(CHAT_REASONING_ENABLED_KEY, reasoningEnabled); + } + return { reasoningEnabled }; + }), setLastOpenRouterChosenModel: (lastOpenRouterChosenModel) => set({ lastOpenRouterChosenModel }), setReasoningStyle: (reasoningStyle) => set({ reasoningStyle }), @@ -493,8 +513,18 @@ export const useChatRuntimeStore = create((set) => ({ saveBool(PRESERVE_THINKING_KEY, preserveThinking); return { preserveThinking }; }), - setToolsEnabled: (toolsEnabled) => set({ toolsEnabled }), - setCodeToolsEnabled: (codeToolsEnabled) => set({ codeToolsEnabled }), + setToolsEnabled: (toolsEnabled, options) => + set(() => { + if (options?.persist !== false) { + saveBool(CHAT_TOOLS_ENABLED_KEY, toolsEnabled); + } + return { toolsEnabled }; + }), + setCodeToolsEnabled: (codeToolsEnabled) => + set(() => { + saveBool(CHAT_CODE_TOOLS_ENABLED_KEY, codeToolsEnabled); + return { codeToolsEnabled }; + }), setToolStatus: (toolStatus) => set({ toolStatus }), setGeneratingStatus: (generatingStatus) => set({ generatingStatus }), setAutoHealToolCalls: (autoHealToolCalls) => diff --git a/studio/frontend/src/features/chat/utils/qwen-params.ts b/studio/frontend/src/features/chat/utils/qwen-params.ts index 5eaa2388da..c6e55b7f4c 100644 --- a/studio/frontend/src/features/chat/utils/qwen-params.ts +++ b/studio/frontend/src/features/chat/utils/qwen-params.ts @@ -14,7 +14,10 @@ import { useChatRuntimeStore } from "../stores/chat-runtime-store"; export function applyQwenThinkingParams(thinkingOn: boolean): void { const store = useChatRuntimeStore.getState(); const checkpoint = store.params.checkpoint?.toLowerCase() ?? ""; - if (!checkpoint.includes("qwen3")) { + if ( + !checkpoint.includes("qwen3") || + store.activePresetSource !== "builtin-default" + ) { return; } const needsPresencePenalty =