Studio: Refine chat preset and group built-in presets (#5159)

* UX: Refine chat preset and group built-in presets

* fix: reuse built-in preset names and unify GGUF state reads

* fix: built-in chat preset save and refresh behavior

* Add chat preset invariant tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: decouple chat presets from model-specific settings

Limit chat preset compare/apply/save behavior to temperature, topP, topK, minP, repetitionPenalty, presencePenalty, maxTokens, and systemPrompt.

Preserve legacy stored preset data on load for backwards compatibility, but stop treating model-specific settings such as checkpoint, trustRemoteCode, and maxSeqLength as part of preset identity.

Also align legacy prompt migration dedupe with the new preset semantics and add invariant coverage for preset-owned config comparisons.

* fix: detect built-in preset edits from param changes

* fix: correct built-in preset dirty state and speculative select values

* fix: preserve default preset sync and keep qwen think pristine

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Lee Jackson 2026-04-28 10:40:15 +01:00 committed by GitHub
commit 975a5c354f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1040 additions and 465 deletions

View file

@ -484,10 +484,6 @@ export function ChatPage(): ReactElement {
const settingsOpen = useChatRuntimeStore((s) => s.settingsPanelOpen);
const setSettingsOpen = useChatRuntimeStore((s) => s.setSettingsPanelOpen);
useEffect(() => {
return () => setSettingsOpen(false);
}, [setSettingsOpen]);
useEffect(() => {
const threadId = search.thread;
if (!threadId) return;

View file

@ -19,6 +19,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
@ -64,54 +65,36 @@ import {
} from "@/components/ui/tooltip";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { AnimatePresence, motion } from "motion/react";
import type { ReactNode } from "react";
import { Fragment, type ReactNode } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
import {
DEFAULT_INFERENCE_PARAMS,
type InferenceParams,
} from "./types/runtime";
applyPresetParams,
BUILTIN_PRESET_NAMES,
BUILTIN_PRESETS,
defaultInferenceParams,
getBuiltinVariantName,
getOrderedPresets,
getPresetOwnedConfigKey,
getPresetSaveState,
getPresetSource,
getUniquePresetName,
isSamePresetConfig,
normalizeCustomPresets,
toPresetParams,
type Preset,
} from "./presets/preset-policy";
import type { InferenceParams } from "./types/runtime";
export const defaultInferenceParams = DEFAULT_INFERENCE_PARAMS;
export { defaultInferenceParams, type Preset } from "./presets/preset-policy";
export type { InferenceParams } from "./types/runtime";
export interface Preset {
name: string;
params: InferenceParams;
}
interface LegacySystemPromptTemplate {
name: string;
content: string;
}
const BUILTIN_PRESETS: Preset[] = [
{ name: "Default", params: { ...defaultInferenceParams } },
{
name: "Creative",
params: {
...defaultInferenceParams,
temperature: 1.5,
topP: 1.0,
topK: 0,
minP: 0.1,
repetitionPenalty: 1.0,
},
},
{
name: "Precise",
params: {
...defaultInferenceParams,
temperature: 0.1,
topP: 0.95,
topK: 80,
minP: 0.01,
repetitionPenalty: 1.0,
},
},
];
const CHAT_PRESETS_KEY = "unsloth_chat_custom_presets";
const CHAT_ACTIVE_PRESET_KEY = "unsloth_chat_active_preset";
const LEGACY_CHAT_SYSTEM_PROMPTS_KEY = "unsloth_chat_system_prompts";
@ -122,16 +105,13 @@ function canUseStorage(): boolean {
return typeof window !== "undefined";
}
function getUniquePresetName(baseName: string, usedNames: Set<string>): string {
const normalizedBase = baseName.trim() || "Imported Prompt";
let nextName = normalizedBase;
let suffix = 2;
while (usedNames.has(nextName)) {
nextName = `${normalizedBase} ${suffix}`;
suffix += 1;
function saveCustomPresets(presets: Preset[]): void {
if (!canUseStorage()) return;
try {
localStorage.setItem(CHAT_PRESETS_KEY, JSON.stringify(presets));
} catch {
// ignore
}
usedNames.add(nextName);
return nextName;
}
function migrateLegacySystemPromptTemplates(presets: Preset[]): Preset[] {
@ -161,18 +141,7 @@ function migrateLegacySystemPromptTemplates(presets: Preset[]): Preset[] {
]);
const seenImportedConfigKeys = new Set(
[...BUILTIN_PRESETS, ...presets].map((preset) =>
JSON.stringify({
temperature: preset.params.temperature,
topP: preset.params.topP,
topK: preset.params.topK,
minP: preset.params.minP,
repetitionPenalty: preset.params.repetitionPenalty,
presencePenalty: preset.params.presencePenalty,
maxSeqLength: preset.params.maxSeqLength,
maxTokens: preset.params.maxTokens,
systemPrompt: preset.params.systemPrompt,
trustRemoteCode: preset.params.trustRemoteCode ?? false,
}),
getPresetOwnedConfigKey(preset.params),
),
);
const importedPresets = parsed
@ -191,18 +160,7 @@ function migrateLegacySystemPromptTemplates(presets: Preset[]): Preset[] {
},
}))
.filter(({ importedParams }) => {
const configKey = JSON.stringify({
temperature: importedParams.temperature,
topP: importedParams.topP,
topK: importedParams.topK,
minP: importedParams.minP,
repetitionPenalty: importedParams.repetitionPenalty,
presencePenalty: importedParams.presencePenalty,
maxSeqLength: importedParams.maxSeqLength,
maxTokens: importedParams.maxTokens,
systemPrompt: importedParams.systemPrompt,
trustRemoteCode: importedParams.trustRemoteCode ?? false,
});
const configKey = getPresetOwnedConfigKey(importedParams);
if (seenImportedConfigKeys.has(configKey)) return false;
seenImportedConfigKeys.add(configKey);
return true;
@ -216,8 +174,8 @@ function migrateLegacySystemPromptTemplates(presets: Preset[]): Preset[] {
localStorage.setItem(LEGACY_CHAT_SYSTEM_PROMPTS_MIGRATED_KEY, raw);
return presets;
}
const mergedPresets = [...presets, ...importedPresets];
localStorage.setItem(CHAT_PRESETS_KEY, JSON.stringify(mergedPresets));
const mergedPresets = normalizeCustomPresets([...presets, ...importedPresets]);
saveCustomPresets(mergedPresets);
try {
localStorage.setItem(LEGACY_CHAT_SYSTEM_PROMPTS_MIGRATED_KEY, raw);
localStorage.removeItem(LEGACY_CHAT_SYSTEM_PROMPTS_KEY);
@ -255,7 +213,11 @@ function loadSavedCustomPresets(): Preset[] {
},
}))
.filter((preset) => preset.name.length > 0);
return migrateLegacySystemPromptTemplates(presets);
const normalized = normalizeCustomPresets(presets);
if (JSON.stringify(normalized) !== JSON.stringify(presets)) {
saveCustomPresets(normalized);
}
return migrateLegacySystemPromptTemplates(normalized);
} catch {
return migrateLegacySystemPromptTemplates([]);
}
@ -270,82 +232,6 @@ function loadSavedActivePreset(): string {
}
}
type PresetSaveMode =
| "disabled"
| "overwrite-active"
| "overwrite-other"
| "create";
interface PresetSaveState {
mode: PresetSaveMode;
canSubmit: boolean;
isSaveReady: boolean;
buttonLabel: string;
title: string;
}
function isSamePresetConfig(a: InferenceParams, b: InferenceParams): boolean {
return (
a.temperature === b.temperature &&
a.topP === b.topP &&
a.topK === b.topK &&
a.minP === b.minP &&
a.repetitionPenalty === b.repetitionPenalty &&
a.presencePenalty === b.presencePenalty &&
a.maxSeqLength === b.maxSeqLength &&
a.maxTokens === b.maxTokens &&
a.systemPrompt === b.systemPrompt &&
(a.trustRemoteCode ?? false) === (b.trustRemoteCode ?? false)
);
}
function getPresetSaveState({
rawName,
activePreset,
presets,
activePresetDirty,
}: {
rawName: string;
activePreset: string;
presets: Preset[];
activePresetDirty: boolean;
}): PresetSaveState {
const trimmedName = rawName.trim();
if (!trimmedName) {
return {
mode: "disabled",
canSubmit: false,
isSaveReady: false,
buttonLabel: "Save",
title: "Enter a preset name",
};
}
const matchingPreset = presets.find((preset) => preset.name === trimmedName);
if (matchingPreset) {
const isActiveMatch = matchingPreset.name === activePreset;
return {
mode: isActiveMatch ? "overwrite-active" : "overwrite-other",
canSubmit: !isActiveMatch || activePresetDirty,
isSaveReady: !isActiveMatch || activePresetDirty,
buttonLabel: isActiveMatch && !activePresetDirty ? "Saved" : "Overwrite",
title: isActiveMatch
? activePresetDirty
? "Save current settings to this preset"
: "No unsaved changes"
: `Overwrite preset "${trimmedName}"`,
};
}
return {
mode: "create",
canSubmit: true,
isSaveReady: true,
buttonLabel: "Save as New",
title: `Save current settings as "${trimmedName}"`,
};
}
function ParamSlider({
label,
value,
@ -518,6 +404,10 @@ export function ChatSettingsPanel({
const setCustomContextLength = useChatRuntimeStore(
(s) => s.setCustomContextLength,
);
const setActivePresetSource = useChatRuntimeStore(
(s) => s.setActivePresetSource,
);
const activePresetSource = useChatRuntimeStore((s) => s.activePresetSource);
const ctxDisplayValue = customContextLength ?? ggufContextLength ?? "";
const ctxMaxValue = ggufNativeContextLength ?? ggufContextLength ?? null;
@ -540,12 +430,9 @@ export function ChatSettingsPanel({
>(undefined);
const [systemPromptEditorOpen, setSystemPromptEditorOpen] = useState(false);
const [systemPromptDraft, setSystemPromptDraft] = useState("");
const [activePresetBaseline, setActivePresetBaseline] = useState(params);
const presets = useMemo(() => {
const overrides = new Set(customPresets.map((preset) => preset.name));
return [
...BUILTIN_PRESETS.filter((preset) => !overrides.has(preset.name)),
...customPresets,
];
return getOrderedPresets(customPresets);
}, [customPresets]);
const activePresetDefinition = useMemo(
() => presets.find((preset) => preset.name === activePreset) ?? null,
@ -555,17 +442,23 @@ export function ChatSettingsPanel({
() => customPresets.find((preset) => preset.name === activePreset) ?? null,
[activePreset, customPresets],
);
const activeBuiltinPreset = useMemo(
() =>
BUILTIN_PRESETS.find((preset) => preset.name === activePreset) ?? null,
[activePreset],
);
const activePresetDirty = useMemo(
() =>
activePresetDefinition == null
? false
: !isSamePresetConfig(activePresetDefinition.params, params),
[activePresetDefinition, params],
const hasUnsavedPresetChanges = useMemo(
() => {
if (activePresetDefinition == null) {
return false;
}
if (BUILTIN_PRESET_NAMES.has(activePresetDefinition.name)) {
if (activePresetDefinition.name === "Default") {
return activePresetSource === "modified";
}
return (
activePresetSource === "modified" ||
!isSamePresetConfig(activePresetDefinition.params, params)
);
}
return !isSamePresetConfig(activePresetDefinition.params, params);
},
[activePresetDefinition, activePresetSource, params],
);
const presetSaveState = useMemo(
() =>
@ -573,9 +466,9 @@ export function ChatSettingsPanel({
rawName: presetNameInput,
activePreset,
presets,
activePresetDirty,
hasUnsavedPresetChanges,
}),
[activePreset, activePresetDirty, presetNameInput, presets],
[activePreset, hasUnsavedPresetChanges, presetNameInput, presets],
);
const systemPromptEditorDirty = systemPromptDraft !== params.systemPrompt;
const trustRemoteCodeMissing =
@ -584,27 +477,24 @@ export function ChatSettingsPanel({
!(params.trustRemoteCode ?? false);
function set<K extends keyof InferenceParams>(key: K) {
return (v: InferenceParams[K]) => onParamsChange({ ...params, [key]: v });
return (v: InferenceParams[K]) => {
const nextParams = { ...params, [key]: v };
const nextSource = isSamePresetConfig(activePresetBaseline, nextParams)
? getPresetSource(activePreset)
: "modified";
setActivePresetSource(nextSource);
onParamsChange(nextParams);
};
}
function applyPreset(name: string) {
const p = presets.find((pr) => pr.name === name);
if (p) {
if (
modelRequiresTrustRemoteCode &&
!(p.params.trustRemoteCode ?? false)
) {
toast.warning("This configuration turns custom code off", {
description:
"The current model needs custom code enabled to load. Keep it on for this model.",
});
return;
}
onParamsChange({
...p.params,
checkpoint: params.checkpoint,
...applyPresetParams(params, p.params),
});
setActivePreset(name);
setActivePresetSource(getPresetSource(name));
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_ACTIVE_PRESET_KEY, name);
@ -621,27 +511,29 @@ export function ChatSettingsPanel({
toast.error("Enter a preset name");
return;
}
const usedNames = new Set([
...BUILTIN_PRESET_NAMES,
...customPresets.map((preset) => preset.name),
]);
const saveName = BUILTIN_PRESET_NAMES.has(trimmed)
? getBuiltinVariantName(trimmed, usedNames)
: trimmed;
setCustomPresets((prev) => {
const next = prev.filter((p) => p.name !== trimmed);
const merged = [...next, { name: trimmed, params: { ...params } }];
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_PRESETS_KEY, JSON.stringify(merged));
} catch {
// ignore
}
}
const next = prev.filter((p) => p.name !== saveName);
const merged = [...next, { name: saveName, params: toPresetParams(params) }];
saveCustomPresets(merged);
return merged;
});
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_ACTIVE_PRESET_KEY, trimmed);
localStorage.setItem(CHAT_ACTIVE_PRESET_KEY, saveName);
} catch {
// ignore
}
}
setActivePreset(trimmed);
setPresetNameInput(trimmed);
setActivePreset(saveName);
setActivePresetSource("custom");
setPresetNameInput(saveName);
}
function deletePreset(name: string) {
@ -651,41 +543,20 @@ export function ChatSettingsPanel({
if (!hasCustomPreset) {
return;
}
const builtinPreset = BUILTIN_PRESETS.find((preset) => preset.name === name);
const fallbackPreset =
builtinPreset ??
BUILTIN_PRESETS.find((preset) => preset.name === "Default") ??
null;
if (
activePreset === name &&
fallbackPreset &&
modelRequiresTrustRemoteCode &&
!(fallbackPreset.params.trustRemoteCode ?? false)
) {
toast.warning("Reset would turn custom code off", {
description:
"The current model needs custom code enabled to load. Keep it on for this model.",
});
return;
}
BUILTIN_PRESETS.find((preset) => preset.name === "Default") ?? null;
setCustomPresets((prev) => {
const next = prev.filter((preset) => preset.name !== name);
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_PRESETS_KEY, JSON.stringify(next));
} catch {
// ignore
}
}
saveCustomPresets(next);
return next;
});
if (activePreset === name) {
if (fallbackPreset) {
onParamsChange({
...fallbackPreset.params,
checkpoint: params.checkpoint,
...applyPresetParams(params, fallbackPreset.params),
});
setActivePreset(fallbackPreset.name);
setActivePresetSource("builtin-default");
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_ACTIVE_PRESET_KEY, fallbackPreset.name);
@ -708,8 +579,46 @@ export function ChatSettingsPanel({
}
useEffect(() => {
if (presets.some((preset) => preset.name === activePreset)) return;
if (activePresetSource !== "modified") {
setActivePresetBaseline(params);
}
}, [activePresetSource, params]);
useEffect(() => {
if (presets.some((preset) => preset.name === activePreset)) {
const expectedSource = getPresetSource(activePreset);
if (activePresetDefinition != null) {
if (BUILTIN_PRESET_NAMES.has(activePresetDefinition.name)) {
if (activePresetDefinition.name === "Default") {
if (
activePresetSource !== "modified" &&
activePresetSource !== expectedSource
) {
setActivePresetSource(expectedSource);
}
return;
}
const matchesActivePreset = isSamePresetConfig(
activePresetDefinition.params,
params,
);
const nextSource = matchesActivePreset ? expectedSource : "modified";
if (activePresetSource !== nextSource) {
setActivePresetSource(nextSource);
}
return;
}
}
if (
activePresetSource !== "modified" &&
activePresetSource !== expectedSource
) {
setActivePresetSource(expectedSource);
}
return;
}
setActivePreset("Default");
setActivePresetSource("builtin-default");
if (canUseStorage()) {
try {
localStorage.setItem(CHAT_ACTIVE_PRESET_KEY, "Default");
@ -717,7 +626,14 @@ export function ChatSettingsPanel({
// ignore
}
}
}, [activePreset, presets]);
}, [
activePreset,
activePresetDefinition,
activePresetSource,
params,
presets,
setActivePresetSource,
]);
useEffect(() => {
setPresetNameInput(activePreset);
@ -741,6 +657,187 @@ export function ChatSettingsPanel({
return () => ro.disconnect();
}, [open]);
const modelSection = (
<CollapsibleSection
icon={Settings02Icon}
label="Model"
defaultOpen={true}
>
<div className="flex flex-col gap-3 py-1">
{isGguf && (
<>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Context Length</span>
<Input
type="number"
value={
typeof ctxDisplayValue === "number"
? ctxDisplayValue
: (ggufContextLength ?? "")
}
placeholder="..."
min={128}
max={ctxMaxValue ?? undefined}
step={1024}
className="h-6 w-[100px] text-right text-xs tabular-nums"
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
setCustomContextLength(null);
return;
}
const v = Number.parseInt(raw, 10);
if (!Number.isNaN(v) && v >= 0) {
const maxCtx = ctxMaxValue ?? Number.POSITIVE_INFINITY;
const clamped = Math.min(v, maxCtx);
setCustomContextLength(
clamped === (ggufContextLength ?? 0) ? null : clamped,
);
}
}}
/>
</div>
<Slider
min={1024}
max={ctxMaxValue ?? 4096}
step={1024}
value={[
Math.min(
typeof ctxDisplayValue === "number"
? ctxDisplayValue
: (ggufContextLength ?? 4096),
ctxMaxValue ?? 4096,
),
]}
onValueChange={([v]) => {
setCustomContextLength(
v === (ggufContextLength ?? 0) ? null : v,
);
}}
/>
{ggufMaxContextLength != null &&
typeof ctxDisplayValue === "number" &&
ctxDisplayValue > ggufMaxContextLength && (
<p className="text-[11px] text-amber-500">
Exceeds estimated VRAM capacity (
{ggufMaxContextLength.toLocaleString()} tokens). The model
may use system RAM.
</p>
)}
</div>
<div className="grid grid-cols-[minmax(0,1fr)_65px] items-center gap-x-3">
<div className="min-w-0">
<div className="text-xs font-medium">KV Cache Dtype</div>
<div className="text-[11px] text-muted-foreground">
Quantize KV cache to reduce VRAM.
</div>
</div>
<div className="w-full min-w-0">
<Select
value={kvCacheDtype ?? "f16"}
onValueChange={(v) => {
setKvCacheDtype(v === "f16" ? null : v);
}}
>
<SelectTrigger className="grid h-7 w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 px-2 py-0 text-xs [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="f16">f16</SelectItem>
<SelectItem value="bf16">bf16</SelectItem>
<SelectItem value="q8_0">q8_0</SelectItem>
<SelectItem value="q5_1">q5_1</SelectItem>
<SelectItem value="q4_1">q4_1</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{!currentModelIsVision && (
<div className="grid grid-cols-[minmax(0,1fr)_65px] items-center gap-x-3">
<div className="min-w-0">
<div className="text-xs font-medium">
Speculative Decoding
</div>
<div className="text-[11px] text-muted-foreground">
Speed up generation with no VRAM cost.
</div>
</div>
<div className="w-full min-w-0">
<Select
value={speculativeType ?? "off"}
onValueChange={(v) => {
setSpeculativeType(v === "off" ? null : v);
}}
>
<SelectTrigger className="grid h-7 w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 px-2 py-0 text-xs [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">On</SelectItem>
<SelectItem value="off">Off</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{modelSettingsDirty && (
<div className="flex flex-wrap gap-1.5 pt-1">
<button
type="button"
onClick={() => onReloadModel?.()}
className="rounded-md bg-primary px-2.5 py-1 text-[11px] font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Apply
</button>
<button
type="button"
onClick={() => {
setCustomContextLength(null);
setKvCacheDtype(loadedKvCacheDtype);
setSpeculativeType(loadedSpeculativeType);
}}
className="rounded-md border px-2.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent"
>
Reset
</button>
</div>
)}
</>
)}
{!isGguf && params.checkpoint && (
<>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium">Enable custom code</div>
<div className="text-[11px] text-muted-foreground">
Allow models with custom code (e.g. Nemotron). Only enable if
sure.
</div>
</div>
<Switch
checked={params.trustRemoteCode ?? false}
onCheckedChange={set("trustRemoteCode")}
/>
</div>
{trustRemoteCodeMissing && (
<Alert className="border-amber-200/70 bg-amber-50/70 px-3 py-2 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/35 dark:text-amber-100">
<AlertTitle className="text-[11px] font-medium">
Keep custom code enabled for this model
</AlertTitle>
<AlertDescription className="text-[11px] text-amber-800 dark:text-amber-200">
This model requires custom code to load. You can edit the
toggle, but loading will stay blocked until it is turned back
on.
</AlertDescription>
</Alert>
)}
</>
)}
</div>
</CollapsibleSection>
);
const settingsContent = (
<>
<div className="aui-thread-viewport relative h-full overflow-y-auto">
@ -834,13 +931,16 @@ export function ChatSettingsPanel({
: undefined
}
>
{presets.map((p) => (
<DropdownMenuItem
key={p.name}
onSelect={() => applyPreset(p.name)}
>
{p.name}
</DropdownMenuItem>
{presets.map((p, index) => (
<Fragment key={p.name}>
<DropdownMenuItem onSelect={() => applyPreset(p.name)}>
{p.name}
</DropdownMenuItem>
{index === BUILTIN_PRESETS.length - 1 &&
presets.length > BUILTIN_PRESETS.length && (
<DropdownMenuSeparator className="mx-2.5! my-1.5! h-0! border-t border-border/70 bg-transparent!" />
)}
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
@ -874,9 +974,7 @@ export function ChatSettingsPanel({
className="h-8 w-full text-xs text-muted-foreground"
title={
activeCustomPreset
? activeBuiltinPreset
? "Reset selected preset to built-in defaults"
: "Delete selected preset"
? "Delete selected preset"
: "No saved override to delete"
}
>
@ -918,188 +1016,6 @@ export function ChatSettingsPanel({
/>
</div>
<CollapsibleSection
icon={Settings02Icon}
label="Model"
defaultOpen={true}
>
<div className="flex flex-col gap-3 py-1">
{isGguf && (
<>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Context Length</span>
<Input
type="number"
value={
typeof ctxDisplayValue === "number"
? ctxDisplayValue
: (ggufContextLength ?? "")
}
placeholder="..."
min={128}
max={ctxMaxValue ?? undefined}
step={1024}
className="h-6 w-[100px] text-right text-xs tabular-nums"
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
setCustomContextLength(null);
return;
}
const v = Number.parseInt(raw, 10);
if (!Number.isNaN(v) && v >= 0) {
const maxCtx =
ctxMaxValue ?? Number.POSITIVE_INFINITY;
const clamped = Math.min(v, maxCtx);
setCustomContextLength(
clamped === (ggufContextLength ?? 0)
? null
: clamped,
);
}
}}
/>
</div>
<Slider
min={1024}
max={ctxMaxValue ?? 4096}
step={1024}
value={[
Math.min(
typeof ctxDisplayValue === "number"
? ctxDisplayValue
: (ggufContextLength ?? 4096),
ctxMaxValue ?? 4096,
),
]}
onValueChange={([v]) => {
setCustomContextLength(
v === (ggufContextLength ?? 0) ? null : v,
);
}}
/>
{ggufMaxContextLength != null &&
typeof ctxDisplayValue === "number" &&
ctxDisplayValue > ggufMaxContextLength && (
<p className="text-[11px] text-amber-500">
Exceeds estimated VRAM capacity (
{ggufMaxContextLength.toLocaleString()} tokens). The
model may use system RAM.
</p>
)}
</div>
<div className="grid grid-cols-[minmax(0,1fr)_65px] items-center gap-x-3">
<div className="min-w-0">
<div className="text-xs font-medium">KV Cache Dtype</div>
<div className="text-[11px] text-muted-foreground">
Quantize KV cache to reduce VRAM.
</div>
</div>
<div className="w-full min-w-0">
<Select
value={kvCacheDtype ?? "f16"}
onValueChange={(v) => {
setKvCacheDtype(v === "f16" ? null : v);
}}
>
<SelectTrigger className="grid h-7 w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 px-2 py-0 text-xs [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="f16">f16</SelectItem>
<SelectItem value="bf16">bf16</SelectItem>
<SelectItem value="q8_0">q8_0</SelectItem>
<SelectItem value="q5_1">q5_1</SelectItem>
<SelectItem value="q4_1">q4_1</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{!currentModelIsVision && (
<div className="grid grid-cols-[minmax(0,1fr)_65px] items-center gap-x-3">
<div className="min-w-0">
<div className="text-xs font-medium">
Speculative Decoding
</div>
<div className="text-[11px] text-muted-foreground">
Speed up generation with no VRAM cost.
</div>
</div>
<div className="w-full min-w-0">
<Select
value={speculativeType ?? "off"}
onValueChange={(v) => {
setSpeculativeType(v === "off" ? null : v);
}}
>
<SelectTrigger className="grid h-7 w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 px-2 py-0 text-xs [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">On</SelectItem>
<SelectItem value="off">Off</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{modelSettingsDirty && (
<div className="flex flex-wrap gap-1.5 pt-1">
<button
type="button"
onClick={() => onReloadModel?.()}
className="rounded-md bg-primary px-2.5 py-1 text-[11px] font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Apply
</button>
<button
type="button"
onClick={() => {
setCustomContextLength(null);
setKvCacheDtype(loadedKvCacheDtype);
setSpeculativeType(loadedSpeculativeType);
}}
className="rounded-md border px-2.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent"
>
Reset
</button>
</div>
)}
</>
)}
{!isGguf && params.checkpoint && (
<>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium">Enable custom code</div>
<div className="text-[11px] text-muted-foreground">
Allow models with custom code (e.g. Nemotron). Only
enable if sure.
</div>
</div>
<Switch
checked={params.trustRemoteCode ?? false}
onCheckedChange={set("trustRemoteCode")}
/>
</div>
{trustRemoteCodeMissing && (
<Alert className="border-amber-200/70 bg-amber-50/70 px-3 py-2 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/35 dark:text-amber-100">
<AlertTitle className="text-[11px] font-medium">
Keep custom code enabled for this model
</AlertTitle>
<AlertDescription className="text-[11px] text-amber-800 dark:text-amber-200">
This model requires custom code to load. You can edit the
toggle, but loading will stay blocked until it is turned
back on.
</AlertDescription>
</Alert>
)}
</>
)}
</div>
</CollapsibleSection>
<CollapsibleSection
icon={SlidersHorizontalIcon}
label="Sampling"
@ -1186,6 +1102,8 @@ export function ChatSettingsPanel({
</div>
</CollapsibleSection>
{modelSection}
<CollapsibleSection icon={Wrench01Icon} label="Tools">
<div className="flex flex-col gap-3 py-1">
<AutoHealToolCallsToggle />

View file

@ -17,6 +17,10 @@ import {
} from "../api/chat-api";
import { formatEta, formatRate } from "../utils/format-transfer";
import { useChatRuntimeStore } from "../stores/chat-runtime-store";
import {
mergeBackendRecommendedInference,
resolveLoadMaxSeqLength,
} from "../presets/preset-policy";
import type { InferenceStatusResponse, LoadModelResponse } from "../types/api";
import type {
ChatLoraSummary,
@ -132,46 +136,10 @@ function toLoraSummary(lora: {
};
}
function toFiniteNumber(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
return value;
}
function getTrustRemoteCodeRequiredMessage(modelName: string): string {
return `${modelName} needs custom code enabled to load. Turn on "Enable custom code" in Chat Settings, then try again.`;
}
function mergeRecommendedInference(
current: InferenceParams,
response: LoadModelResponse | InferenceStatusResponse,
modelId: string,
): InferenceParams {
const inference = response.inference;
// GGUF: use actual context length from GGUF metadata, fallback to 131072
// Non-GGUF: 4096
const defaultMaxTokens = response.is_gguf
? (response.context_length ?? 131072)
: 4096;
return {
...current,
checkpoint: modelId,
maxTokens: defaultMaxTokens,
temperature:
toFiniteNumber(inference?.temperature) ?? current.temperature,
topP: toFiniteNumber(inference?.top_p) ?? current.topP,
topK: toFiniteNumber(inference?.top_k) ?? current.topK,
minP: toFiniteNumber(inference?.min_p) ?? current.minP,
presencePenalty:
toFiniteNumber(inference?.presence_penalty) ?? current.presencePenalty,
trustRemoteCode:
typeof inference?.trust_remote_code === "boolean"
? inference.trust_remote_code
: current.trustRemoteCode,
};
}
export function useChatModelRuntime() {
const params = useChatRuntimeStore((state) => state.params);
const models = useChatRuntimeStore((state) => state.models);
@ -273,7 +241,12 @@ export function useChatModelRuntime() {
speculative_type: statusRes.speculative_type,
};
setParams(
mergeRecommendedInference(currentParams, statusRes, statusRes.active_model),
mergeBackendRecommendedInference({
current: currentParams,
response: statusRes,
modelId: statusRes.active_model,
presetSource: useChatRuntimeStore.getState().activePresetSource,
}),
);
}
@ -465,13 +438,25 @@ export function useChatModelRuntime() {
previousWasUnloaded = true;
}
const { chatTemplateOverride, kvCacheDtype, customContextLength, ggufContextLength, speculativeType } = useChatRuntimeStore.getState();
// GGUF: use custom context length, or 0 = model's native context
// Non-GGUF: use the Max Seq Length slider value
const isDirectGgufFile = modelId.toLowerCase().endsWith(".gguf");
const effectiveMaxSeqLength = customContextLength != null
? customContextLength
: (ggufVariant != null || isDirectGgufFile) ? (ggufContextLength ?? 0) : maxSeqLength;
const {
chatTemplateOverride,
kvCacheDtype,
customContextLength,
ggufContextLength,
speculativeType,
activePresetSource,
activeGgufVariant,
} = useChatRuntimeStore.getState();
const effectiveMaxSeqLength = resolveLoadMaxSeqLength({
modelId,
ggufVariant,
customContextLength,
ggufContextLength,
currentCheckpoint,
activeGgufVariant,
maxSeqLength,
presetSource: activePresetSource,
});
const loadResponse = await loadModel({
model_path: modelId,
hf_token: hfToken,
@ -491,7 +476,12 @@ export function useChatModelRuntime() {
const currentParams = useChatRuntimeStore.getState().params;
setParams(
mergeRecommendedInference(currentParams, loadResponse, modelId),
mergeBackendRecommendedInference({
current: currentParams,
response: loadResponse,
modelId,
presetSource: useChatRuntimeStore.getState().activePresetSource,
}),
);
// Qwen3.5/3.6 small models (0.8B, 2B, 4B, 9B) disable thinking by default
let reasoningDefault = loadResponse.supports_reasoning ?? false;
@ -545,12 +535,31 @@ export function useChatModelRuntime() {
// Qwen3/3.5/3.6: apply thinking-mode-specific params after load
if (modelId.toLowerCase().includes("qwen3") && (loadResponse.supports_reasoning ?? false)) {
const store = useChatRuntimeStore.getState();
const mid = modelId.toLowerCase();
const needsPresencePenalty = mid.includes("qwen3.5") || mid.includes("qwen3.6");
const p = reasoningDefault
? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0, ...(needsPresencePenalty ? { presencePenalty: 1.5 } : {}) }
: { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0, ...(needsPresencePenalty ? { presencePenalty: 1.5 } : {}) };
store.setParams({ ...store.params, ...p });
if (store.activePresetSource === "builtin-default") {
const mid = modelId.toLowerCase();
const needsPresencePenalty =
mid.includes("qwen3.5") || mid.includes("qwen3.6");
const p = reasoningDefault
? {
temperature: 0.6,
topP: 0.95,
topK: 20,
minP: 0.0,
...(needsPresencePenalty
? { presencePenalty: 1.5 }
: {}),
}
: {
temperature: 0.7,
topP: 0.8,
topK: 20,
minP: 0.0,
...(needsPresencePenalty
? { presencePenalty: 1.5 }
: {}),
};
store.setParams({ ...store.params, ...p });
}
}
await refresh();
} catch (error) {

View file

@ -0,0 +1,351 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import {
DEFAULT_INFERENCE_PARAMS,
type InferenceParams,
} from "../types/runtime";
export const defaultInferenceParams = DEFAULT_INFERENCE_PARAMS;
export interface Preset {
name: string;
params: InferenceParams;
}
export type PresetOwnedParams = Pick<
InferenceParams,
| "temperature"
| "topP"
| "topK"
| "minP"
| "repetitionPenalty"
| "presencePenalty"
| "maxTokens"
| "systemPrompt"
>;
export const BUILTIN_PRESETS: Preset[] = [
{ name: "Default", params: { ...defaultInferenceParams } },
{
name: "Creative",
params: {
...defaultInferenceParams,
temperature: 1.5,
topP: 1.0,
topK: 0,
minP: 0.1,
repetitionPenalty: 1.0,
},
},
{
name: "Precise",
params: {
...defaultInferenceParams,
temperature: 0.1,
topP: 0.95,
topK: 80,
minP: 0.01,
repetitionPenalty: 1.0,
},
},
];
export const BUILTIN_PRESET_NAMES = new Set(
BUILTIN_PRESETS.map((preset) => preset.name),
);
export type ChatPresetSource =
| "builtin-default"
| "builtin-fixed"
| "custom"
| "modified";
export function getPresetSource(name: string): ChatPresetSource {
if (name === "Default") return "builtin-default";
if (BUILTIN_PRESET_NAMES.has(name)) return "builtin-fixed";
return "custom";
}
export function getUniquePresetName(
baseName: string,
usedNames: Set<string>,
): string {
const normalizedBase = baseName.trim() || "Imported Prompt";
let nextName = normalizedBase;
let suffix = 2;
while (usedNames.has(nextName)) {
nextName = `${normalizedBase} ${suffix}`;
suffix += 1;
}
usedNames.add(nextName);
return nextName;
}
export function getBuiltinVariantName(
baseName: string,
usedNames: Set<string>,
): string {
const normalizedBase = baseName.trim() || "Imported Prompt";
let suffix = 1;
let nextName = `${normalizedBase} ${suffix}`;
while (usedNames.has(nextName)) {
suffix += 1;
nextName = `${normalizedBase} ${suffix}`;
}
usedNames.add(nextName);
return nextName;
}
export function normalizeCustomPresets(presets: Preset[]): Preset[] {
const usedNames = new Set(BUILTIN_PRESET_NAMES);
return presets
.map((preset): Preset | null => {
const trimmedName = preset.name.trim();
if (!trimmedName) return null;
const name = usedNames.has(trimmedName)
? getBuiltinVariantName(trimmedName, usedNames)
: trimmedName;
usedNames.add(name);
return {
name,
params: preset.params,
};
})
.filter((preset): preset is Preset => preset !== null);
}
export function getOrderedPresets(customPresets: Preset[]): Preset[] {
return [...BUILTIN_PRESETS, ...normalizeCustomPresets(customPresets)];
}
export function isSamePresetConfig(
a: InferenceParams,
b: InferenceParams,
): boolean {
const left = getPresetOwnedParams(a);
const right = getPresetOwnedParams(b);
return (
left.temperature === right.temperature &&
left.topP === right.topP &&
left.topK === right.topK &&
left.minP === right.minP &&
left.repetitionPenalty === right.repetitionPenalty &&
left.presencePenalty === right.presencePenalty &&
left.maxTokens === right.maxTokens &&
left.systemPrompt === right.systemPrompt
);
}
export function getPresetOwnedParams(
params: InferenceParams,
): PresetOwnedParams {
return {
temperature: params.temperature,
topP: params.topP,
topK: params.topK,
minP: params.minP,
repetitionPenalty: params.repetitionPenalty,
presencePenalty: params.presencePenalty,
maxTokens: params.maxTokens,
systemPrompt: params.systemPrompt,
};
}
export function getPresetOwnedConfigKey(params: InferenceParams): string {
return JSON.stringify(getPresetOwnedParams(params));
}
export function toPresetParams(params: InferenceParams): InferenceParams {
return {
...defaultInferenceParams,
...getPresetOwnedParams(params),
};
}
export function applyPresetParams(
current: InferenceParams,
preset: InferenceParams,
): InferenceParams {
return {
...current,
...getPresetOwnedParams(preset),
};
}
export type PresetSaveMode =
| "disabled"
| "overwrite-active"
| "overwrite-other"
| "copy-builtin"
| "create";
export interface PresetSaveState {
mode: PresetSaveMode;
canSubmit: boolean;
isSaveReady: boolean;
buttonLabel: string;
title: string;
}
export function getPresetSaveState({
rawName,
activePreset,
presets,
hasUnsavedPresetChanges,
}: {
rawName: string;
activePreset: string;
presets: Preset[];
hasUnsavedPresetChanges: boolean;
}): PresetSaveState {
const trimmedName = rawName.trim();
if (!trimmedName) {
return {
mode: "disabled",
canSubmit: false,
isSaveReady: false,
buttonLabel: "Save",
title: "Enter a preset name",
};
}
if (BUILTIN_PRESET_NAMES.has(trimmedName)) {
const variantName = getBuiltinVariantName(trimmedName, new Set(presets.map((preset) => preset.name)));
return {
mode: "copy-builtin",
canSubmit: activePreset !== trimmedName || hasUnsavedPresetChanges,
isSaveReady: activePreset !== trimmedName || hasUnsavedPresetChanges,
buttonLabel:
activePreset === trimmedName && !hasUnsavedPresetChanges
? "Saved"
: "Save",
title:
activePreset === trimmedName && !hasUnsavedPresetChanges
? "No unsaved changes"
: `Save current settings as "${variantName}"`,
};
}
const matchingPreset = presets.find((preset) => preset.name === trimmedName);
if (matchingPreset) {
const isActiveMatch = matchingPreset.name === activePreset;
return {
mode: isActiveMatch ? "overwrite-active" : "overwrite-other",
canSubmit: !isActiveMatch || hasUnsavedPresetChanges,
isSaveReady: !isActiveMatch || hasUnsavedPresetChanges,
buttonLabel:
isActiveMatch && !hasUnsavedPresetChanges ? "Saved" : "Save",
title: isActiveMatch
? hasUnsavedPresetChanges
? "Save current settings to this preset"
: "No unsaved changes"
: `Overwrite preset "${trimmedName}"`,
};
}
return {
mode: "create",
canSubmit: true,
isSaveReady: true,
buttonLabel: "Save",
title: `Save current settings as "${trimmedName}"`,
};
}
function toFiniteNumber(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
return value;
}
interface BackendInferenceDefaults {
temperature?: number;
top_p?: number;
top_k?: number;
min_p?: number;
presence_penalty?: number;
trust_remote_code?: boolean;
}
export interface BackendInferenceEnvelope {
is_gguf?: boolean;
context_length?: number | null;
inference?: BackendInferenceDefaults;
}
export function mergeBackendRecommendedInference({
current,
response,
modelId,
presetSource,
}: {
current: InferenceParams;
response: BackendInferenceEnvelope;
modelId: string;
presetSource: ChatPresetSource;
}): InferenceParams {
const inference = response.inference;
const next: InferenceParams = {
...current,
checkpoint: modelId,
trustRemoteCode:
typeof inference?.trust_remote_code === "boolean"
? inference.trust_remote_code
: current.trustRemoteCode,
};
if (presetSource !== "builtin-default") {
return next;
}
const defaultMaxTokens = response.is_gguf
? (response.context_length ?? current.maxTokens)
: 4096;
return {
...next,
maxTokens: defaultMaxTokens,
temperature:
toFiniteNumber(inference?.temperature) ?? defaultInferenceParams.temperature,
topP: toFiniteNumber(inference?.top_p) ?? defaultInferenceParams.topP,
topK: toFiniteNumber(inference?.top_k) ?? defaultInferenceParams.topK,
minP: toFiniteNumber(inference?.min_p) ?? defaultInferenceParams.minP,
presencePenalty:
toFiniteNumber(inference?.presence_penalty) ??
defaultInferenceParams.presencePenalty,
};
}
export function resolveLoadMaxSeqLength({
modelId,
ggufVariant,
customContextLength,
ggufContextLength,
currentCheckpoint,
activeGgufVariant,
maxSeqLength,
presetSource,
}: {
modelId: string;
ggufVariant?: string | null;
customContextLength: number | null;
ggufContextLength: number | null;
currentCheckpoint: string;
activeGgufVariant?: string | null;
maxSeqLength: number;
presetSource: ChatPresetSource;
}): number {
const isDirectGgufFile = modelId.toLowerCase().endsWith(".gguf");
const isGgufLoad = ggufVariant != null || isDirectGgufFile;
const isReloadingCurrentGguf =
isGgufLoad &&
currentCheckpoint === modelId &&
(ggufVariant ?? null) === (activeGgufVariant ?? null);
if (customContextLength != null) return customContextLength;
if (isGgufLoad && presetSource === "builtin-default") return 0;
if (isReloadingCurrentGguf) return ggufContextLength ?? 0;
if (isGgufLoad) return 0;
return maxSeqLength;
}

View file

@ -9,6 +9,10 @@ import {
type ChatModelSummary,
type InferenceParams,
} from "../types/runtime";
import {
getPresetSource,
type ChatPresetSource,
} from "../presets/preset-policy";
const AUTO_TITLE_KEY = "unsloth_chat_auto_title";
const AUTO_HEAL_TOOL_CALLS_KEY = "unsloth_auto_heal_tool_calls";
@ -16,6 +20,8 @@ const MAX_TOOL_CALLS_KEY = "unsloth_max_tool_calls_per_message";
const TOOL_CALL_TIMEOUT_KEY = "unsloth_tool_call_timeout";
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";
const REASONING_EFFORT_KEY = "unsloth_reasoning_effort";
const PRESERVE_THINKING_KEY = "unsloth_preserve_thinking";
@ -157,8 +163,24 @@ function saveInferenceParams(params: InferenceParams): boolean {
}
}
function loadPresetSource(): ChatPresetSource {
const activePreset = loadString(CHAT_ACTIVE_PRESET_KEY, "Default");
if (canUseStorage()) {
try {
const raw = localStorage.getItem(CHAT_ACTIVE_PRESET_SOURCE_KEY);
if (raw === "modified") {
return "modified";
}
} catch {
// ignore
}
}
return getPresetSource(activePreset);
}
type ChatRuntimeStore = {
params: InferenceParams;
activePresetSource: ChatPresetSource;
models: ChatModelSummary[];
loras: ChatLoraSummary[];
runningByThreadId: Record<string, boolean>;
@ -207,6 +229,7 @@ type ChatRuntimeStore = {
setModelLoading: (loading: boolean) => void;
setModelRequiresTrustRemoteCode: (required: boolean) => void;
setParams: (params: InferenceParams) => void;
setActivePresetSource: (source: ChatPresetSource) => void;
setModels: (models: ChatModelSummary[]) => void;
setLoras: (loras: ChatLoraSummary[]) => void;
setThreadRunning: (threadId: string, running: boolean) => void;
@ -241,6 +264,7 @@ type ChatRuntimeStore = {
export const useChatRuntimeStore = create<ChatRuntimeStore>((set) => ({
params: loadInferenceParams(),
activePresetSource: loadPresetSource(),
models: [],
loras: [],
runningByThreadId: {},
@ -296,6 +320,11 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set) => ({
}
return { params };
}),
setActivePresetSource: (activePresetSource) =>
set(() => {
saveString(CHAT_ACTIVE_PRESET_SOURCE_KEY, activePresetSource);
return { activePresetSource };
}),
setModels: (models) => set({ models }),
setLoras: (loras) => set({ loras }),
setThreadRunning: (threadId, running) =>

View file

@ -0,0 +1,272 @@
import json
import os
import shutil
import subprocess
import textwrap
from pathlib import Path
import pytest
WORKDIR = Path(__file__).resolve().parents[2]
PRESET_POLICY = (
WORKDIR / "unsloth_repo/studio/frontend/src/features/chat/presets/preset-policy.ts"
)
RUNTIME_TYPES = (
WORKDIR / "unsloth_repo/studio/frontend/src/features/chat/types/runtime.ts"
)
TEMP = WORKDIR / "temp" / "chat_preset_builtin_invariants"
def _require_node():
if shutil.which("node") is None:
pytest.skip("node not available")
if not PRESET_POLICY.exists() or not RUNTIME_TYPES.exists():
pytest.skip("studio chat sources not present")
def _ensure_harness():
TEMP.mkdir(parents = True, exist_ok = True)
(TEMP / "register.mjs").write_text(
"import { register } from 'node:module';\n"
"register('./loader.mjs', import.meta.url);\n"
)
(TEMP / "loader.mjs").write_text(
"export function resolve(specifier, context, next) {\n"
" if (specifier.endsWith('/types/runtime')) return next(specifier + '.ts', context);\n"
" return next(specifier, context);\n"
"}\n"
)
def _run(script: str):
_require_node()
_ensure_harness()
script_path = TEMP / "run.mts"
script_path.write_text(script)
env = dict(os.environ, NODE_NO_WARNINGS = "1")
result = subprocess.run(
[
"node",
"--experimental-strip-types",
"--import=./register.mjs",
"--no-warnings",
"run.mts",
],
cwd = str(TEMP),
capture_output = True,
text = True,
timeout = 30,
env = env,
)
assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}"
last = [line for line in result.stdout.strip().splitlines() if line.strip()][-1]
return json.loads(last)
def _policy_path():
return os.path.relpath(PRESET_POLICY, TEMP).replace("\\", "/")
def _runtime_path():
return os.path.relpath(RUNTIME_TYPES, TEMP).replace("\\", "/")
def test_default_builtin_matches_default_inference_params():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
import {{ DEFAULT_INFERENCE_PARAMS }} from "{_runtime_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
console.log(JSON.stringify({{
found: !!def,
matches: def ? isSamePresetConfig(def.params, DEFAULT_INFERENCE_PARAMS) : null,
}}));
"""
)
)
assert out["found"] is True
assert out["matches"] is True
def test_is_same_preset_config_detects_temperature_edit():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const edited = {{ ...def.params, temperature: def.params.temperature + 0.1 }};
console.log(JSON.stringify({{ same: isSamePresetConfig(def.params, edited) }}));
"""
)
)
assert out["same"] is False
def test_is_same_preset_config_detects_system_prompt_edit():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const edited = {{ ...def.params, systemPrompt: "you are a pirate" }};
console.log(JSON.stringify({{ same: isSamePresetConfig(def.params, edited) }}));
"""
)
)
assert out["same"] is False
def test_is_same_preset_config_ignores_checkpoint_difference():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const withCheckpoint = {{ ...def.params, checkpoint: "meta-llama/Llama-3-8B" }};
console.log(JSON.stringify({{ same: isSamePresetConfig(def.params, withCheckpoint) }}));
"""
)
)
assert out["same"] is True
def test_is_same_preset_config_ignores_model_owned_fields():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const edited = {{
...def.params,
maxSeqLength: def.params.maxSeqLength + 1024,
trustRemoteCode: !def.params.trustRemoteCode,
}};
console.log(JSON.stringify({{ same: isSamePresetConfig(def.params, edited) }}));
"""
)
)
assert out["same"] is True
def test_preset_owned_config_key_ignores_model_owned_fields():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, getPresetOwnedConfigKey }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const edited = {{
...def.params,
checkpoint: "foo/bar",
maxSeqLength: def.params.maxSeqLength + 1024,
trustRemoteCode: !def.params.trustRemoteCode,
}};
console.log(JSON.stringify({{
same: getPresetOwnedConfigKey(def.params) === getPresetOwnedConfigKey(edited),
}}));
"""
)
)
assert out["same"] is True
def test_to_preset_params_strips_model_owned_fields():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ toPresetParams }} from "{_policy_path()}";
const sanitized = toPresetParams({{
temperature: 0.9,
topP: 0.8,
topK: 40,
minP: 0.05,
repetitionPenalty: 1.1,
presencePenalty: 0.4,
maxSeqLength: 16384,
maxTokens: 2048,
systemPrompt: "hello",
checkpoint: "foo/bar",
trustRemoteCode: true,
}});
console.log(JSON.stringify({{
checkpoint: sanitized.checkpoint,
trustRemoteCode: sanitized.trustRemoteCode,
maxSeqLength: sanitized.maxSeqLength,
maxTokens: sanitized.maxTokens,
systemPrompt: sanitized.systemPrompt,
}}));
"""
)
)
assert out["checkpoint"] == ""
assert out["trustRemoteCode"] is False
assert out["maxSeqLength"] == 4096
assert out["maxTokens"] == 2048
assert out["systemPrompt"] == "hello"
def test_apply_preset_params_preserves_model_owned_fields():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, applyPresetParams }} from "{_policy_path()}";
const creative = BUILTIN_PRESETS.find((p) => p.name === "Creative");
const applied = applyPresetParams(
{{
temperature: 0.6,
topP: 0.95,
topK: 20,
minP: 0.01,
repetitionPenalty: 1.0,
presencePenalty: 0.0,
maxSeqLength: 16384,
maxTokens: 8192,
systemPrompt: "keep me?",
checkpoint: "foo/bar",
trustRemoteCode: true,
}},
creative.params,
);
console.log(JSON.stringify({{
checkpoint: applied.checkpoint,
trustRemoteCode: applied.trustRemoteCode,
maxSeqLength: applied.maxSeqLength,
temperature: applied.temperature,
topK: applied.topK,
}}));
"""
)
)
assert out["checkpoint"] == "foo/bar"
assert out["trustRemoteCode"] is True
assert out["maxSeqLength"] == 16384
assert out["temperature"] == 1.5
assert out["topK"] == 0
def test_creative_and_precise_builtins_differ_from_default():
out = _run(
textwrap.dedent(
f"""
// @ts-nocheck
import {{ BUILTIN_PRESETS, isSamePresetConfig }} from "{_policy_path()}";
const def = BUILTIN_PRESETS.find((p) => p.name === "Default");
const creative = BUILTIN_PRESETS.find((p) => p.name === "Creative");
const precise = BUILTIN_PRESETS.find((p) => p.name === "Precise");
console.log(JSON.stringify({{
creativeDiffers: !isSamePresetConfig(def.params, creative.params),
preciseDiffers: !isSamePresetConfig(def.params, precise.params),
}}));
"""
)
)
assert out["creativeDiffers"] is True
assert out["preciseDiffers"] is True