diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py
index a98151bc3..b4d103d21 100644
--- a/changedetectionio/blueprint/settings/__init__.py
+++ b/changedetectionio/blueprint/settings/__init__.py
@@ -14,8 +14,10 @@ from changedetectionio.auth_decorator import login_optionally_required
def construct_blueprint(datastore: ChangeDetectionStore):
from changedetectionio.blueprint.settings.llm import construct_llm_blueprint
+ from changedetectionio.llm.evaluator import is_llm_features_disabled
settings_blueprint = Blueprint('settings', __name__, template_folder="templates")
- settings_blueprint.register_blueprint(construct_llm_blueprint(datastore), url_prefix='/llm')
+ if not is_llm_features_disabled():
+ settings_blueprint.register_blueprint(construct_llm_blueprint(datastore), url_prefix='/llm')
@settings_blueprint.route("", methods=['GET', "POST"])
@login_optionally_required
diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html
index ed05839bf..b2e578978 100644
--- a/changedetectionio/blueprint/settings/templates/settings.html
+++ b/changedetectionio/blueprint/settings/templates/settings.html
@@ -34,7 +34,9 @@
{{ tab.tab_label }}
{% endfor %}
{% endif %}
+ {% if not llm_features_disabled %}
{{ _('AI / LLM') }}
+ {% endif %}
{{ _('Info') }}
@@ -394,7 +396,9 @@ nav
{% endfor %}
{% endif %}
+ {% if not llm_features_disabled %}
{% include 'settings_llm_tab.html' %}
+ {% endif %}
{{ _('Uptime:') }} {{ uptime_seconds|format_duration }}
{{ _('Python version:') }} {{ python_version }}
diff --git a/changedetectionio/blueprint/ui/templates/edit.html b/changedetectionio/blueprint/ui/templates/edit.html
index 8ea17b439..420ec72c2 100644
--- a/changedetectionio/blueprint/ui/templates/edit.html
+++ b/changedetectionio/blueprint/ui/templates/edit.html
@@ -57,7 +57,9 @@
{% if capabilities.supports_visual_selector %}
{{ _('Visual Filter Selector') }}
{% endif %}
+ {% if not llm_features_disabled %}
{{ _('AI / LLM') }}
+ {% endif %}
{% if capabilities.supports_text_filters_and_triggers %}
{{ _('Filters & Triggers') }}
{{ _('Conditions') }}
@@ -321,9 +323,11 @@ Math: {{ 1 + 1 }}") }}
+ {% if not llm_features_disabled %}
{% include "edit/include_llm_intent.html" %}
+ {% endif %}
{{ _('Activate preview') }}
@@ -503,7 +507,7 @@ Math: {{ 1 + 1 }}") }}
{{ _('Server type reply') }} |
{{ watch.get('remote_server_reply') }} |
- {% if settings_application.get('llm', {}).get('model') %}
+ {% if not llm_features_disabled and settings_application.get('llm', {}).get('model') %}
| {{ _('AI tokens (last check)') }} |
{{ "{:,}".format(watch.get('llm_last_tokens_used') or 0) }} |
diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py
index 987309b8c..3449726bf 100644
--- a/changedetectionio/flask_app.py
+++ b/changedetectionio/flask_app.py
@@ -522,6 +522,11 @@ def changedetection_app(config=None, datastore_o=None):
available_languages=available_languages
)
+ @app.context_processor
+ def inject_llm_features_disabled():
+ from changedetectionio.llm.evaluator import is_llm_features_disabled
+ return dict(llm_features_disabled=is_llm_features_disabled())
+
# Set up a request hook to check authentication for all routes
@app.before_request
def check_authentication():
diff --git a/changedetectionio/llm/evaluator.py b/changedetectionio/llm/evaluator.py
index 3360f3ec0..085e9a2d4 100644
--- a/changedetectionio/llm/evaluator.py
+++ b/changedetectionio/llm/evaluator.py
@@ -20,6 +20,8 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from loguru import logger
+from changedetectionio.strtobool import strtobool
+
from . import client as llm_client
from .prompt_builder import (
build_change_summary_prompt, build_change_summary_system_prompt,
@@ -31,6 +33,11 @@ from .response_parser import parse_eval_response, parse_preview_response, parse_
_DEFAULT_MAX_INPUT_CHARS = 100_000
+
+def is_llm_features_disabled() -> bool:
+ """True when the LLM_FEATURES_DISABLED env var is set to a truthy value."""
+ return bool(strtobool(os.getenv('LLM_FEATURES_DISABLED', '')))
+
def _get_max_input_chars(datastore) -> int:
"""Max input characters to send to the LLM. Resolution: env var → datastore → 100,000.
Always returns at least 1 — unlimited is not permitted.
@@ -207,6 +214,8 @@ def get_llm_config(datastore) -> dict | None:
1. Environment variables: LLM_MODEL, LLM_API_KEY, LLM_API_BASE
2. Datastore settings (set via UI)
"""
+ if is_llm_features_disabled():
+ return None
# 1. Environment variable override
env_model = os.getenv('LLM_MODEL', '').strip()
if env_model:
@@ -225,6 +234,8 @@ def get_llm_config(datastore) -> dict | None:
def llm_configured_via_env() -> bool:
"""True when LLM config comes from environment variables, not the UI."""
+ if is_llm_features_disabled():
+ return False
return bool(os.getenv('LLM_MODEL', '').strip())
diff --git a/changedetectionio/templates/_common_fields.html b/changedetectionio/templates/_common_fields.html
index 696185c23..829ca1c4b 100644
--- a/changedetectionio/templates/_common_fields.html
+++ b/changedetectionio/templates/_common_fields.html
@@ -112,7 +112,7 @@
{{ '{{triggered_text}}' }} |
{{ _('Text that tripped the trigger from filters') }} |
- {% if settings_application and settings_application.get('llm', {}).get('model') %}
+ {% if not llm_features_disabled and settings_application and settings_application.get('llm', {}).get('model') %}
{{ '{{diff}}' }} {{ _('(upgraded)') }} |
{{ _('When AI Change Summary is configured, contains the AI-generated description instead of the raw diff. Falls back to raw diff when not configured.') }} |
diff --git a/changedetectionio/templates/base.html b/changedetectionio/templates/base.html
index 0204d84b2..94d318b10 100644
--- a/changedetectionio/templates/base.html
+++ b/changedetectionio/templates/base.html
@@ -281,6 +281,7 @@
+ {% if not llm_features_disabled %}
+ {% endif %}
{% if current_user.is_authenticated or not has_password %}
diff --git a/changedetectionio/templates/menu.html b/changedetectionio/templates/menu.html
index 4afaa8b5d..a67f901e6 100644
--- a/changedetectionio/templates/menu.html
+++ b/changedetectionio/templates/menu.html
@@ -37,10 +37,12 @@
{% endif %}