agent-zero/plugins/_model_config/api/model_config_set.py
frdel 15a2b553be Unify global model presets
Make model presets a single global collection and treat presets as reusable setups (main, utility, embedding). Persisted scope configs now store only a preset name (Default is always present and immutable); project-scoped preset definitions were removed and attempts to edit/reset project presets are rejected. Add embedding-model support throughout: include embedding slot in presets, surface embedding in the switcher and override APIs, and notify extensions when the effective embedding model changes. Implement rename/retire propagation to update plugin configs, saved chats, and live AgentContext objects when preset names change or presets are removed. Update CLI/integration commands to use an "inherit" action for returning to the scoped preset and to report the effective scoped-or-chat preset. Add startup migration/bootstrap to initialize presets from remote or bundled fallback and numerous docs/UI text updates to reflect the new preset model. Extra: refactor config resolution helpers and add safety checks (validation, atomic writes) when saving presets.
2026-07-17 12:16:10 +02:00

83 lines
3.3 KiB
Python

from copy import deepcopy
from helpers.api import ApiHandler, Request, Response
from helpers import defer, dotenv
from helpers.extension import call_extensions_async
from plugins._model_config.helpers import model_config
API_KEY_PLACEHOLDER = "************"
class ModelConfigSet(ApiHandler):
async def process(self, input: dict, request: Request) -> dict | Response:
project_name = input.get("project_name", "")
agent_profile = input.get("agent_profile", "")
config = input.get("config")
if not config or not isinstance(config, dict):
return Response(status=400, response="Missing or invalid config")
config_to_save = deepcopy(config)
for section_name in ("chat_model", "utility_model", "embedding_model"):
section = config_to_save.get(section_name, {})
if not isinstance(section, dict):
continue
provider = str(section.get("provider", "")).strip()
api_key = section.get("api_key", "")
if (
provider
and isinstance(api_key, str)
and api_key.strip()
and api_key != API_KEY_PLACEHOLDER
):
dotenv.save_dotenv_value(f"API_KEY_{provider.upper()}", api_key)
section.pop("api_key", None)
preset_name = str(
input.get("preset_name")
or config_to_save.get(model_config.MODEL_PRESET_CONFIG_KEY)
or model_config.get_configured_preset_name(
project_name=project_name or None,
agent_profile=agent_profile or None,
)
).strip()
preset = model_config.resolve_preset(preset_name)
if not preset:
return Response(status=404, response=f"Preset '{preset_name}' not found")
preset_name = str(preset.get("name") or model_config.DEFAULT_PRESET_NAME)
# Read the preset before saving so embedding changes can still trigger
# the established re-index notification.
prev_config = model_config.resolve_config_settings(
{model_config.MODEL_PRESET_CONFIG_KEY: preset_name}
)
try:
model_config.update_preset_from_config(preset_name, config_to_save)
except ValueError as exc:
return Response(status=400, response=str(exc))
# Keep the requested scope pointed at the preset being edited. This is
# selection-only persistence; model dictionaries live in presets.yaml.
from helpers import plugins
plugins.save_plugin_config(
"_model_config",
project_name=project_name or None,
agent_profile=agent_profile or None,
settings={model_config.MODEL_PRESET_CONFIG_KEY: preset_name},
)
# Check if embedding model changed and notify
prev_embed = prev_config.get("embedding_model", {})
new_embed = config_to_save.get("embedding_model", {})
if (
prev_embed.get("provider") != new_embed.get("provider")
or prev_embed.get("name") != new_embed.get("name")
or prev_embed.get("kwargs") != new_embed.get("kwargs")
):
defer.DeferredTask().start_task(
call_extensions_async, "embedding_model_changed"
)
return {"ok": True, "preset_name": preset_name}