UI - Prevent accidental change of LLM Key (#4473)

This commit is contained in:
dgtlmoon authored and GitHub committed 2026-09-20 19:44:06 +02:00
1 parent de4637c0bd
commit 239a69dcc4
23 files changed
+329 -6

No files matched your search

@@ -238,6 +238,15 @@ def construct_blueprint(datastore: ChangeDetectionStore):
)
llm_config = _get_llm_cfg(datastore) or {}
llm_env_configured = llm_configured_via_env()
# Once a key is stored, the provider and the key itself are locked and can only be
# cleared with "Remove provider". The key field renders blank so that an untouched
# save preserves it, which makes any stray value in it - a browser autofill, a bad
# paste - silently overwrite a working key on the next Save, with no copy kept
# anywhere to recover from. Disabling the input takes it out of the POST entirely;
# the form is seeded with data=default, so an absent field falls back to the stored
# value and the merge below is a no-op for it.
llm_provider_locked = bool(llm_config.get('api_key')) and not llm_env_configured
llm_stored = datastore.data['settings']['application'].get('llm') or {}
llm_token_budget_month = get_global_token_budget_month(datastore)
llm_token_budget_month_env = get_global_token_budget_month() # env var only, for readonly logic
@@ -253,6 +262,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
api_key=datastore.data['settings']['application'].get('api_access_token'),
llm_config=llm_config,
llm_env_configured=llm_env_configured,
llm_provider_locked=llm_provider_locked,
llm_stored=llm_stored,
llm_token_budget_month=llm_token_budget_month,
llm_token_budget_month_env=llm_token_budget_month_env,
@@ -118,7 +118,7 @@
<div class="pure-control-group">
<label for="llm-provider">{{ _('Provider') }}</label>
<select id="llm-provider" onchange="llmOnProviderChange(this.value)">
<select id="llm-provider" onchange="llmOnProviderChange(this.value)"{% if llm_provider_locked %} disabled{% endif %}>
<option value="">— {{ _('select a provider') }} —</option>
<option value="anthropic">Anthropic</option>
<option value="gemini">Google (Gemini)</option>
@@ -130,8 +130,13 @@
</div>
<div class="pure-control-group">
{{ render_field(form.llm.form.api_key) }}
<span class="pure-form-message-inline" id="llm-key-hint"></span>
{#- Disabled once a key is stored: a disabled input is not submitted at all, so there is
no way for an autofilled or mistyped value to reach the save path and overwrite a
working key. "Remove provider" is the only way to change it. -#}
{{ render_field(form.llm.form.api_key, disabled=llm_provider_locked) }}
<span class="pure-form-message-inline" id="llm-key-hint">
{%- if llm_provider_locked %}{{ _('Saved. Use "Remove provider" below to change the provider or key.') }}{% endif -%}
</span>
</div>
<div class="pure-control-group" id="llm-base-group" style="display:none">
{{ render_field(form.llm.form.api_base) }}
@@ -202,7 +207,7 @@
data-confirm-message="<p>{{ _('This will remove your saved AI provider, model, and API key.') }}</p>"
data-confirm-button="{{ _('Remove') }}"
data-cancel-button="{{ _('Cancel') }}">
&#10005; {{ _('Remove') }}
&#10005; {{ _('Remove provider') }}
</a>
&nbsp;
<button type="button" id="llm-test-btn" class="cdio-btn button-xsmall" onclick="llmRunTest()"
+17 -1
View File
@@ -1265,7 +1265,23 @@ class globalSettingsLLMForm(Form):
_l('API Key'),
validators=[validators.Optional()],
render_kw={
"autocomplete": "off",
# NOT "off": Chrome deliberately ignores autocomplete="off" on type=password,
# so the browser's saved site password was being prefilled here. This field
# renders blank precisely so that submitting it untouched PRESERVES the stored
# key - a prefilled value therefore silently overwrote a working API key on the
# next Save. "new-password" is the value Chrome honours; the data-* attributes
# ask 1Password and LastPass to keep out of it too.
"autocomplete": "new-password",
"data-1p-ignore": "true",
"data-lpignore": "true",
"data-form-type": "other",
# Belt and braces, for when no key is stored yet and the field is editable
# (once one IS stored the template disables it outright). Chrome will not
# autofill a readonly input, and global-settings.js drops the attribute the
# moment the field is focused or clicked. readonly rather than disabled here,
# because a disabled input is not submitted and the key could never be set.
"readonly": True,
"data-unlock-on-interact": "1",
"style": "width: 24em;",
},
)
@@ -1,4 +1,16 @@
$(document).ready(function () {
// Secret fields (the LLM API key) render with readonly set, because Chrome ignores
// autocomplete="off" on type=password and will happily drop the saved site password in.
// That is destructive rather than merely annoying: the field renders blank so that
// saving it untouched PRESERVES the stored key, so anything prefilled silently replaces
// a working key on the next Save, with no copy kept anywhere to restore from.
//
// readonly rather than disabled - a disabled input is never submitted, so the key could
// never be set at all. Unlocked on focus as well as click so keyboard tabbing works.
$('[data-unlock-on-interact]').on('focus click', function () {
$(this).removeAttr('readonly');
});
$("#api-key").hover(
function () {
$("#api-key-copy").html('copy').fadeIn();
@@ -13,6 +13,7 @@ API endpoint's response body for the key string.
"""
import json
from pathlib import Path
from flask import url_for
@@ -20,6 +21,11 @@ from changedetectionio.tests.util import live_server_setup, delete_all_watches
CANARY_KEY = 'sk-CANARY-SECRET-DO-NOT-EXPOSE-12345'
# Resolved from this file rather than the working directory: CI does not run pytest from
# the repo root, and a cwd-relative path turns a security assertion into a FileNotFoundError
# there. tests/ -> the changedetectionio package.
APP_BASE = Path(__file__).resolve().parents[1]
def _configure_llm(datastore, api_key=CANARY_KEY):
"""Inject a recognisable API key into the datastore LLM settings."""
@@ -32,6 +38,16 @@ def _configure_llm(datastore, api_key=CANARY_KEY):
})
def _element(page: bytes, element_id: bytes, tag: bytes = b'<input') -> bytes:
"""Return the whole opening tag carrying `element_id`.
Not page.split(id)[1] - WTForms emits attributes alphabetically, so `autocomplete` and
`disabled` land BEFORE `id=` and slicing forwards from the id silently misses them.
"""
i = page.index(element_id)
return page[page.rindex(tag, 0, i):page.index(b'>', i) + 1]
def _api_token(client):
return client.application.config.get('DATASTORE').data['settings']['application'].get('api_access_token')
@@ -353,6 +369,126 @@ def test_settings_form_preserves_api_key_when_submitted_blank(
delete_all_watches(client)
def test_settings_form_preserves_api_key_when_field_is_absent(
client, live_server, measure_memory_usage, datastore_path):
"""
Once a key is stored the field renders disabled, and a disabled input is not submitted
at all - so the save path sees no llm-api_key key whatsoever, not an empty one.
That must preserve the stored key. It does, because the form is built with data=default
and the merge is {**stored, **form_input}, but it is exactly the kind of thing a later
refactor breaks silently: the symptom is a working API key replaced by nothing, with no
copy kept anywhere to restore it from.
"""
ds = client.application.config.get('DATASTORE')
_configure_llm(ds, api_key='sk-should-survive-absence')
res = client.post(
url_for('settings.settings_page'),
data={
'llm-model': 'gpt-4o',
# no 'llm-api_key' at all — this is what a disabled input submits
'llm-api_base': '',
'application-pager_size': '50',
'application-notification_format': 'System default',
'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-workers': '10',
'requests-timeout': '60',
},
follow_redirects=True,
)
assert res.status_code == 200
saved_key = ds.data['settings']['application'].get('llm', {}).get('api_key', '')
assert saved_key == 'sk-should-survive-absence', \
f"An absent llm-api_key must not clear the stored key (got '{saved_key}')"
delete_all_watches(client)
def test_datastore_never_ends_up_with_a_blank_key(
client, live_server, measure_memory_usage, datastore_path):
"""A stored key must never be replaced by an empty/whitespace value through the form.
Whatever the field submits - missing, empty, or whitespace - the stored key stands. The
only supported way to clear it is "Remove provider" (llm_clear).
"""
ds = client.application.config.get('DATASTORE')
base = {
'llm-model': 'gpt-4o',
'llm-api_base': '',
'application-pager_size': '50',
'application-notification_format': 'System default',
'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-workers': '10',
'requests-timeout': '60',
}
for label, extra in (('absent', {}), ('empty', {'llm-api_key': ''}),
('whitespace', {'llm-api_key': ' '})):
_configure_llm(ds, api_key='sk-original')
client.post(url_for('settings.settings_page'), data={**base, **extra},
follow_redirects=True)
saved = ds.data['settings']['application'].get('llm', {}).get('api_key', '')
assert saved == 'sk-original', f"{label} submission blanked the stored key (got '{saved}')"
delete_all_watches(client)
def test_provider_and_key_are_locked_once_a_key_is_stored(
client, live_server, measure_memory_usage, datastore_path):
"""Locked means disabled, not readonly: a disabled input never reaches the POST, so no
autofilled or mistyped value can overwrite the key."""
ds = client.application.config.get('DATASTORE')
_configure_llm(ds, api_key='sk-locked')
page = client.get(url_for('settings.settings_page')).data
key_field = _element(page, b'id="llm-api_key"')
assert b'disabled' in key_field, "stored key must render the field disabled"
provider = _element(page, b'id="llm-provider"', tag=b'<select')
assert b'disabled' in provider, "provider select must be locked alongside the key"
assert b'Remove provider' in page, "the only supported way out of the locked state"
delete_all_watches(client)
def test_provider_and_key_are_editable_when_nothing_is_stored(
client, live_server, measure_memory_usage, datastore_path):
"""The lock must not strand a fresh install with fields it can never fill in."""
page = client.get(url_for('settings.settings_page')).data
key_field = _element(page, b'id="llm-api_key"')
assert b'disabled' not in key_field, "no stored key - the field has to be usable"
provider = _element(page, b'id="llm-provider"', tag=b'<select')
assert b'disabled' not in provider
# While editable it still must not be autofillable. Chrome ignores autocomplete="off"
# on type=password, so a saved site password lands in the box and the next Save writes
# it over the real key. readonly (Chrome won't autofill one) is dropped by
# global-settings.js on first focus or click.
assert b'autocomplete="new-password"' in key_field, \
'Chrome ignores autocomplete="off" on password inputs'
assert b'autocomplete="off"' not in key_field
assert b'readonly' in key_field
assert b'data-unlock-on-interact' in key_field, "nothing would remove the readonly"
script = (APP_BASE / 'static' / 'js' / 'global-settings.js').read_bytes()
assert b'data-unlock-on-interact' in script and b"removeAttr('readonly')" in script, \
"a readonly field that nothing unlocks is just a broken field"
# ---------------------------------------------------------------------------
# SSRF — api_base must reject private/loopback/reserved hosts (GHSA-jrxm-qjfh-g54f)
# ---------------------------------------------------------------------------
@@ -1055,6 +1055,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1112,6 +1116,10 @@ msgstr "Odstranit"
msgid "Cancel"
msgstr "Zrušit"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "Otestovat připojení"
@@ -1071,6 +1071,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1128,6 +1132,10 @@ msgstr ""
msgid "Cancel"
msgstr "Abbrechen"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1053,6 +1053,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1110,6 +1114,10 @@ msgstr ""
msgid "Cancel"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1053,6 +1053,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1110,6 +1114,10 @@ msgstr ""
msgid "Cancel"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1091,6 +1091,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1148,6 +1152,10 @@ msgstr ""
msgid "Cancel"
msgstr "Cancelar"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1059,6 +1059,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1116,6 +1120,10 @@ msgstr ""
msgid "Cancel"
msgstr "Annuler"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1117,6 +1117,10 @@ msgstr "pilih penyedia"
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr "Kompatibel dengan OpenAI (vLLM, LM Studio, llama.cpp)"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr "Hanya diperlukan untuk Ollama atau endpoint kustom/self-hosted. Biarkan kosong untuk penyedia cloud."
@@ -1179,6 +1183,10 @@ msgstr "Hapus"
msgid "Cancel"
msgstr "Batal"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "Uji koneksi"
@@ -1055,6 +1055,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1112,6 +1116,10 @@ msgstr ""
msgid "Cancel"
msgstr "Annulla"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1060,6 +1060,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1117,6 +1121,10 @@ msgstr ""
msgid "Cancel"
msgstr "キャンセル"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1061,6 +1061,10 @@ msgstr "프로바이더 선택"
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr "Ollama 또는 사용자 지정/자체 호스팅 엔드포인트에만 필요합니다. 클라우드 프로바이더는 비워 두세요."
@@ -1118,6 +1122,10 @@ msgstr "제거"
msgid "Cancel"
msgstr "취소"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "연결 테스트"
+9 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.60.7\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-09-20 08:41+0200\n"
"POT-Creation-Date: 2026-09-20 17:57+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"
@@ -1052,6 +1052,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1109,6 +1113,10 @@ msgstr ""
msgid "Cancel"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1145,6 +1145,10 @@ msgstr "wybierz dostawcę"
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr "Zgodność z OpenAI (vLLM, LM Studio, llama.cpp)"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1210,6 +1214,10 @@ msgstr "Usuń"
msgid "Cancel"
msgstr "Anuluj"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "Sprawdź połączenie"
@@ -1078,6 +1078,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1135,6 +1139,10 @@ msgstr ""
msgid "Cancel"
msgstr "Cancelar"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1119,6 +1119,10 @@ msgstr "выберите провайдера"
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr "Совместимость с OpenAI (vLLM, LM Studio, llama.cpp)"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1184,6 +1188,10 @@ msgstr "Удалять"
msgid "Cancel"
msgstr "Отмена"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "Тестовое соединение"
@@ -1088,6 +1088,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1145,6 +1149,10 @@ msgstr ""
msgid "Cancel"
msgstr "İptal"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1068,6 +1068,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1125,6 +1129,10 @@ msgstr ""
msgid "Cancel"
msgstr "Скасувати"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1058,6 +1058,10 @@ msgstr ""
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr ""
@@ -1115,6 +1119,10 @@ msgstr ""
msgid "Cancel"
msgstr "取消"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr ""
@@ -1059,6 +1059,10 @@ msgstr "選擇供應商"
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
msgstr "與 OpenAI 相容(vLLM、LM Studio、llama.cpp)"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Saved. Use \"Remove provider\" below to change the provider or key."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers."
msgstr "僅適用於 Ollama 或自訂 / 自行架設的端點。雲端供應商請留空。"
@@ -1119,6 +1123,10 @@ msgstr "移除"
msgid "Cancel"
msgstr "取消"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Remove provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Test connection"
msgstr "測試連線"