mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-06-04 16:01:05 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f4cc2d6c1 | |||
| 9adf6a478e | |||
| dd56a502c0 | |||
| baae46deed | |||
| d7a1b67c5a | |||
| b7bb67fac4 |
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
# Semver means never use .01, or 00. Should be .1.
|
||||
__version__ = '0.55.6'
|
||||
__version__ = '0.55.7'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -99,6 +99,12 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
|
||||
llm_form_input = dict(form.data.get('llm') or {})
|
||||
|
||||
# Empty IntegerField submissions come back as None from WTForms;
|
||||
# the schema declares those fields as strict `int`, so passing
|
||||
# them through would fail validation. Treat None like the
|
||||
# absent-key case: keep the stored value, don't merge.
|
||||
llm_form_input = {k: v for k, v in llm_form_input.items() if v is not None}
|
||||
|
||||
# PasswordField never re-renders, so a blank submitted value means
|
||||
# "keep stored key" — drop it from the merge.
|
||||
if not (llm_form_input.get('api_key') or '').strip():
|
||||
|
||||
@@ -82,10 +82,23 @@ def _check_input_size(text: str, max_chars: int) -> None:
|
||||
|
||||
def _thinking_extra_body(model: str, budget: int) -> dict | None:
|
||||
"""Return litellm extra_body to control thinking for models that support it.
|
||||
For Gemini 2.5+: passes thinkingConfig with the given budget (0 = disabled).
|
||||
For all other models: returns None (no-op).
|
||||
|
||||
The `thinkingConfig.thinkingBudget` payload is Gemini-specific (Anthropic and
|
||||
OpenAI reasoning models use different parameters), so we gate on the gemini/
|
||||
provider prefix first, then defer to litellm's model registry for the actual
|
||||
"does this model think?" decision. That picks up new Gemini variants and
|
||||
rolling aliases (`gemini-flash-latest`, etc.) as litellm's registry tracks
|
||||
them, without us hardcoding model names here.
|
||||
"""
|
||||
if not model.startswith('gemini/gemini-2.5'):
|
||||
if not model.startswith('gemini/'):
|
||||
return None
|
||||
try:
|
||||
import litellm
|
||||
if not litellm.get_model_info(model).get('supports_reasoning'):
|
||||
return None
|
||||
except Exception:
|
||||
# Unknown model or registry lookup failed — skip the thinking config
|
||||
# rather than guess. Worst case: thinking stays at the provider default.
|
||||
return None
|
||||
return {'generationConfig': {'thinkingConfig': {'thinkingBudget': budget}}}
|
||||
|
||||
|
||||
@@ -196,6 +196,81 @@ def test_settings_form_preserves_token_counters(
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_settings_form_blank_llm_integer_fields_preserve_stored_values(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Empty IntegerField submissions come back as None from WTForms. LLMSettings
|
||||
declares token_budget_month / max_input_chars / max_tokens_per_count_period /
|
||||
local_token_multiplier as strict `int`, so a None passed through to
|
||||
model_validate raises ValidationError and 500s the settings save.
|
||||
|
||||
Regression for settings/__init__.py — the LLM merge must drop None values
|
||||
(treat them like absent keys) so blank IntegerField submissions preserve
|
||||
the stored value instead of crashing the form.
|
||||
"""
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
ds.data['settings']['application']['llm'] = {
|
||||
'model': 'gpt-4o',
|
||||
'api_key': 'sk-existing',
|
||||
'token_budget_month': 50000,
|
||||
'max_input_chars': 200000,
|
||||
'max_tokens_per_count_period': 1000,
|
||||
'local_token_multiplier': 3,
|
||||
}
|
||||
|
||||
res = client.post(
|
||||
url_for('settings.settings_page'),
|
||||
data={
|
||||
'llm-model': 'gpt-4o',
|
||||
'llm-api_key': '',
|
||||
'llm-api_base': '',
|
||||
# The bug-trigger: every LLM IntegerField submitted blank
|
||||
'llm-token_budget_month': '',
|
||||
'llm-max_input_chars': '',
|
||||
'llm-max_tokens_per_count_period': '',
|
||||
'llm-local_token_multiplier': '',
|
||||
# Minimal required fields for the rest of the form to validate.
|
||||
# 'System default' is popped from notification_format choices for the
|
||||
# global form, so it must be one of the real codes (e.g. 'html').
|
||||
'application-pager_size': '50',
|
||||
'application-notification_format': 'html',
|
||||
'application-fetch_backend': 'html_requests',
|
||||
'application-rss_diff_length': '5',
|
||||
'application-filter_failure_notification_threshold_attempts': '0',
|
||||
'requests-time_between_check-days': '0',
|
||||
'requests-time_between_check-hours': '0',
|
||||
'requests-time_between_check-minutes': '5',
|
||||
'requests-time_between_check-seconds': '0',
|
||||
'requests-time_between_check-weeks': '0',
|
||||
'requests-jitter_seconds': '0',
|
||||
'requests-workers': '10',
|
||||
'requests-timeout': '60',
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert res.status_code == 200, \
|
||||
f"Settings save crashed on blank LLM IntegerField submission (got {res.status_code})"
|
||||
# Sanity: the form must have actually validated and reached the LLM save path
|
||||
# — without this the test would trivially pass because the buggy code never ran.
|
||||
assert b'Settings updated.' in res.data, \
|
||||
"Settings form did not validate — the bug-path was never exercised. Check fixture fields."
|
||||
body = res.data.decode('utf-8', errors='replace')
|
||||
assert 'ValidationError' not in body, \
|
||||
"Pydantic ValidationError leaked into the response — blank IntegerField wasn't filtered"
|
||||
|
||||
llm = ds.data['settings']['application'].get('llm') or {}
|
||||
assert llm.get('token_budget_month') == 50000, \
|
||||
f"Blank submission must preserve stored token_budget_month (got {llm.get('token_budget_month')!r})"
|
||||
assert llm.get('max_input_chars') == 200000, \
|
||||
f"Blank submission must preserve stored max_input_chars (got {llm.get('max_input_chars')!r})"
|
||||
assert llm.get('max_tokens_per_count_period') == 1000, \
|
||||
f"Blank submission must preserve stored max_tokens_per_count_period (got {llm.get('max_tokens_per_count_period')!r})"
|
||||
assert llm.get('local_token_multiplier') == 3, \
|
||||
f"Blank submission must preserve stored local_token_multiplier (got {llm.get('local_token_multiplier')!r})"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_settings_form_cannot_inject_fake_token_counts(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
|
||||
Binary file not shown.
@@ -584,7 +584,7 @@ msgstr "Pozn.: Toto je aplikováno globálně dodatečně k pravidlům nastaven
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Matching text will be ignored in the text snapshot (you can still see it but it wont trigger a change)"
|
||||
msgstr ""
|
||||
msgstr "Text shody bude ignorován ve snímku textu (bude i nadále vidět, ale nebude spouštět upozornění na změnu)"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Each line processed separately, any line matching will be ignored (removed before creating the checksum)"
|
||||
@@ -772,23 +772,23 @@ msgstr "Využití"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
msgstr "Přehled"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
msgstr "Poskytovatel"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Prompts"
|
||||
msgstr ""
|
||||
msgstr "Prompty"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Behaviour"
|
||||
msgstr ""
|
||||
msgstr "Chování"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "AI-powered change monitoring"
|
||||
msgstr ""
|
||||
msgstr "AI podporované sledování změn"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Connect an LLM to move from \"something changed\" to \"only the thing you care about changed\"."
|
||||
@@ -925,31 +925,31 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
msgstr "Načíst dostupné modely"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Available models"
|
||||
msgstr ""
|
||||
msgstr "Dostupné modely"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "choose a model"
|
||||
msgstr ""
|
||||
msgstr "vybrat model"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Enter API key and click 'Load available models'"
|
||||
msgstr ""
|
||||
msgstr "Vložit API klíč a kliknout na 'Načíst dostupné modely'"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Remove AI / LLM configuration?"
|
||||
msgstr ""
|
||||
msgstr "Odstranit AI / LLM konfiguraci?"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "This will remove your saved AI provider, model, and API key."
|
||||
msgstr ""
|
||||
msgstr "Toto odstraní uloženého poskytovatele AI, model a API klíč."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
msgstr "Odstranit"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html changedetectionio/templates/base.html
|
||||
@@ -958,7 +958,7 @@ msgstr "Zrušit"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Test connection"
|
||||
msgstr ""
|
||||
msgstr "Otestovat připojení"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid ""
|
||||
@@ -1294,7 +1294,7 @@ msgstr "Sledovat skupinu / Značka"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "Groups allows you to manage filters and notifications for multiple watches under a single organisational tag."
|
||||
msgstr ""
|
||||
msgstr "Skupiny umožňují spravovat filtry a upozornění pro vícero sledování seskupené pod jedním organizačním tagem."
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "# Watches"
|
||||
@@ -1329,7 +1329,7 @@ msgstr "Smazat skupinu?"
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#, python-format
|
||||
msgid "<p>Are you sure you want to delete group <strong>%(title)s</strong>?</p><p>This action cannot be undone.</p>"
|
||||
msgstr ""
|
||||
msgstr "<p>Opravdu chcete smazat skupinu <strong>%(title)s</strong>?</p><p>Tuto akci nelze vzít zpět.</p>"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
@@ -1350,6 +1350,8 @@ msgid ""
|
||||
"<p>Are you sure you want to unlink all watches from group <strong>%(title)s</strong>?</p><p>The tag will be kept but "
|
||||
"watches will be removed from it.</p>"
|
||||
msgstr ""
|
||||
"<p>Opravud chcete odpojit všechna sledování pod skupinou <strong>%(title)s</strong>?</p><p>Tag bude zachován, ale "
|
||||
"podřazená sledování budou odstraněna.</p>"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "Unlink"
|
||||
@@ -1391,22 +1393,22 @@ msgstr "{} sledování ztlumeno"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches un-muted"
|
||||
msgstr ""
|
||||
msgstr "{} sledování zesíleno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches queued for rechecking"
|
||||
msgstr ""
|
||||
msgstr "{} sledování ve frontě ke kontrole"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches errors cleared"
|
||||
msgstr ""
|
||||
msgstr "{} chyb sledování vyčištěno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches cleared/reset."
|
||||
msgstr ""
|
||||
msgstr "{} sledování vyčištěno/resetováno."
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
@@ -1416,7 +1418,7 @@ msgstr "{} monitorů nastaveno na použití výchozího nastavení oznámení"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches were tagged"
|
||||
msgstr ""
|
||||
msgstr "{} sledování otagováno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Watch not found"
|
||||
@@ -1433,7 +1435,7 @@ msgstr "jasný"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "History clearing started in background"
|
||||
msgstr ""
|
||||
msgstr "Čištění historie spuštěno na pozadí"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Incorrect confirmation text."
|
||||
@@ -1442,7 +1444,7 @@ msgstr "Žádné informace"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "The watch by UUID {} does not exist."
|
||||
msgstr ""
|
||||
msgstr "Sledování s UUID {} neexistuje."
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Deleted."
|
||||
@@ -1450,15 +1452,15 @@ msgstr "Smazáno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Cloned, you are editing the new watch."
|
||||
msgstr ""
|
||||
msgstr "Naklonováno, upravujete nové sledování."
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Watch is already queued or being checked."
|
||||
msgstr ""
|
||||
msgstr "Sledování je již zařazeno do fronty ke kontrole."
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Queued 1 watch for rechecking."
|
||||
msgstr ""
|
||||
msgstr "Zařazeno 1 sledování ke kontrole."
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
@@ -1477,7 +1479,7 @@ msgstr "Přidává se sledování do fronty pro opětovnou kontrolu na pozadí..
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "Could not share, something went wrong while communicating with the share server - {}"
|
||||
msgstr ""
|
||||
msgstr "Sdílení selhalo, něco se pokazilo při komunikaci se sdílecím serverem = {}"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Language set to auto-detect from browser"
|
||||
@@ -1485,52 +1487,52 @@ msgstr "Jazyk nastaven na automatickou detekci z prohlížeče"
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py changedetectionio/blueprint/ui/preview.py
|
||||
msgid "No history found for the specified link, bad link?"
|
||||
msgstr ""
|
||||
msgstr "Historie pro vybraný odkaz nenalezena, chybný odkaz?"
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py
|
||||
msgid "Not enough history (2 snapshots required) to show difference page for this watch."
|
||||
msgstr ""
|
||||
msgstr "Nedostatečná historie (vyžadovány 2 záchyty) pro zobrazení rozdílů tohoto sledování."
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py
|
||||
#, python-format
|
||||
msgid "Monthly AI token budget of %(budget)s tokens reached (%(used)s used). Resets next month."
|
||||
msgstr ""
|
||||
msgstr "Dosažen měsíční počet %(budget)s AI tokenů (%(used)s použito). Resetuje se příští měsíc."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "No watches to edit"
|
||||
msgstr ""
|
||||
msgstr "Žádná sledování k úpravě"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "No watch with the UUID {} found."
|
||||
msgstr ""
|
||||
msgstr "Sledování s UUID {} nenalezeno."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Switched to mode - {}."
|
||||
msgstr ""
|
||||
msgstr "Přepnuto na mód - {}."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing. Please select a different processor."
|
||||
msgstr ""
|
||||
msgstr "Nelze načíst '{}' procesor, zásuvný modul procesoru nejspíše chybí. Vyberte prosím jiný procesor."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
msgstr "Nelze načíst '{}' procesor, zásuvný modul procesoru nejspíše chybí."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
msgstr "Výchozí systémová nastavení"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
msgstr "Výchozí"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
msgstr "Sledování aktualizováno - znovu spuštěno!"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch."
|
||||
@@ -1538,15 +1540,15 @@ msgstr "Sledování aktualizováno."
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
msgstr "Náhled nedostupný - stažení/kontrola nedokončena nebo nebyly splněny podmínky kontroly"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
msgstr "Rozdíly"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
msgstr "Toto odstraní historii verzí (snímky) pro VŠECHNA sledování, ale ponechá seznam URL adres!"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "You may like to use the"
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.6\n"
|
||||
"Project-Id-Version: changedetection.io 0.55.7\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-05-25 17:59+0200\n"
|
||||
"POT-Creation-Date: 2026-06-03 12:07+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
@@ -70,6 +70,9 @@ services:
|
||||
# For complete privacy if you don't want to use the 'check version' / telemetry service
|
||||
# - DISABLE_VERSION_CHECK=true
|
||||
#
|
||||
# Disable all LLM / AI features, prompts etc
|
||||
# - LLM_FEATURES_DISABLED=true
|
||||
#
|
||||
# A valid timezone name to run as (for scheduling watch checking) see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
# - TZ=America/Los_Angeles
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user