mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-07 04:22:01 +00:00
UI - Prevent accidental change of LLM Key (#4473)
This commit is contained in:
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') }}">
|
||||
✕ {{ _('Remove') }}
|
||||
✕ {{ _('Remove provider') }}
|
||||
</a>
|
||||
|
||||
<button type="button" id="llm-test-btn" class="cdio-btn button-xsmall" onclick="llmRunTest()"
|
||||
|
||||
@@ -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 "연결 테스트"
|
||||
|
||||
@@ -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 "測試連線"
|
||||
|
||||
Reference in new issue
Block a user