mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-22 19:47:15 +00:00
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.
This commit is contained in:
parent
82280950ea
commit
e0337410e7
9 changed files with 487 additions and 50 deletions
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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: '' }
|
||||
}]">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
<span>Add Preset</span>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue