This commit is contained in:
dgtlmoon
2026-05-24 13:18:35 +02:00
parent 536b626cf0
commit 8525a4af37
3 changed files with 23 additions and 37 deletions
@@ -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)
+3 -4
View File
@@ -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
+15 -24
View File
@@ -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',
)