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:
parent
2469ac885b
commit
975a5c354f
6 changed files with 1040 additions and 465 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
351
studio/frontend/src/features/chat/presets/preset-policy.ts
Normal file
351
studio/frontend/src/features/chat/presets/preset-policy.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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) =>
|
||||
|
|
|
|||
272
tests/studio/test_chat_preset_builtin_invariants.py
Normal file
272
tests/studio/test_chat_preset_builtin_invariants.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue