diff --git a/server/src/app/services/hermes_sync.py b/server/src/app/services/hermes_sync.py index 4f6af10..c634675 100644 --- a/server/src/app/services/hermes_sync.py +++ b/server/src/app/services/hermes_sync.py @@ -60,12 +60,15 @@ def sync_hermes_model_settings( target_path.parent.mkdir(parents=True, exist_ok=True) config = _load_existing_config(target_path) - config["model"] = _build_primary_model_config(primary_route) + config["model"] = _build_primary_model_config(primary_route, existing_model_config=config.get("model")) if fallback_route is None: config.pop("fallback_model", None) else: - config["fallback_model"] = _build_fallback_model_config(fallback_route) + config["fallback_model"] = _build_fallback_model_config( + fallback_route, + existing_fallback_config=config.get("fallback_model"), + ) _atomic_yaml_write(target_path, config) return target_path @@ -87,7 +90,11 @@ def _load_existing_config(config_path: Path) -> dict[str, Any]: return dict(loaded) -def _build_primary_model_config(route: HermesModelRoute) -> dict[str, Any]: +def _build_primary_model_config( + route: HermesModelRoute, + *, + existing_model_config: Any = None, +) -> dict[str, Any]: normalized_model = route.model.strip() normalized_endpoint = route.endpoint.strip().rstrip("/") if not normalized_model or not normalized_endpoint: @@ -99,10 +106,11 @@ def _build_primary_model_config(route: HermesModelRoute) -> dict[str, Any]: "default": normalized_model, "base_url": normalized_endpoint, } + existing_api_key = _extract_existing_api_key(existing_model_config) if route.api_key.strip(): payload["api_key"] = route.api_key.strip() - else: - payload.pop("api_key", None) + elif existing_api_key: + payload["api_key"] = existing_api_key if api_mode != "chat_completions": payload["api_mode"] = api_mode else: @@ -110,7 +118,11 @@ def _build_primary_model_config(route: HermesModelRoute) -> dict[str, Any]: return payload -def _build_fallback_model_config(route: HermesModelRoute) -> dict[str, Any]: +def _build_fallback_model_config( + route: HermesModelRoute, + *, + existing_fallback_config: Any = None, +) -> dict[str, Any]: normalized_model = route.model.strip() normalized_endpoint = route.endpoint.strip().rstrip("/") if not normalized_model or not normalized_endpoint: @@ -122,10 +134,11 @@ def _build_fallback_model_config(route: HermesModelRoute) -> dict[str, Any]: "model": normalized_model, "base_url": normalized_endpoint, } + existing_api_key = _extract_existing_api_key(existing_fallback_config) if route.api_key.strip(): payload["api_key"] = route.api_key.strip() - else: - payload.pop("api_key", None) + elif existing_api_key: + payload["api_key"] = existing_api_key if api_mode != "chat_completions": payload["api_mode"] = api_mode else: @@ -133,6 +146,15 @@ def _build_fallback_model_config(route: HermesModelRoute) -> dict[str, Any]: return payload +def _extract_existing_api_key(config_section: Any) -> str: + if not isinstance(config_section, dict): + return "" + api_key = config_section.get("api_key") + if not isinstance(api_key, str): + return "" + return api_key.strip() + + def _infer_api_mode(route: HermesModelRoute) -> str: provider_label = route.provider_label.strip().casefold() endpoint = route.endpoint.strip().lower().rstrip("/") diff --git a/server/src/app/services/settings.py b/server/src/app/services/settings.py index 667922e..bf4237d 100644 --- a/server/src/app/services/settings.py +++ b/server/src/app/services/settings.py @@ -1,5 +1,6 @@ -from __future__ import annotations - +from __future__ import annotations + +import logging from dataclasses import dataclass from datetime import datetime @@ -23,7 +24,9 @@ from app.services.hermes_sync import ( restore_hermes_config_snapshot, sync_hermes_model_settings, ) - + +logger = logging.getLogger(__name__) + @dataclass(frozen=True, slots=True) class ModelSlotConfig: @@ -314,14 +317,14 @@ class SettingsService: def load_saved_model_api_key(self, slot: str | None) -> str: if not slot or slot not in MODEL_SLOT_CONFIGS: return "" - - settings_row, secrets_row = self.ensure_settings_ready() - model_rows = self.ensure_model_settings_ready(settings_row, secrets_row) - encrypted_value = model_rows[slot].api_key_encrypted - if not encrypted_value: - return "" - return decrypt_secret(encrypted_value) + settings_row, secrets_row = self.ensure_settings_ready() + model_rows = self.ensure_model_settings_ready(settings_row, secrets_row) + encrypted_value = model_rows[slot].api_key_encrypted + if not encrypted_value: + return "" + + return self._decrypt_model_api_key(encrypted_value, slot=slot) def get_runtime_model_config(self, slot: str) -> dict[str, str]: if slot not in MODEL_SLOT_CONFIGS: @@ -489,9 +492,7 @@ class SettingsService: model_row.api_key_encrypted = encrypt_secret(normalized_api_key) def _build_hermes_model_route(self, model_row: SystemModelSetting) -> HermesModelRoute: - api_key = "" - if model_row.api_key_encrypted: - api_key = decrypt_secret(model_row.api_key_encrypted) + api_key = self._decrypt_model_api_key(model_row.api_key_encrypted, slot=model_row.slot) return HermesModelRoute( provider_label=str(model_row.provider or "").strip(), @@ -500,6 +501,16 @@ class SettingsService: api_key=api_key, ) + def _decrypt_model_api_key(self, encrypted_value: str, *, slot: str) -> str: + normalized_value = str(encrypted_value or "").strip() + if not normalized_value: + return "" + try: + return decrypt_secret(normalized_value) + except ValueError: + logger.warning("Skipping undecryptable model API key for slot=%s", slot) + return "" + def _ensure_settings_schema(self) -> None: bind = self.db.get_bind() inspector = inspect(bind) @@ -511,7 +522,7 @@ class SettingsService: settings_columns = {column["name"] for column in inspector.get_columns("system_settings")} if "onlyoffice_enabled" not in settings_columns: migration_statements.append( - "ALTER TABLE system_settings ADD COLUMN onlyoffice_enabled BOOLEAN DEFAULT 0" + "ALTER TABLE system_settings ADD COLUMN onlyoffice_enabled BOOLEAN DEFAULT FALSE" ) if "onlyoffice_public_url" not in settings_columns: migration_statements.append(