Studio: persist external provider selection across page refresh (#5697)
Selecting a connected external provider (Anthropic, OpenAI, Google, etc.) and refreshing the page reverted the picker back to no selection. Root cause is that `PersistedInferenceParams` in `chat-settings-api.ts` excludes `checkpoint` from the server-side settings payload by design. Local model selections survive refresh because the backend re-derives them from `/api/inference/status.active_model`, but external selections have no backend mirror, so they were lost. Fix: persist `external::*` checkpoints to a small dedicated `localStorage` key (`unsloth_chat_last_external_checkpoint`) and hydrate from it on store init. Local checkpoints continue to come from the backend status as before; only external ids are mirrored client-side. `setCheckpoint` writes the key when an external id is selected and clears it when switching back to a local id, and `clearCheckpoint` clears it so the picker does not snap back after an explicit reset.
This commit is contained in:
parent
a226b7e7e9
commit
228d1cd40c
1 changed files with 69 additions and 11 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
DEFAULT_INFERENCE_PARAMS,
|
||||
type InferenceParams,
|
||||
} from "../types/runtime";
|
||||
import { isExternalModelId } from "../external-providers";
|
||||
import {
|
||||
loadChatSettingsWithLegacyImport,
|
||||
savePersistedChatSettingsPatch,
|
||||
|
|
@ -23,6 +24,40 @@ const HF_TOKEN_KEY = "unsloth_hf_token";
|
|||
export const CHAT_REASONING_ENABLED_KEY = "unsloth_chat_reasoning_enabled";
|
||||
export const CHAT_TOOLS_ENABLED_KEY = "unsloth_chat_tools_enabled";
|
||||
export const CHAT_CODE_TOOLS_ENABLED_KEY = "unsloth_chat_code_tools_enabled";
|
||||
// External provider selection is encoded into `params.checkpoint` as
|
||||
// `external::<providerId>::<modelId>`. PersistedChatSettings deliberately
|
||||
// Omits `checkpoint` because the local-model side is mirrored by the
|
||||
// backend's `/api/inference/status.active_model` response. External
|
||||
// selections have no such backend mirror, so without explicit
|
||||
// localStorage persistence here the user's external pick is silently
|
||||
// reset to the default on every page refresh.
|
||||
const LAST_EXTERNAL_CHECKPOINT_KEY = "unsloth_chat_last_external_checkpoint";
|
||||
|
||||
function loadLastExternalCheckpoint(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const value = window.localStorage.getItem(LAST_EXTERNAL_CHECKPOINT_KEY);
|
||||
return isExternalModelId(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveLastExternalCheckpoint(value: string | null): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
if (value && isExternalModelId(value)) {
|
||||
window.localStorage.setItem(LAST_EXTERNAL_CHECKPOINT_KEY, value);
|
||||
} else {
|
||||
// Clearing on a switch to a local / empty checkpoint means the
|
||||
// next refresh won't override the now-active local selection.
|
||||
window.localStorage.removeItem(LAST_EXTERNAL_CHECKPOINT_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Storage quota / private-mode failures are non-fatal -- the
|
||||
// selection just won't survive the refresh.
|
||||
}
|
||||
}
|
||||
|
||||
export type ReasoningStyle = "enable_thinking" | "reasoning_effort";
|
||||
export type ReasoningEffort =
|
||||
|
|
@ -479,7 +514,16 @@ function setScalarSettingVersion<K extends ScalarSettingKey>(
|
|||
|
||||
export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
||||
settingsHydrated: false,
|
||||
params: DEFAULT_INFERENCE_PARAMS,
|
||||
// Hydrate the last external checkpoint into params.checkpoint so the
|
||||
// external picker selection survives a page refresh. Local model
|
||||
// checkpoints are re-derived from the backend in useChatModelRuntime
|
||||
// and intentionally NOT persisted here.
|
||||
params: (() => {
|
||||
const persistedExternal = loadLastExternalCheckpoint();
|
||||
return persistedExternal
|
||||
? { ...DEFAULT_INFERENCE_PARAMS, checkpoint: persistedExternal }
|
||||
: DEFAULT_INFERENCE_PARAMS;
|
||||
})(),
|
||||
customPresets: [],
|
||||
activePreset: "Default",
|
||||
activePresetSource: getPresetSource("Default"),
|
||||
|
|
@ -640,18 +684,31 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
}),
|
||||
setModelsError: (modelsError) => set({ modelsError }),
|
||||
setCheckpoint: (modelId, ggufVariant) =>
|
||||
set((state) => ({
|
||||
params: {
|
||||
...state.params,
|
||||
checkpoint: modelId,
|
||||
},
|
||||
activeGgufVariant: ggufVariant ?? null,
|
||||
})),
|
||||
set((state) => {
|
||||
// Persist external selections so they survive a page refresh.
|
||||
// Local model ids are NOT persisted here -- they get re-derived
|
||||
// from the backend's `/api/inference/status.active_model` on
|
||||
// mount, and a stale persisted local id would race against the
|
||||
// freshly-loaded model. See LAST_EXTERNAL_CHECKPOINT_KEY notes.
|
||||
saveLastExternalCheckpoint(isExternalModelId(modelId) ? modelId : null);
|
||||
return {
|
||||
params: {
|
||||
...state.params,
|
||||
checkpoint: modelId,
|
||||
},
|
||||
activeGgufVariant: ggufVariant ?? null,
|
||||
};
|
||||
}),
|
||||
setActiveThreadId: (activeThreadId) =>
|
||||
set({ activeThreadId, contextUsage: null }),
|
||||
setSettingsPanelOpen: (settingsPanelOpen) => set({ settingsPanelOpen }),
|
||||
clearCheckpoint: () =>
|
||||
set((state) => ({
|
||||
clearCheckpoint: () => {
|
||||
// Mirror setCheckpoint's persistence behavior: dropping the
|
||||
// checkpoint must also clear any stored external selection so
|
||||
// the next refresh doesn't snap back to a model the user
|
||||
// intentionally cleared.
|
||||
saveLastExternalCheckpoint(null);
|
||||
return set((state) => ({
|
||||
params: {
|
||||
...state.params,
|
||||
checkpoint: "",
|
||||
|
|
@ -687,7 +744,8 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
defaultChatTemplate: null,
|
||||
chatTemplateOverride: null,
|
||||
loadedChatTemplateOverride: null,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
setReasoningEnabled: (reasoningEnabled, options) =>
|
||||
set(() => {
|
||||
if (options?.persist !== false) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue