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
This commit is contained in:
Lee Jackson 2026-05-19 19:03:00 +01:00 committed by GitHub
commit 6c52697ad0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 28 deletions

View file

@ -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;

View file

@ -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,

View file

@ -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);
}
}}

View file

@ -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<ChatRuntimeStore>((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<ChatRuntimeStore>((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<ChatRuntimeStore>((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<ChatRuntimeStore>((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) =>

View file

@ -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 =