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), }; },