mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-23 22:06:58 +00:00
LLM - Master on/off switch (enable/disable)
This commit is contained in:
@@ -39,6 +39,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
'llm_provider_kind': _stored_llm.get('provider_kind', ''),
|
||||
'llm_local_token_multiplier': _stored_llm.get('local_token_multiplier', 5),
|
||||
'llm_change_summary_default': datastore.data['settings']['application'].get('llm_change_summary_default', ''),
|
||||
'llm_enabled': datastore.data['settings']['application'].get('llm_enabled', True),
|
||||
'llm_override_diff_with_summary': datastore.data['settings']['application'].get('llm_override_diff_with_summary', True),
|
||||
'llm_restock_use_fallback_extract': datastore.data['settings']['application'].get('llm_restock_use_fallback_extract', True),
|
||||
'llm_debug': datastore.data['settings']['application'].get('llm_debug', False),
|
||||
@@ -120,6 +121,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
datastore.data['settings']['application']['llm_change_summary_default'] = (
|
||||
llm_data.get('llm_change_summary_default') or ''
|
||||
).strip()
|
||||
datastore.data['settings']['application']['llm_enabled'] = (
|
||||
bool(llm_data.get('llm_enabled', True))
|
||||
)
|
||||
datastore.data['settings']['application']['llm_override_diff_with_summary'] = (
|
||||
bool(llm_data.get('llm_override_diff_with_summary', True))
|
||||
)
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
{% call stab_pane('provider') %}
|
||||
<p class="stab-section-title">{{ _('AI Provider') }}</p>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label></label>
|
||||
{{ form.llm.form.llm_enabled() }}
|
||||
<label for="{{ form.llm.form.llm_enabled.id }}" style="display:inline; font-weight:normal;">
|
||||
{{ form.llm.form.llm_enabled.label.text }}
|
||||
</label>
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('Master switch. When off, all AI lookups are skipped even if a provider is configured — your saved config below is kept so you can re-enable any time.') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if not llm_env_configured and not (llm_config and llm_config.get('model')) %}
|
||||
<div class="stab-overview-disclaimer">
|
||||
<div class="stab-disclaimer-icon">⚠</div>
|
||||
|
||||
@@ -1193,6 +1193,14 @@ class globalSettingsLLMForm(Form):
|
||||
"style": "width: 10em;",
|
||||
},
|
||||
)
|
||||
# Master on/off switch for ALL LLM lookups at runtime. When False, every entry point
|
||||
# in evaluator.py (and the restock fallback) short-circuits with a logger.debug
|
||||
# message — even if a provider+model is still configured. Saved config and the
|
||||
# "configured" badge remain visible so the user can toggle back on without re-entering.
|
||||
llm_enabled = BooleanField(
|
||||
_l('Enable AI / LLM features'),
|
||||
default=True,
|
||||
)
|
||||
llm_override_diff_with_summary = BooleanField(
|
||||
_l('Replace {{diff}} notification token with AI summary'),
|
||||
default=True,
|
||||
|
||||
@@ -228,6 +228,28 @@ def llm_configured_via_env() -> bool:
|
||||
return bool(os.getenv('LLM_MODEL', '').strip())
|
||||
|
||||
|
||||
def _runtime_llm_config(datastore) -> dict | None:
|
||||
"""
|
||||
Runtime gate used by every LLM entry point in this module (and the restock
|
||||
fallback). Returns the resolved config dict only when both:
|
||||
- the master 'llm_enabled' toggle is on (default True)
|
||||
- a provider+model is actually configured
|
||||
|
||||
When the toggle is off but a config exists, logs a debug message and returns
|
||||
None so callers fall through their existing "not configured" early-return path.
|
||||
|
||||
The settings UI deliberately still calls get_llm_config() directly so the
|
||||
"AI / LLM configured: ..." badge keeps showing the saved provider even while
|
||||
the toggle is off.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not bool(datastore.data['settings']['application'].get('llm_enabled', True)):
|
||||
if cfg:
|
||||
logger.debug("LLM features disabled via settings (llm_enabled=False) — skipping LLM lookup")
|
||||
return None
|
||||
return cfg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global monthly token budget
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -379,7 +401,7 @@ def run_setup(watch, datastore, snapshot_text: str) -> None:
|
||||
Stores result in watch['llm_prefilter'] (str selector or None).
|
||||
Called once when intent is first set, and again if pre-filter returns zero matches.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
if not cfg:
|
||||
return
|
||||
|
||||
@@ -509,7 +531,7 @@ def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
The result replaces {{ diff }} in notifications so the user gets a
|
||||
readable description instead of raw +/- diff lines.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
if not cfg:
|
||||
return ''
|
||||
|
||||
@@ -597,7 +619,7 @@ def preview_extract(watch, datastore, content: str) -> dict | None:
|
||||
|
||||
Returns {'found': bool, 'answer': str} or None if LLM not configured / no intent.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
@@ -648,7 +670,7 @@ def evaluate_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
|
||||
Results are cached by (intent, diff) hash — each unique diff is evaluated exactly once.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class model(dict):
|
||||
'shared_diff_access': False,
|
||||
'strip_ignored_lines': False,
|
||||
'tags': None, # Initialized in __init__ with real datastore_path
|
||||
'llm_enabled': True,
|
||||
'llm_thinking_budget': LLM_DEFAULT_THINKING_BUDGET,
|
||||
'llm_max_summary_tokens': LLM_DEFAULT_MAX_SUMMARY_TOKENS,
|
||||
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
||||
|
||||
@@ -203,15 +203,17 @@ def get_itemprop_availability_override(content, fetcher_name, fetcher_instance,
|
||||
return None
|
||||
|
||||
try:
|
||||
from changedetectionio.llm.evaluator import get_llm_config, accumulate_global_tokens
|
||||
from changedetectionio.llm.evaluator import _runtime_llm_config, accumulate_global_tokens
|
||||
from changedetectionio.llm import client as llm_client
|
||||
except ImportError as e:
|
||||
logger.debug(f"LLM restock fallback: LLM libraries not available ({e})")
|
||||
return None
|
||||
|
||||
llm_cfg = get_llm_config(datastore)
|
||||
# _runtime_llm_config returns None (with a debug log) when the master 'llm_enabled'
|
||||
# toggle is off, so this path is gated for free.
|
||||
llm_cfg = _runtime_llm_config(datastore)
|
||||
if not llm_cfg or not llm_cfg.get('model'):
|
||||
logger.debug("LLM restock fallback: no LLM model configured, skipping")
|
||||
logger.debug("LLM restock fallback: no LLM model configured or LLM disabled, skipping")
|
||||
return None
|
||||
|
||||
text_content = _strip_html(content) if content else ''
|
||||
|
||||
Reference in New Issue
Block a user