mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
fix(models): support atomic preset renames
This commit is contained in:
@@ -3308,7 +3308,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
"settings.model_configuration.update",
|
||||
{
|
||||
"name": "fast-writing",
|
||||
"label": "Codex",
|
||||
"new_name": "Codex",
|
||||
"provider": "openai",
|
||||
"model": "openai/gpt-5.5",
|
||||
},
|
||||
@@ -3320,24 +3320,24 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
updated_presets = {
|
||||
preset["name"]: preset for preset in updated_preset_body["model_presets"]
|
||||
}
|
||||
assert updated_presets["fast-writing"]["label"] == "fast-writing"
|
||||
assert updated_presets["Codex"]["label"] == "Codex"
|
||||
|
||||
call_order_updated = await _webui_mutate(
|
||||
webui_client,
|
||||
"settings.model_call_order.update",
|
||||
{"order": ["fast-writing", "deep"]},
|
||||
{"order": ["Codex", "deep"]},
|
||||
)
|
||||
assert call_order_updated.status_code == 200
|
||||
call_order_body = call_order_updated.json()
|
||||
assert call_order_body["agent"]["model_preset"] == "fast-writing"
|
||||
assert call_order_body["agent"]["model_preset"] == "Codex"
|
||||
assert call_order_body["agent"]["model"] == "openai/gpt-5.5"
|
||||
assert call_order_body["model_call_order"] == ["fast-writing", "deep"]
|
||||
assert call_order_body["model_call_order"] == ["Codex", "deep"]
|
||||
|
||||
duplicate_preset = await _webui_mutate(
|
||||
webui_client,
|
||||
"settings.model_configuration.create",
|
||||
{
|
||||
"label": "Fast writing",
|
||||
"name": "codex",
|
||||
"provider": "openai",
|
||||
"model": "openai/gpt-4.1-mini",
|
||||
},
|
||||
@@ -3435,10 +3435,10 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
saved = load_config(config_path)
|
||||
assert saved.agents.defaults.model == "atomic_chat/test"
|
||||
assert saved.agents.defaults.provider == "atomic_chat"
|
||||
assert saved.agents.defaults.model_preset == "fast-writing"
|
||||
assert saved.agents.defaults.model_preset == "Codex"
|
||||
assert saved.agents.defaults.fallback_models == ["deep"]
|
||||
assert saved.model_presets["fast-writing"].model == "openai/gpt-5.5"
|
||||
assert saved.model_presets["fast-writing"].provider == "openai"
|
||||
assert saved.model_presets["Codex"].model == "openai/gpt-5.5"
|
||||
assert saved.model_presets["Codex"].provider == "openai"
|
||||
assert saved.agents.defaults.timezone == "Asia/Shanghai"
|
||||
assert saved.agents.defaults.bot_name == "nanobot"
|
||||
assert saved.agents.defaults.bot_icon == "🐈"
|
||||
|
||||
@@ -792,9 +792,40 @@ def _model_configuration_name(value: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _model_configuration_name_exists(config: Config, name: str) -> bool:
|
||||
def _model_configuration_name_exists(
|
||||
config: Config,
|
||||
name: str,
|
||||
*,
|
||||
exclude: str | None = None,
|
||||
) -> bool:
|
||||
normalized = name.casefold()
|
||||
return any(existing.casefold() == normalized for existing in config.model_presets)
|
||||
return any(
|
||||
existing != exclude and existing.casefold() == normalized
|
||||
for existing in config.model_presets
|
||||
)
|
||||
|
||||
|
||||
def _rename_model_configuration(config: Config, old_name: str, new_name: str) -> bool:
|
||||
"""Rename one preset and every config reference to it."""
|
||||
if old_name == new_name:
|
||||
return False
|
||||
if _model_configuration_name_exists(config, new_name, exclude=old_name):
|
||||
raise WebUISettingsError("configuration already exists", status=409)
|
||||
|
||||
config.model_presets = {
|
||||
(new_name if name == old_name else name): preset
|
||||
for name, preset in config.model_presets.items()
|
||||
}
|
||||
defaults = config.agents.defaults
|
||||
if defaults.model_preset == old_name:
|
||||
defaults.model_preset = new_name
|
||||
defaults.fallback_models = [
|
||||
new_name if fallback == old_name else fallback
|
||||
for fallback in defaults.fallback_models
|
||||
]
|
||||
if defaults.dream.model_override == old_name:
|
||||
defaults.dream.model_override = new_name
|
||||
return True
|
||||
|
||||
|
||||
def _custom_provider_key(config: Config, display_name: str) -> str:
|
||||
@@ -1139,6 +1170,13 @@ def update_model_configuration(
|
||||
raise WebUISettingsError("unknown model configuration")
|
||||
|
||||
changed = False
|
||||
new_name_value = query_first_alias(query, "new_name", "newName")
|
||||
if new_name_value is not None:
|
||||
new_name = _model_configuration_name(new_name_value)
|
||||
changed = _rename_model_configuration(config, name, new_name) or changed
|
||||
name = new_name
|
||||
preset = config.model_presets[name]
|
||||
|
||||
model = query_first(query, "model")
|
||||
if model is not None:
|
||||
model = model.strip()
|
||||
|
||||
@@ -418,6 +418,61 @@ def test_update_model_configuration_edits_named_preset_without_selecting(
|
||||
assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"
|
||||
|
||||
|
||||
def test_update_model_configuration_renames_preset_and_config_references(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.model_presets = {
|
||||
"openai": ModelPresetConfig(model="openai/gpt-4.1"),
|
||||
"backup": ModelPresetConfig(model="anthropic/claude-sonnet-4"),
|
||||
}
|
||||
defaults = config.agents.defaults
|
||||
defaults.model_preset = "openai"
|
||||
defaults.fallback_models = ["backup", "openai"]
|
||||
defaults.dream.model_override = "openai"
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
payload = update_model_configuration(
|
||||
{"name": ["openai"], "new_name": ["Codex"]}
|
||||
)
|
||||
|
||||
assert payload["agent"]["model_preset"] == "Codex"
|
||||
assert payload["model_call_order"] == ["Codex", "backup", "Codex"]
|
||||
assert [row["name"] for row in payload["model_presets"]] == [
|
||||
"default",
|
||||
"Codex",
|
||||
"backup",
|
||||
]
|
||||
saved = load_config(config_path)
|
||||
assert list(saved.model_presets) == ["Codex", "backup"]
|
||||
assert saved.agents.defaults.model_preset == "Codex"
|
||||
assert saved.agents.defaults.fallback_models == ["backup", "Codex"]
|
||||
assert saved.agents.defaults.dream.model_override == "Codex"
|
||||
|
||||
|
||||
def test_update_model_configuration_rejects_duplicate_rename(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.model_presets = {
|
||||
"openai": ModelPresetConfig(model="openai/gpt-4.1"),
|
||||
"Codex": ModelPresetConfig(model="openai/gpt-5.5"),
|
||||
}
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
with pytest.raises(WebUISettingsError) as duplicate:
|
||||
update_model_configuration({"name": ["openai"], "new_name": ["codex"]})
|
||||
|
||||
assert duplicate.value.status == 409
|
||||
assert set(load_config(config_path).model_presets) == {"openai", "Codex"}
|
||||
|
||||
|
||||
def test_settings_payload_exposes_named_model_call_order(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -136,6 +136,7 @@ export function SettingsPage({
|
||||
modelMigrationSaving,
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetPendingDelete,
|
||||
nanobotFeatureAction,
|
||||
nanobotFeatureConfirm,
|
||||
@@ -185,6 +186,7 @@ export function SettingsPage({
|
||||
setMcpOAuthCallbackError,
|
||||
setMcpOAuthCallbackUrl,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetPendingDelete,
|
||||
setNanobotFeatureConfirm,
|
||||
setNanobotFeatures,
|
||||
@@ -240,6 +242,7 @@ export function SettingsPage({
|
||||
token={token}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
editingPresetName={modelPresetEditingName}
|
||||
settings={settings}
|
||||
dirty={modelDirty}
|
||||
creating={modelPresetCreating}
|
||||
@@ -256,8 +259,9 @@ export function SettingsPage({
|
||||
onMigrate={handleMigrateModelConfigurations}
|
||||
onBeginCreate={beginModelPresetCreation}
|
||||
onCancelCreate={cancelModelPresetCreation}
|
||||
onSelectConfiguration={() => {
|
||||
onSelectConfiguration={(name) => {
|
||||
setModelPresetCreating(false);
|
||||
setModelPresetEditingName(name);
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
}}
|
||||
onDeleteConfiguration={setModelPresetPendingDelete}
|
||||
|
||||
@@ -159,6 +159,7 @@ export function ModelsSettings({
|
||||
token,
|
||||
form,
|
||||
setForm,
|
||||
editingPresetName,
|
||||
settings,
|
||||
dirty,
|
||||
creating,
|
||||
@@ -181,6 +182,7 @@ export function ModelsSettings({
|
||||
token: string;
|
||||
form: AgentSettingsDraft;
|
||||
setForm: Dispatch<SetStateAction<AgentSettingsDraft>>;
|
||||
editingPresetName: string;
|
||||
settings: SettingsPayload;
|
||||
dirty: boolean;
|
||||
creating: boolean;
|
||||
@@ -197,7 +199,7 @@ export function ModelsSettings({
|
||||
onMigrate: () => void;
|
||||
onBeginCreate: () => void;
|
||||
onCancelCreate: () => void;
|
||||
onSelectConfiguration: () => void;
|
||||
onSelectConfiguration: (name: string) => void;
|
||||
onDeleteConfiguration: (preset: SettingsPayload["model_presets"][number]) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -230,7 +232,7 @@ export function ModelsSettings({
|
||||
preset,
|
||||
})),
|
||||
];
|
||||
const selectedPreset = namedPresetsByName.get(form.modelPreset) ?? null;
|
||||
const selectedPreset = namedPresetsByName.get(editingPresetName) ?? null;
|
||||
const activeEditorRowKey =
|
||||
editorRowKey ??
|
||||
presetRows.find((row) => row.name === selectedPreset?.name)?.key ??
|
||||
@@ -263,7 +265,7 @@ export function ModelsSettings({
|
||||
const modelFieldsMissing =
|
||||
!form.model.trim() ||
|
||||
!form.provider.trim() ||
|
||||
(creating && !form.modelPreset.trim()) ||
|
||||
!form.modelPreset.trim() ||
|
||||
form.maxTokens <= 0 ||
|
||||
form.temperature < 0 ||
|
||||
form.temperature > 2;
|
||||
@@ -277,7 +279,7 @@ export function ModelsSettings({
|
||||
) => {
|
||||
const toggleCurrentPreset =
|
||||
!creating && selectedPreset?.name === preset.name && activeEditorRowKey === rowKey;
|
||||
onSelectConfiguration();
|
||||
onSelectConfiguration(preset.name);
|
||||
if (toggleCurrentPreset) {
|
||||
setEditorOpen((open) => !open);
|
||||
return;
|
||||
@@ -348,28 +350,21 @@ export function ModelsSettings({
|
||||
) : null}
|
||||
<SettingsRow
|
||||
title={tx("settings.models.presetName", "Preset name")}
|
||||
description={
|
||||
creating
|
||||
? tx(
|
||||
"settings.models.presetNameHelp",
|
||||
"Used everywhere, including /model commands. Names must be unique.",
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{creating ? (
|
||||
<Input
|
||||
autoFocus
|
||||
value={form.modelPreset}
|
||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
||||
onChange={(event) =>
|
||||
setForm((prev) => ({ ...prev, modelPreset: event.target.value }))
|
||||
}
|
||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[13px] font-medium text-foreground">{form.modelPreset}</span>
|
||||
description={tx(
|
||||
"settings.models.presetNameHelp",
|
||||
"Used everywhere, including /model commands. Names must be unique.",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
autoFocus={creating}
|
||||
aria-label={tx("settings.models.presetName", "Preset name")}
|
||||
value={form.modelPreset}
|
||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
||||
onChange={(event) =>
|
||||
setForm((prev) => ({ ...prev, modelPreset: event.target.value }))
|
||||
}
|
||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow title={t("settings.rows.provider")}>
|
||||
<ProviderPicker
|
||||
|
||||
@@ -90,6 +90,7 @@ export function useModelSettingsActions({
|
||||
modelMigrationSaving,
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetPendingDelete,
|
||||
providerForms,
|
||||
providerOAuthCompleting,
|
||||
@@ -105,6 +106,7 @@ export function useModelSettingsActions({
|
||||
setModelConfigurationSaving,
|
||||
setModelMigrationSaving,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms,
|
||||
setProviderOAuthCompleting,
|
||||
@@ -158,6 +160,7 @@ export function useModelSettingsActions({
|
||||
applyPayload(payload);
|
||||
if (createdPreset) {
|
||||
setForm(agentDraftFromPayload(payload, createdPreset));
|
||||
setModelPresetEditingName(createdPreset);
|
||||
}
|
||||
|
||||
let finalPayload = payload;
|
||||
@@ -168,6 +171,7 @@ export function useModelSettingsActions({
|
||||
}
|
||||
if (createdPreset) {
|
||||
setForm(agentDraftFromPayload(finalPayload, createdPreset));
|
||||
setModelPresetEditingName(createdPreset);
|
||||
}
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
onModelNameChange(finalPayload.agent.model || null);
|
||||
@@ -182,14 +186,16 @@ export function useModelSettingsActions({
|
||||
|
||||
if (!modelDirty) return;
|
||||
const selectedPreset = settings.model_presets.find(
|
||||
(preset) => !preset.is_default && preset.name === form.modelPreset,
|
||||
(preset) => !preset.is_default && preset.name === modelPresetEditingName,
|
||||
);
|
||||
if (!selectedPreset) return;
|
||||
const nextName = form.modelPreset.trim();
|
||||
const reasoningEffort = form.reasoningEffort || null;
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = await updateModelConfiguration(client, {
|
||||
name: selectedPreset.name,
|
||||
newName: nextName !== selectedPreset.name ? nextName : undefined,
|
||||
model: form.model !== selectedPreset.model ? form.model : undefined,
|
||||
provider: form.provider !== selectedPreset.provider ? form.provider : undefined,
|
||||
maxTokens:
|
||||
@@ -205,7 +211,8 @@ export function useModelSettingsActions({
|
||||
reasoningEffort !== selectedPreset.reasoning_effort ? reasoningEffort : undefined,
|
||||
});
|
||||
applyPayload(payload);
|
||||
setForm(agentDraftFromPayload(payload, selectedPreset.name));
|
||||
setForm(agentDraftFromPayload(payload, nextName));
|
||||
setModelPresetEditingName(nextName);
|
||||
onModelNameChange(payload.agent.model || null);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
@@ -227,7 +234,7 @@ export function useModelSettingsActions({
|
||||
configuredModelProviderOptions.find((option) => option.name === currentProvider)?.name ??
|
||||
configuredModelProviderOptions[0]?.name ??
|
||||
"";
|
||||
modelPresetBeforeCreateRef.current = form.modelPreset;
|
||||
modelPresetBeforeCreateRef.current = modelPresetEditingName;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
modelPreset: "",
|
||||
@@ -248,6 +255,7 @@ export function useModelSettingsActions({
|
||||
const previousPreset = modelPresetBeforeCreateRef.current;
|
||||
setModelPresetCreating(false);
|
||||
setForm(agentDraftFromPayload(settings, previousPreset ?? undefined));
|
||||
setModelPresetEditingName(previousPreset ?? agentDraftFromPayload(settings).modelPreset);
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import type { ProviderForm } from "@/components/settings/models/ProviderSettings
|
||||
import type { ProviderOAuthAuthorizationRequired, SettingsPayload } from "@/lib/types";
|
||||
|
||||
export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
const initialForm = initialSettings
|
||||
? agentDraftFromPayload(initialSettings)
|
||||
: DEFAULT_AGENT_SETTINGS_DRAFT;
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [modelPresetCreating, setModelPresetCreating] = useState(false);
|
||||
const [modelConfigurationSaving, setModelConfigurationSaving] = useState(false);
|
||||
@@ -28,8 +31,9 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
const [providerForms, setProviderForms] = useState<Record<string, ProviderForm>>({});
|
||||
const [visibleProviderKeys, setVisibleProviderKeys] = useState<Record<string, boolean>>({});
|
||||
const [editingProviderKeys, setEditingProviderKeys] = useState<Record<string, boolean>>({});
|
||||
const [form, setForm] = useState<AgentSettingsDraft>(() =>
|
||||
initialSettings ? agentDraftFromPayload(initialSettings) : DEFAULT_AGENT_SETTINGS_DRAFT,
|
||||
const [form, setForm] = useState<AgentSettingsDraft>(initialForm);
|
||||
const [modelPresetEditingName, setModelPresetEditingName] = useState(
|
||||
initialForm.modelPreset,
|
||||
);
|
||||
const [modelCallOrder, setModelCallOrder] = useState<string[]>(
|
||||
() => initialSettings?.model_call_order ?? [],
|
||||
@@ -45,6 +49,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
modelMigrationSaving,
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetPendingDelete,
|
||||
providerForms,
|
||||
providerOAuthCompleting,
|
||||
@@ -62,6 +67,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
setModelConfigurationSaving,
|
||||
setModelMigrationSaving,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms,
|
||||
setProviderOAuthCompleting,
|
||||
|
||||
@@ -93,10 +93,12 @@ export function useSettingsController({
|
||||
const {
|
||||
editingProviderKeys, expandedProvider, form, modelCallOrder, modelCallOrderSaving,
|
||||
modelConfigurationSaving, modelMigrationSaving, modelPresetBeforeCreateRef,
|
||||
modelPresetCreating, modelPresetPendingDelete, providerForms, providerOAuthCompleting,
|
||||
modelPresetCreating, modelPresetEditingName, modelPresetPendingDelete,
|
||||
providerForms, providerOAuthCompleting,
|
||||
providerOAuthDialogError, providerOAuthFlow, providerOAuthFlowRef, providerOAuthResponse,
|
||||
providerSaving, saving, setForm,
|
||||
setModelCallOrder, setModelPresetCreating, setModelPresetPendingDelete,
|
||||
setModelCallOrder, setModelPresetCreating, setModelPresetEditingName,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms, setProviderOAuthCompleting, setProviderOAuthDialogError,
|
||||
setProviderOAuthFlow, setProviderOAuthResponse, visibleProviderKeys,
|
||||
} = modelState;
|
||||
@@ -147,7 +149,9 @@ export function useSettingsController({
|
||||
) => {
|
||||
setSettings(payload);
|
||||
if (!options.preserveAgentForm) {
|
||||
setForm(agentDraftFromPayload(payload));
|
||||
const nextForm = agentDraftFromPayload(payload);
|
||||
setForm(nextForm);
|
||||
setModelPresetEditingName(nextForm.modelPreset);
|
||||
setModelPresetCreating(false);
|
||||
}
|
||||
setModelCallOrder(payload.model_call_order ?? []);
|
||||
@@ -252,10 +256,11 @@ export function useSettingsController({
|
||||
const modelDirty = useMemo(() => {
|
||||
if (!settings) return false;
|
||||
const selectedPreset = settings.model_presets.find(
|
||||
(preset) => !preset.is_default && preset.name === form.modelPreset,
|
||||
(preset) => !preset.is_default && preset.name === modelPresetEditingName,
|
||||
);
|
||||
if (!selectedPreset) return false;
|
||||
return (
|
||||
form.modelPreset.trim() !== selectedPreset.name ||
|
||||
form.model !== selectedPreset.model ||
|
||||
form.provider !== selectedPreset.provider ||
|
||||
form.maxTokens !== selectedPreset.max_tokens ||
|
||||
@@ -263,7 +268,7 @@ export function useSettingsController({
|
||||
form.temperature !== selectedPreset.temperature ||
|
||||
form.reasoningEffort !== (selectedPreset.reasoning_effort ?? "")
|
||||
);
|
||||
}, [form, settings]);
|
||||
}, [form, modelPresetEditingName, settings]);
|
||||
|
||||
const imageGenerationDirty = useMemo(() => {
|
||||
if (!settings) return false;
|
||||
@@ -532,6 +537,7 @@ export function useSettingsController({
|
||||
modelMigrationSaving,
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetPendingDelete,
|
||||
nanobotFeatureAction,
|
||||
nanobotFeatureConfirm,
|
||||
@@ -581,6 +587,7 @@ export function useSettingsController({
|
||||
setMcpOAuthCallbackError,
|
||||
setMcpOAuthCallbackUrl,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetPendingDelete,
|
||||
setNanobotFeatureConfirm,
|
||||
setNanobotFeatures,
|
||||
|
||||
@@ -927,6 +927,7 @@ export async function updateModelConfiguration(
|
||||
"settings.model_configuration.update",
|
||||
{
|
||||
name: configuration.name,
|
||||
...(configuration.newName !== undefined ? { new_name: configuration.newName } : {}),
|
||||
...(configuration.provider !== undefined ? { provider: configuration.provider } : {}),
|
||||
...(configuration.model !== undefined ? { model: configuration.model } : {}),
|
||||
...modelGenerationSettingsPayload(configuration),
|
||||
|
||||
@@ -1097,6 +1097,7 @@ export interface ModelConfigurationCreate {
|
||||
|
||||
export interface ModelConfigurationUpdate {
|
||||
name: string;
|
||||
newName?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
maxTokens?: number;
|
||||
|
||||
@@ -466,6 +466,7 @@ describe("webui API helpers", () => {
|
||||
it("serializes model configuration updates", async () => {
|
||||
await updateModelConfiguration(mutationTransport, {
|
||||
name: "codex",
|
||||
newName: "Codex",
|
||||
provider: "openai_codex",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
maxTokens: 8192,
|
||||
@@ -478,6 +479,7 @@ describe("webui API helpers", () => {
|
||||
"settings.model_configuration.update",
|
||||
{
|
||||
name: "codex",
|
||||
new_name: "Codex",
|
||||
provider: "openai_codex",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
max_tokens: 8192,
|
||||
|
||||
@@ -106,10 +106,45 @@ describe("Settings models", () => {
|
||||
|
||||
const editor = screen.getByTestId("model-preset-editor");
|
||||
expect(within(editor).getByText("Preset name")).toBeInTheDocument();
|
||||
expect(within(editor).getByText("openai")).toBeInTheDocument();
|
||||
expect(within(editor).getByRole("textbox", { name: "Preset name" })).toHaveValue(
|
||||
"openai",
|
||||
);
|
||||
expect(within(editor).queryByText("minimax")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renames an existing preset without losing the editor selection", async () => {
|
||||
const payload = settingsPayload();
|
||||
const renamedPayload: SettingsPayload = {
|
||||
...payload,
|
||||
agent: { ...payload.agent, model_preset: "Codex" },
|
||||
model_presets: payload.model_presets.map((preset) => ({
|
||||
...preset,
|
||||
name: "Codex",
|
||||
label: "Codex",
|
||||
})),
|
||||
model_call_order: ["Codex"],
|
||||
};
|
||||
vi.stubGlobal("fetch", vi.fn(() => new Promise<Response>(() => {})));
|
||||
requestMutationMock.mockResolvedValueOnce(renamedPayload);
|
||||
|
||||
renderSettingsView({ initialSection: "models", initialSettings: payload });
|
||||
await togglePresetEditor();
|
||||
|
||||
const nameInput = screen.getByRole("textbox", { name: "Preset name" });
|
||||
fireEvent.change(nameInput, { target: { value: "Codex" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save preset" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestMutationMock).toHaveBeenCalledWith(
|
||||
"settings.model_configuration.update",
|
||||
{ name: "primary", new_name: "Codex" },
|
||||
20_000,
|
||||
);
|
||||
});
|
||||
expect(await screen.findByTestId("model-call-order-row-Codex")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox", { name: "Preset name" })).toHaveValue("Codex");
|
||||
});
|
||||
|
||||
it("keeps generation parameters collapsed until advanced options are opened", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -187,7 +222,9 @@ describe("Settings models", () => {
|
||||
"lg:max-w-6xl",
|
||||
"rounded-floating",
|
||||
);
|
||||
expect(within(editor).getByText("primary")).toBeInTheDocument();
|
||||
expect(within(editor).getByRole("textbox", { name: "Preset name" })).toHaveValue(
|
||||
"primary",
|
||||
);
|
||||
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
expect(deleteButton).toHaveAttribute("aria-describedby", "model-preset-delete-hint");
|
||||
|
||||
Reference in New Issue
Block a user