From 8525a4af374a113d0e55740b9d1c6c8a5ea8fd6f Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Sun, 24 May 2026 13:18:35 +0200 Subject: [PATCH] WIP --- .../blueprint/settings/__init__.py | 14 +++---- changedetectionio/blueprint/settings/llm.py | 7 ++-- changedetectionio/model/LLMSettings.py | 39 +++++++------------ 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py index 7614bccf..ff723e74 100644 --- a/changedetectionio/blueprint/settings/__init__.py +++ b/changedetectionio/blueprint/settings/__init__.py @@ -127,15 +127,11 @@ def construct_blueprint(datastore: ChangeDetectionStore): # model_dump() — even though .attribute access reads the alias correctly. merged = LLMSettings.model_validate({**existing_llm.model_dump(by_alias=True), **llm_form_input}) - # Clearing the model strips only the credential fields. User toggles - # (llm_enabled, debug, override_diff_with_summary, …), the global summary - # prompt, monthly budgets, and the system token counters all survive — - # matches the /llm/clear endpoint's semantics. - merged_dict = merged.model_dump() - if not merged.model.strip(): - for key in ('model', 'api_key', 'api_base', 'provider_kind', 'local_token_multiplier'): - merged_dict.pop(key, None) - datastore.data['settings']['application']['llm'] = merged_dict + # Clearing the model field strips only the provider-connection fields. + # User toggles, budgets, the summary prompt, and system counters survive + # (same semantics as /llm/clear). + exclude = set(LLMSettings.CONNECTION_FIELDS) if not merged.model.strip() else None + datastore.data['settings']['application']['llm'] = merged.model_dump(exclude=exclude) # Handle dynamic worker count adjustment old_worker_count = datastore.data['settings']['requests'].get('workers', 1) diff --git a/changedetectionio/blueprint/settings/llm.py b/changedetectionio/blueprint/settings/llm.py index c07da003..91bcb9d5 100644 --- a/changedetectionio/blueprint/settings/llm.py +++ b/changedetectionio/blueprint/settings/llm.py @@ -232,12 +232,11 @@ def construct_llm_blueprint(datastore: ChangeDetectionStore): @llm_blueprint.route("/clear", methods=['POST']) @login_optionally_required def llm_clear(): + from changedetectionio.model.LLMSettings import LLMSettings logger.debug("LLM configuration cleared by user") - # Strip only the credential / connection fields — user-set toggles, the - # global summary prompt, monthly budgets, and the system token counters - # all survive a "clear credentials" action. + # Strip provider-connection fields only. Toggles, prompts, budgets and counters survive. llm = datastore.data['settings']['application'].get('llm') or {} - for key in ('model', 'api_key', 'api_base', 'provider_kind', 'local_token_multiplier'): + for key in LLMSettings.CONNECTION_FIELDS: llm.pop(key, None) if llm: datastore.data['settings']['application']['llm'] = llm diff --git a/changedetectionio/model/LLMSettings.py b/changedetectionio/model/LLMSettings.py index 1088ee1b..17c9664a 100644 --- a/changedetectionio/model/LLMSettings.py +++ b/changedetectionio/model/LLMSettings.py @@ -1,16 +1,10 @@ """ -LLMSettings — validation/typing layer over the LLM config dict. +Validation/typing layer for the LLM config dict stored at + datastore.data['settings']['application']['llm'] -Storage shape (after migration update_31): everything lives under - datastore.data['settings']['application']['llm'] = { ... } - -Field names are stripped (enabled, debug, model, …). WTForms field names are -still llm_-prefixed (llm_enabled, llm_debug, …) and Pydantic Field aliases -bridge both sides, so callers don't repeat the rename. - -The store stays a plain dict (orjson-serialized) — this model is hydrated on -read (model_validate) and dumped on write (model_dump). Pydantic instances -are never held in datastore.data. +Storage stays a plain dict (orjson-serialized). This model is hydrated on read +(model_validate) and dumped on write (model_dump). Form-side WTForms field names +keep the llm_-prefix; Field aliases bridge them to the stripped storage names. """ from typing import ClassVar, Tuple @@ -25,10 +19,7 @@ LLM_DEFAULT_BUDGET_ACTION = 'skip_llm' class LLMSettings(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - extra='allow', - ) + model_config = ConfigDict(populate_by_name=True, extra='allow') enabled: bool = Field(default=True, alias='llm_enabled') debug: bool = Field(default=False, alias='llm_debug') @@ -38,14 +29,14 @@ class LLMSettings(BaseModel): max_summary_tokens: int = Field(default=LLM_DEFAULT_MAX_SUMMARY_TOKENS, alias='llm_max_summary_tokens') budget_action: str = Field(default=LLM_DEFAULT_BUDGET_ACTION, alias='llm_budget_action') change_summary_default: str = Field(default='', alias='llm_change_summary_default') + token_budget_month: int = Field(default=0, alias='llm_token_budget_month') + max_input_chars: int = Field(default=LLM_DEFAULT_MAX_INPUT_CHARS, alias='llm_max_input_chars') model: str = Field(default='', alias='llm_model') api_key: str = Field(default='', alias='llm_api_key') api_base: str = Field(default='', alias='llm_api_base') provider_kind: str = Field(default='', alias='llm_provider_kind') local_token_multiplier: int = Field(default=LLM_DEFAULT_LOCAL_TOKEN_MULTIPLIER, alias='llm_local_token_multiplier') - token_budget_month: int = Field(default=0, alias='llm_token_budget_month') - max_input_chars: int = Field(default=LLM_DEFAULT_MAX_INPUT_CHARS, alias='llm_max_input_chars') tokens_total_cumulative: int = 0 tokens_this_month: int = 0 @@ -53,12 +44,12 @@ class LLMSettings(BaseModel): cost_usd_total_cumulative: float = 0.0 cost_usd_this_month: float = 0.0 - # Runtime-managed counters that must survive form submissions. The settings - # POST handler strips these from form input before applying the merge. + # Provider-connection fields wiped on /llm/clear and when the model is emptied. + CONNECTION_FIELDS: ClassVar[Tuple[str, ...]] = ( + 'model', 'api_key', 'api_base', 'provider_kind', 'local_token_multiplier', + ) + # Runtime-managed counters — form submissions must never overwrite these. PROTECTED_FIELDS: ClassVar[Tuple[str, ...]] = ( - 'tokens_total_cumulative', - 'tokens_this_month', - 'tokens_month_key', - 'cost_usd_total_cumulative', - 'cost_usd_this_month', + 'tokens_total_cumulative', 'tokens_this_month', 'tokens_month_key', + 'cost_usd_total_cumulative', 'cost_usd_this_month', )