From e0337410e7f9a72cdee9e83dc15eb25b5e5cd5e7 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Mon, 18 May 2026 02:45:08 +0200 Subject: [PATCH] Preserve model preset inherited settings Deep-merge model preset slots with the active configuration so custom context windows, rate limits, and nested kwargs survive preset switches. Treat legacy utility preset defaults as implicit values, allow omitted utility and embedding slots to inherit configured models, and document the partial-preset behavior. --- docs/guides/model-presets.md | 5 + plugins/_browser/helpers/config.py | 11 +- plugins/_model_config/README.md | 5 +- plugins/_model_config/default_presets.yaml | 12 -- plugins/_model_config/helpers/model_config.py | 184 ++++++++++++++-- plugins/_model_config/webui/main.html | 4 +- .../_model_config/webui/model-config-store.js | 106 +++++++++- tests/test_model_config_project_presets.py | 200 ++++++++++++++++++ webui/components/projects/projects-store.js | 10 +- 9 files changed, 487 insertions(+), 50 deletions(-) diff --git a/docs/guides/model-presets.md b/docs/guides/model-presets.md index a809318f6..18f3bc0f2 100644 --- a/docs/guides/model-presets.md +++ b/docs/guides/model-presets.md @@ -39,6 +39,11 @@ Think of a preset as a label on a model setup. | **Main model** | The model that does the main conversation and reasoning. | | **Utility model** | A smaller helper model for lighter internal tasks. | +Presets can be partial. If a preset does not set a utility model, Agent Zero +uses your configured Utility Model. If a preset changes the utility model but +does not set advanced fields, your configured context window, rate limits, and +other advanced values stay in place. + ## Add A Preset Click **Add Preset**, give it a name, choose models, then click **Save Presets**. diff --git a/plugins/_browser/helpers/config.py b/plugins/_browser/helpers/config.py index bf19f08c7..f39e52e51 100644 --- a/plugins/_browser/helpers/config.py +++ b/plugins/_browser/helpers/config.py @@ -223,7 +223,16 @@ def resolve_browser_model_selection( if preset_name: preset = model_config.get_preset_by_name(preset_name) if isinstance(preset, dict): - chat_cfg = preset.get("chat", {}) + if hasattr(model_config, "build_config_from_preset"): + preset_config = model_config.build_config_from_preset( + preset, + model_config.get_config(agent) if hasattr(model_config, "get_config") else {}, + strip_api_key=False, + slots=("chat",), + ) + chat_cfg = preset_config.get("chat_model", {}) + else: + chat_cfg = preset.get("chat", {}) if isinstance(chat_cfg, dict) and ( str(chat_cfg.get("provider", "") or "").strip() or str(chat_cfg.get("name", "") or "").strip() diff --git a/plugins/_model_config/README.md b/plugins/_model_config/README.md index d95fa653d..b69837f37 100644 --- a/plugins/_model_config/README.md +++ b/plugins/_model_config/README.md @@ -75,12 +75,9 @@ The project preset file uses the same plain YAML list schema as global presets. utility: provider: openrouter name: openai/gpt-5.4-mini - api_base: "" - ctx_length: 128000 - ctx_input: 0.7 ``` -Selecting a preset for a project copies the preset's `chat` and optional `utility` settings into the project's `config.json`. The embedding model is copied from the current effective config, because presets currently define chat and utility only. +Preset slots are partial overlays. Missing fields inherit from the current effective config, so a preset can switch only the model identity while preserving tuned context windows, rate limits, and nested `kwargs`. The `utility` and `embedding` slots are optional and only apply when they declare a provider or model name; otherwise those configured models are inherited. Selecting a preset for a project writes the merged result into the project's `config.json`. ## Plugin Metadata diff --git a/plugins/_model_config/default_presets.yaml b/plugins/_model_config/default_presets.yaml index 1d20b6232..be3dcabfd 100644 --- a/plugins/_model_config/default_presets.yaml +++ b/plugins/_model_config/default_presets.yaml @@ -10,10 +10,6 @@ utility: provider: "openrouter" name: "openai/gpt-5.4-mini" - api_key: "" - api_base: "" - ctx_length: 128000 - ctx_input: 0.7 - name: "Balance" chat: provider: "openrouter" @@ -26,10 +22,6 @@ utility: provider: "openrouter" name: "google/gemini-3.1-flash-lite-preview" - api_key: "" - api_base: "" - ctx_length: 128000 - ctx_input: 0.7 - name: "Cost Efficient" chat: provider: "openrouter" @@ -42,7 +34,3 @@ utility: provider: "openrouter" name: "openai/gpt-5.4-nano" - api_key: "" - api_base: "" - ctx_length: 128000 - ctx_input: 0.7 diff --git a/plugins/_model_config/helpers/model_config.py b/plugins/_model_config/helpers/model_config.py index 30a7c731f..e01430304 100644 --- a/plugins/_model_config/helpers/model_config.py +++ b/plugins/_model_config/helpers/model_config.py @@ -11,6 +11,26 @@ DEFAULT_PRESETS_FILE = "default_presets.yaml" PROVIDER_METADATA_FILE = "provider_metadata.yaml" PRESET_SCOPE_GLOBAL = "global" PRESET_SCOPE_PROJECT = "project" +PRESET_SLOT_CONFIG_SECTIONS = { + "chat": "chat_model", + "utility": "utility_model", + "embedding": "embedding_model", +} +IMPLICIT_PRESET_SLOT_DEFAULTS = { + "utility": { + "ctx_length": 128000, + "ctx_input": 0.7, + "rl_requests": 0, + "rl_input": 0, + "rl_output": 0, + "kwargs": {}, + }, + "embedding": { + "rl_requests": 0, + "rl_input": 0, + "kwargs": {}, + }, +} LOCAL_PROVIDERS = {"ollama", "lm_studio"} LOCAL_EMBEDDING = {"huggingface"} _PROVIDER_METADATA_CACHE: dict | None = None @@ -96,14 +116,33 @@ def _strip_ui_fields(value: dict, *, strip_api_key: bool) -> dict: return cleaned +def _preset_default_values_equal(value, default) -> bool: + if isinstance(default, float): + try: + return float(value) == default + except (TypeError, ValueError): + return False + return value == default + + +def _strip_implicit_preset_defaults(slot: str, slot_config: dict) -> dict: + cleaned = deepcopy(slot_config) + defaults = IMPLICIT_PRESET_SLOT_DEFAULTS.get(slot, {}) + for key, default in defaults.items(): + if key in cleaned and _preset_default_values_equal(cleaned[key], default): + cleaned.pop(key, None) + return cleaned + + def _clean_preset_for_file(preset: dict) -> dict: cleaned = { "name": str(preset.get("name", "") or ""), } - for slot in ("chat", "utility"): + for slot in PRESET_SLOT_CONFIG_SECTIONS: slot_config = preset.get(slot) if isinstance(slot_config, dict): - cleaned[slot] = _strip_ui_fields(slot_config, strip_api_key=False) + slot_clean = _strip_ui_fields(slot_config, strip_api_key=False) + cleaned[slot] = _strip_implicit_preset_defaults(slot, slot_clean) return cleaned @@ -230,17 +269,115 @@ def get_preset_by_name( return resolve_preset(name, scope=scope, project_name=project_name) -def build_config_from_preset(preset: dict, base_config: dict) -> dict: - """Copy chat/utility settings from a preset into a standalone model config.""" - config = normalize_config_for_save(base_config) +def _deep_merge_dict(base: dict, override: dict) -> dict: + """Recursively overlay override onto base without mutating either input.""" + result = deepcopy(base) if isinstance(base, dict) else {} + for key, value in override.items(): + if ( + isinstance(value, dict) + and isinstance(result.get(key), dict) + ): + result[key] = _deep_merge_dict(result[key], value) + else: + result[key] = deepcopy(value) + return result - chat = preset.get("chat") if isinstance(preset, dict) else None - if isinstance(chat, dict): - config["chat_model"] = _strip_ui_fields(chat, strip_api_key=True) - utility = preset.get("utility") if isinstance(preset, dict) else None - if isinstance(utility, dict) and (utility.get("provider") or utility.get("name")): - config["utility_model"] = _strip_ui_fields(utility, strip_api_key=True) +def _slot_has_identity(slot_config: dict) -> bool: + return bool(slot_config.get("provider") or slot_config.get("name")) + + +def _get_preset_slot_config(preset: dict, slot: str) -> dict | None: + """Return the preset payload for a slot. + + Legacy raw overrides store the main/chat model directly at the top level, + while named presets store it under the "chat" key. + """ + if not isinstance(preset, dict): + return None + + slot_config = preset.get(slot) + if isinstance(slot_config, dict): + return slot_config + + if slot == "chat" and not any(key in preset for key in PRESET_SLOT_CONFIG_SECTIONS): + if _slot_has_identity(preset): + return preset + + return None + + +def _should_apply_preset_slot(slot: str, slot_config: dict | None) -> bool: + if not isinstance(slot_config, dict): + return False + + cleaned = _strip_implicit_preset_defaults( + slot, + _strip_ui_fields(slot_config, strip_api_key=False), + ) + meaningful = { + key: value + for key, value in cleaned.items() + if key != "api_key" + } + if not meaningful: + return False + + # Slots inherit the configured model unless the preset declares a model + # identity for that slot. This keeps empty UI placeholders from accidentally + # overriding context/rate-limit settings. + return _slot_has_identity(cleaned) + + +def _merge_model_slot( + slot: str, + base_slot: dict, + preset_slot: dict, + *, + strip_api_key: bool, +) -> dict: + cleaned = _strip_implicit_preset_defaults( + slot, + _strip_ui_fields(preset_slot, strip_api_key=strip_api_key), + ) + if not strip_api_key and not str(cleaned.get("api_key") or "").strip(): + cleaned.pop("api_key", None) + return _deep_merge_dict(base_slot if isinstance(base_slot, dict) else {}, cleaned) + + +def build_config_from_preset( + preset: dict, + base_config: dict, + *, + strip_api_key: bool = True, + slots: tuple[str, ...] | None = None, +) -> dict: + """Overlay preset settings onto a standalone model config. + + Presets are intentionally partial: omitted fields inherit from the current + config, so selecting a preset does not reset tuned values such as context + windows, rate limits, or nested kwargs unless the preset explicitly defines + them. + """ + config = ( + normalize_config_for_save(base_config) + if strip_api_key + else deepcopy(base_config or {}) + ) + + for slot in slots or tuple(PRESET_SLOT_CONFIG_SECTIONS): + section = PRESET_SLOT_CONFIG_SECTIONS.get(slot) + if not section: + continue + slot_config = _get_preset_slot_config(preset, slot) + if not _should_apply_preset_slot(slot, slot_config): + continue + config[section] = _merge_model_slot( + slot, + config.get(section, {}), + slot_config, + strip_api_key=strip_api_key, + ) return config @@ -269,24 +406,31 @@ def _resolve_override(agent) -> dict | None: def get_chat_model_config(agent=None) -> dict: """Get chat model config, with per-chat override if active.""" + cfg = get_config(agent) override = _resolve_override(agent) if override: - # Preset has a nested 'chat' key; raw override is flat - chat_cfg = override.get("chat", override) - if chat_cfg.get("provider") or chat_cfg.get("name"): - return chat_cfg - cfg = get_config(agent) + config = build_config_from_preset( + override, + cfg, + strip_api_key=False, + slots=("chat",), + ) + return config.get("chat_model", {}) return cfg.get("chat_model", {}) def get_utility_model_config(agent=None) -> dict: """Get utility model config, with per-chat override if active.""" + cfg = get_config(agent) override = _resolve_override(agent) if override: - util_cfg = override.get("utility", {}) - if util_cfg.get("provider") or util_cfg.get("name"): - return util_cfg - cfg = get_config(agent) + config = build_config_from_preset( + override, + cfg, + strip_api_key=False, + slots=("utility",), + ) + return config.get("utility_model", {}) return cfg.get("utility_model", {}) diff --git a/plugins/_model_config/webui/main.html b/plugins/_model_config/webui/main.html index b1b988b53..a7ba13605 100644 --- a/plugins/_model_config/webui/main.html +++ b/plugins/_model_config/webui/main.html @@ -65,8 +65,8 @@ @click=" presets = [...presets, { name: 'Preset ' + (presets.length + 1), - chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: '' }, - utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: '' } + chat: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' }, + utility: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' } }]"> add Add Preset diff --git a/plugins/_model_config/webui/model-config-store.js b/plugins/_model_config/webui/model-config-store.js index 268952a34..183a4b781 100644 --- a/plugins/_model_config/webui/model-config-store.js +++ b/plugins/_model_config/webui/model-config-store.js @@ -46,6 +46,99 @@ export function textToHeaders(text) { return d; } +function clonePlain(value) { + if (value === undefined) return undefined; + return JSON.parse(JSON.stringify(value)); +} + +function isBlankPresetValue(value) { + if (value === undefined || value === null || value === '') return true; + if (Array.isArray(value)) return value.length === 0; + if (typeof value === 'object') return Object.keys(value).length === 0; + return false; +} + +const IMPLICIT_PRESET_SLOT_DEFAULTS = { + utility: { + ctx_length: 128000, + ctx_input: 0.7, + rl_requests: 0, + rl_input: 0, + rl_output: 0, + kwargs: {}, + }, + embedding: { + rl_requests: 0, + rl_input: 0, + kwargs: {}, + }, +}; + +function presetDefaultValuesEqual(value, defaultValue) { + if (typeof defaultValue === 'number') return Number(value) === defaultValue; + return JSON.stringify(value) === JSON.stringify(defaultValue); +} + +function cleanPresetSlot(slot, stripApiKey = true, slotKey = '') { + const clean = {}; + const implicitDefaults = IMPLICIT_PRESET_SLOT_DEFAULTS[slotKey] || {}; + for (const [key, value] of Object.entries(slot || {})) { + if (key.startsWith('_')) continue; + if (stripApiKey && key === 'api_key') continue; + if (key === 'api_base' && value === '') { + clean[key] = value; + continue; + } + if (key === 'kwargs' && isBlankPresetValue(value)) continue; + if (isBlankPresetValue(value)) continue; + if (key in implicitDefaults && presetDefaultValuesEqual(value, implicitDefaults[key])) continue; + clean[key] = value; + } + return clean; +} + +function hasModelIdentity(slot) { + return !!(slot?.provider || slot?.name); +} + +export function mergeModelSlot(baseSlot, presetSlot, stripApiKey = true, slotKey = '') { + const result = clonePlain(baseSlot || {}); + const clean = cleanPresetSlot(presetSlot, stripApiKey, slotKey); + for (const [key, value] of Object.entries(clean)) { + if ( + value && + typeof value === 'object' && + !Array.isArray(value) && + result[key] && + typeof result[key] === 'object' && + !Array.isArray(result[key]) + ) { + result[key] = mergeModelSlot(result[key], value, false); + } else { + result[key] = clonePlain(value); + } + } + return result; +} + +export function configFromPreset(preset, baseConfig, stripApiKey = true) { + const config = clonePlain(baseConfig || {}); + const slots = [ + ['chat', 'chat_model'], + ['utility', 'utility_model'], + ['embedding', 'embedding_model'], + ]; + + for (const [slotKey, sectionKey] of slots) { + const slot = preset?.[slotKey]; + if (!slot || typeof slot !== 'object') continue; + if (!hasModelIdentity(slot)) continue; + config[sectionKey] = mergeModelSlot(config[sectionKey] || {}, slot, stripApiKey, slotKey); + } + + return config; +} + // ── Alpine Store ── const API_BASE = "/plugins/_model_config"; @@ -87,8 +180,9 @@ export const store = createStore("modelConfig", { _normalizePresets(rawPresets) { return (rawPresets || []).map(p => ({ name: p.name || '', - chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: kwargsToText(p.chat?.kwargs), ...(p.chat || {}) }, - utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: kwargsToText(p.utility?.kwargs), ...(p.utility || {}) }, + chat: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: kwargsToText(p.chat?.kwargs), ...(p.chat || {}) }, + utility: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: kwargsToText(p.utility?.kwargs), ...(p.utility || {}) }, + embedding: p.embedding ? { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: kwargsToText(p.embedding?.kwargs), ...(p.embedding || {}) } : undefined, })); }, @@ -164,10 +258,14 @@ export const store = createStore("modelConfig", { const c = { name: p.name }; for (const slot of ['chat', 'utility']) { if (p[slot]) { - const { _kwargs_text, api_key, ...rest } = p[slot]; - c[slot] = rest; + const rest = cleanPresetSlot(p[slot], true, slot); + if (hasModelIdentity(rest)) c[slot] = rest; } } + if (p.embedding) { + const embedding = cleanPresetSlot(p.embedding, true, 'embedding'); + if (hasModelIdentity(embedding)) c.embedding = embedding; + } return c; }); try { diff --git a/tests/test_model_config_project_presets.py b/tests/test_model_config_project_presets.py index a0d1d1b81..18ce5128c 100644 --- a/tests/test_model_config_project_presets.py +++ b/tests/test_model_config_project_presets.py @@ -156,6 +156,18 @@ def test_project_presets_are_separate_and_resolve_by_scope(monkeypatch, tmp_path ) +def test_bundled_utility_presets_inherit_advanced_settings(): + import yaml + + presets_path = PROJECT_ROOT / "plugins" / "_model_config" / "default_presets.yaml" + presets = yaml.safe_load(presets_path.read_text(encoding="utf-8")) + + for preset in presets: + utility = preset.get("utility") or {} + assert "ctx_length" not in utility + assert "ctx_input" not in utility + + @pytest.mark.asyncio async def test_model_presets_api_returns_global_or_combined_by_project(monkeypatch, tmp_path): _prepare_a0_tree(monkeypatch, tmp_path) @@ -235,6 +247,194 @@ def test_project_save_copies_selected_preset_to_scoped_model_config(monkeypatch, assert "_model_config" not in project_json +def test_preset_application_deep_merges_model_slots(monkeypatch, tmp_path): + _prepare_a0_tree(monkeypatch, tmp_path) + + from plugins._model_config.helpers import model_config + + base_config = { + "allow_chat_override": True, + "chat_model": { + "provider": "openrouter", + "name": "configured-chat", + "ctx_length": 200000, + "ctx_history": 0.5, + "kwargs": {"temperature": 0.2, "routing": {"order": ["a", "b"]}}, + }, + "utility_model": { + "provider": "openrouter", + "name": "configured-utility", + "ctx_length": 200000, + "ctx_input": 0.4, + "kwargs": {"temperature": 0.1, "routing": {"order": ["fast"]}}, + }, + "embedding_model": { + "provider": "huggingface", + "name": "configured-embedding", + "kwargs": {"device": "cpu", "batch_size": 16}, + }, + } + preset = { + "name": "Research", + "chat": { + "provider": "anthropic", + "name": "claude-research", + "kwargs": {"routing": {"priority": "quality"}}, + }, + "utility": { + "provider": "openrouter", + "name": "utility-research", + "kwargs": {"routing": {"timeout": 30}}, + }, + "embedding": { + "provider": "openai", + "name": "text-embedding-3-large", + }, + } + + config = model_config.build_config_from_preset(preset, base_config) + + assert config["chat_model"]["name"] == "claude-research" + assert config["chat_model"]["ctx_length"] == 200000 + assert config["chat_model"]["kwargs"] == { + "temperature": 0.2, + "routing": {"order": ["a", "b"], "priority": "quality"}, + } + assert config["utility_model"]["name"] == "utility-research" + assert config["utility_model"]["ctx_length"] == 200000 + assert config["utility_model"]["ctx_input"] == 0.4 + assert config["utility_model"]["kwargs"] == { + "temperature": 0.1, + "routing": {"order": ["fast"], "timeout": 30}, + } + assert config["embedding_model"]["name"] == "text-embedding-3-large" + assert config["embedding_model"]["kwargs"] == {"device": "cpu", "batch_size": 16} + + +def test_preset_application_inherits_optional_slots(monkeypatch, tmp_path): + _prepare_a0_tree(monkeypatch, tmp_path) + + from plugins._model_config.helpers import model_config + + base_config = { + "chat_model": {"provider": "openrouter", "name": "configured-chat"}, + "utility_model": { + "provider": "openrouter", + "name": "configured-utility", + "ctx_length": 200000, + }, + "embedding_model": { + "provider": "huggingface", + "name": "configured-embedding", + }, + } + preset = { + "name": "Chat Only", + "chat": {"provider": "anthropic", "name": "claude-research"}, + "utility": {"ctx_length": 128000}, + } + + config = model_config.build_config_from_preset(preset, base_config) + + assert config["chat_model"]["name"] == "claude-research" + assert config["utility_model"] == base_config["utility_model"] + assert config["embedding_model"] == base_config["embedding_model"] + + +def test_legacy_utility_preset_defaults_do_not_override_tuned_config(monkeypatch, tmp_path): + _prepare_a0_tree(monkeypatch, tmp_path) + + from plugins._model_config.helpers import model_config + + base_config = { + "utility_model": { + "provider": "openrouter", + "name": "configured-utility", + "api_base": "https://custom.example/v1", + "ctx_length": 200000, + "ctx_input": 0.4, + "rl_requests": 12, + "rl_input": 34000, + "rl_output": 56000, + "kwargs": {"temperature": 0.1}, + }, + } + preset = { + "name": "Legacy Saved Preset", + "utility": { + "provider": "openrouter", + "name": "preset-utility", + "api_key": "", + "api_base": "", + "ctx_length": 128000, + "ctx_input": 0.7, + "rl_requests": 0, + "rl_input": 0, + "rl_output": 0, + "kwargs": {}, + }, + } + + config = model_config.build_config_from_preset( + preset, + base_config, + strip_api_key=False, + ) + + utility = config["utility_model"] + assert utility["name"] == "preset-utility" + assert utility["api_base"] == "" + assert "api_key" not in utility + assert utility["ctx_length"] == 200000 + assert utility["ctx_input"] == 0.4 + assert utility["rl_requests"] == 12 + assert utility["rl_input"] == 34000 + assert utility["rl_output"] == 56000 + assert utility["kwargs"] == {"temperature": 0.1} + + +def test_preset_override_preserves_configured_utility_context(monkeypatch, tmp_path): + _prepare_a0_tree(monkeypatch, tmp_path) + + from plugins._model_config.helpers import model_config + + base_config = { + "allow_chat_override": True, + "chat_model": {"provider": "openrouter", "name": "configured-chat"}, + "utility_model": { + "provider": "openrouter", + "name": "configured-utility", + "ctx_length": 200000, + "ctx_input": 0.4, + }, + } + preset = { + "name": "Fast", + "chat": {"provider": "openrouter", "name": "fast-chat"}, + "utility": {"provider": "openrouter", "name": "fast-utility"}, + } + + class FakeContext: + def get_data(self, key): + return {"preset_name": "Fast"} if key == "chat_model_override" else None + + class FakeAgent: + context = FakeContext() + + monkeypatch.setattr(model_config, "get_config", lambda *args, **kwargs: base_config) + monkeypatch.setattr( + model_config, + "get_preset_by_name", + lambda name, **kwargs: preset if name == "Fast" else None, + ) + + utility = model_config.get_utility_model_config(FakeAgent()) + + assert utility["name"] == "fast-utility" + assert utility["ctx_length"] == 200000 + assert utility["ctx_input"] == 0.4 + + def test_project_save_disambiguates_same_name_project_preset(monkeypatch, tmp_path): _prepare_a0_tree(monkeypatch, tmp_path) diff --git a/webui/components/projects/projects-store.js b/webui/components/projects/projects-store.js index 69818d152..910200741 100644 --- a/webui/components/projects/projects-store.js +++ b/webui/components/projects/projects-store.js @@ -5,7 +5,7 @@ import * as notifications from "/components/notifications/notification-store.js" import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; import { store as browserStore } from "/components/modals/file-browser/file-browser-store.js"; import { store as skillsImportStore } from "/components/settings/skills/skills-import-store.js"; -import { store as modelConfigStore } from "/plugins/_model_config/webui/model-config-store.js"; +import { store as modelConfigStore, configFromPreset } from "/plugins/_model_config/webui/model-config-store.js"; import * as shortcuts from "/js/shortcuts.js"; import { showConfirmDialog } from "/js/confirmDialog.js"; @@ -546,12 +546,7 @@ const model = { }, _configFromPreset(preset, baseConfig) { - const config = JSON.parse(JSON.stringify(baseConfig || {})); - if (preset.chat) config.chat_model = this._cleanModelSlot(preset.chat, true); - if (preset.utility?.provider || preset.utility?.name) { - config.utility_model = this._cleanModelSlot(preset.utility, true); - } - return config; + return configFromPreset(preset, baseConfig || {}, true); }, _cleanModelSlot(slot, stripApiKey = true) { @@ -568,6 +563,7 @@ const model = { name, chat: this._cleanModelSlot(config?.chat_model || {}, true), utility: this._cleanModelSlot(config?.utility_model || {}, true), + embedding: this._cleanModelSlot(config?.embedding_model || {}, true), }; },