mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-05-30 05:20:57 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d7eb0de71 | |||
| 33c150fe7b | |||
| a2fa9a9e7b | |||
| 972d1206e8 | |||
| bbf56e2253 | |||
| dfc6eaf340 |
@@ -30,7 +30,7 @@ Stop drowning in noise. Connect any LLM (OpenAI, Gemini, Anthropic, Ollama and m
|
||||
|
||||
**AI change summaries** — instead of staring at a raw diff, your notification reads _"Price dropped from $89.99 to $67.00"_ or _"3 new products added to the listing"_. Works globally or per-watch, with full control over the prompt.
|
||||
|
||||
Works with any model you already pay for — GPT-4o-mini and Gemini Flash handle this well at fractions of a cent per check. Or run it entirely locally with Ollama. Powered by [LiteLLM](https://github.com/BerriAI/litellm), giving you seamless access to [100+ supported providers and models](https://docs.litellm.ai/docs/providers).
|
||||
Works with any model you already pay for — GPT-4o-mini and Gemini Flash handle this well at fractions of a cent per check. Or run it entirely locally with **Ollama**, **vLLM**, **LM Studio**, or any **OpenAI-compatible self-hosted endpoint** — pick the *OpenAI-compatible (vLLM, LM Studio, llama.cpp)* option in the provider dropdown and point it at your server's `/v1` URL. Powered by [LiteLLM](https://github.com/BerriAI/litellm), giving you seamless access to [100+ supported providers and models](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
[<img src="./docs/LLM-change-summary.jpeg" style="max-width:100%;" alt="AI-powered website change detection — plain language change summaries and smart alert rules" title="AI website change detection with LLM change summaries and intelligent alert filtering" />](https://changedetection.io?src=github)
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
default['llm'] = {
|
||||
'llm_model': _stored_llm.get('model', ''),
|
||||
'llm_api_base': _stored_llm.get('api_base', ''),
|
||||
'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_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),
|
||||
@@ -148,6 +150,10 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
'model': (llm_data.get('llm_model') or '').strip(),
|
||||
'api_key': effective_api_key,
|
||||
'api_base': (llm_data.get('llm_api_base') or '').strip(),
|
||||
# Identifies a self-hosted OpenAI-compatible endpoint so reasoning-friendly
|
||||
# token caps can be applied conditionally (cloud-LLM defaults stay tight).
|
||||
'provider_kind': (llm_data.get('llm_provider_kind') or '').strip(),
|
||||
'local_token_multiplier': int(llm_data.get('llm_local_token_multiplier') or 5),
|
||||
'token_budget_month': existing_llm.get('token_budget_month', 0),
|
||||
'max_input_chars': existing_llm.get('max_input_chars', 0),
|
||||
**preserved_counters,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from flask import Blueprint, jsonify, redirect, url_for, flash
|
||||
from flask_babel import gettext
|
||||
@@ -8,6 +11,44 @@ from changedetectionio.store import ChangeDetectionStore
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
|
||||
|
||||
class _LiteLLMWarningCapture(logging.Handler):
|
||||
"""Capture warnings emitted on the 'LiteLLM' stdlib logger during a single call.
|
||||
|
||||
litellm.get_valid_models() catches HTTP/auth errors internally, logs a warning,
|
||||
and returns []. Without capturing that warning we can't tell the user *why*
|
||||
no models came back (bad key vs. offline vs. genuinely empty model list).
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(level=logging.WARNING)
|
||||
self.messages = []
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
self.messages.append(record.getMessage())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _humanize_litellm_error(raw: str) -> str:
|
||||
# litellm warnings typically look like:
|
||||
# "Error getting valid models: Failed to get models: { 'error': { 'message': '...' } }"
|
||||
# Pull the inner provider message when present; otherwise trim the boilerplate.
|
||||
if not raw:
|
||||
return raw
|
||||
m = re.search(r'\{.*\}', raw, re.DOTALL)
|
||||
if m:
|
||||
try:
|
||||
body = json.loads(m.group(0))
|
||||
inner = (body.get('error') or {}).get('message') or body.get('message')
|
||||
if inner:
|
||||
return inner
|
||||
except Exception:
|
||||
pass
|
||||
cleaned = re.sub(r'^Error getting valid models:\s*', '', raw)
|
||||
cleaned = re.sub(r'^Failed to get models:\s*', '', cleaned).strip()
|
||||
return cleaned[:500]
|
||||
|
||||
|
||||
def construct_llm_blueprint(datastore: ChangeDetectionStore):
|
||||
llm_blueprint = Blueprint('llm', __name__)
|
||||
|
||||
@@ -30,19 +71,38 @@ def construct_llm_blueprint(datastore: ChangeDetectionStore):
|
||||
api_key = (datastore.data['settings']['application'].get('llm') or {}).get('api_key', '')
|
||||
logger.debug("LLM model list: no api_key in request, using stored key")
|
||||
|
||||
_PREFIXES = {'gemini': 'gemini/', 'ollama': 'ollama/', 'openrouter': 'openrouter/'}
|
||||
_PREFIXES = {'gemini': 'gemini/', 'ollama': 'ollama/', 'openrouter': 'openrouter/',
|
||||
'openai_compatible': 'openai/'}
|
||||
# vLLM / LM Studio / llama.cpp speak OpenAI's wire format — route through litellm's
|
||||
# 'openai' provider but keep the UI-level name distinct from cloud OpenAI.
|
||||
_LITELLM_PROVIDER = {'openai_compatible': 'openai'}
|
||||
prefix = _PREFIXES.get(provider, '')
|
||||
litellm_provider = _LITELLM_PROVIDER.get(provider, provider)
|
||||
|
||||
try:
|
||||
import litellm
|
||||
logger.debug(f"LLM model list: calling litellm.get_valid_models provider={provider!r} api_base={api_base!r}")
|
||||
raw = litellm.get_valid_models(
|
||||
check_provider_endpoint=True,
|
||||
custom_llm_provider=provider,
|
||||
api_key=api_key or None,
|
||||
api_base=api_base or None,
|
||||
) or []
|
||||
logger.debug(f"LLM model list: calling litellm.get_valid_models provider={provider!r} (litellm={litellm_provider!r}) api_base={api_base!r}")
|
||||
|
||||
capture = _LiteLLMWarningCapture()
|
||||
litellm_logger = logging.getLogger('LiteLLM')
|
||||
litellm_logger.addHandler(capture)
|
||||
try:
|
||||
raw = litellm.get_valid_models(
|
||||
check_provider_endpoint=True,
|
||||
custom_llm_provider=litellm_provider,
|
||||
api_key=api_key or None,
|
||||
api_base=api_base or None,
|
||||
) or []
|
||||
finally:
|
||||
litellm_logger.removeHandler(capture)
|
||||
|
||||
models = sorted({(m if m.startswith(prefix) else prefix + m) for m in raw})
|
||||
|
||||
if not models and capture.messages:
|
||||
err = _humanize_litellm_error(capture.messages[-1])
|
||||
logger.debug(f"LLM model list: 0 models, surfacing captured litellm warning: {err!r}")
|
||||
return jsonify({'models': [], 'error': err}), 400
|
||||
|
||||
logger.debug(f"LLM model list: got {len(models)} models for provider={provider!r}")
|
||||
return jsonify({'models': models, 'error': None})
|
||||
except Exception as e:
|
||||
@@ -67,14 +127,18 @@ def construct_llm_blueprint(datastore: ChangeDetectionStore):
|
||||
|
||||
try:
|
||||
logger.debug(f"LLM connection test: sending test prompt to model={model!r}")
|
||||
# Reuse the same multiplier path the production calls use, so cloud providers
|
||||
# stay on a small base cap (matching upstream's pre-existing behavior) and only
|
||||
# 'openai_compatible' endpoints opt into the reasoning-friendly headroom.
|
||||
from changedetectionio.llm.evaluator import apply_local_token_multiplier
|
||||
text, total_tokens, input_tokens, output_tokens = completion(
|
||||
model=model,
|
||||
messages=[{'role': 'user', 'content':
|
||||
'Reply with exactly five words confirming you are ready.'}],
|
||||
'Respond with just the word: ready'}],
|
||||
api_key=llm_cfg.get('api_key') or None,
|
||||
api_base=api_base or None,
|
||||
timeout=20,
|
||||
max_tokens=200,
|
||||
timeout=30,
|
||||
max_tokens=apply_local_token_multiplier(200, llm_cfg),
|
||||
)
|
||||
reply = text.strip()
|
||||
if not reply:
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
</optgroup>
|
||||
<optgroup label="{{ _('Local / Self-hosted') }}">
|
||||
<option value="ollama">Ollama (local)</option>
|
||||
<option value="openai_compatible">{{ _('OpenAI-compatible (vLLM, LM Studio, llama.cpp)') }}</option>
|
||||
</optgroup>
|
||||
<optgroup label="OpenRouter">
|
||||
<option value="openrouter">OpenRouter (200+ models)</option>
|
||||
@@ -127,6 +128,18 @@
|
||||
<span class="pure-form-message-inline">{{ _('Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers.') }}</span>
|
||||
</div>
|
||||
|
||||
{# Hidden field carrying the dropdown selection so the backend knows when to apply
|
||||
reasoning-friendly token caps (only for self-hosted OpenAI-compatible endpoints). #}
|
||||
{{ form.llm.form.llm_provider_kind() }}
|
||||
|
||||
<div class="pure-control-group" id="llm-local-advanced-group" style="display:none">
|
||||
<label for="{{ form.llm.form.llm_local_token_multiplier.id }}">{{ form.llm.form.llm_local_token_multiplier.label.text }}</label>
|
||||
{{ form.llm.form.llm_local_token_multiplier() }}
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to %(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps.', default='5x') | safe }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group" id="llm-fetch-group" style="display:none">
|
||||
<label></label>
|
||||
<button type="button" id="llm-fetch-btn" class="pure-button button-xsmall" onclick="llmFetchModels()"
|
||||
@@ -377,14 +390,15 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const LIVE_PROVIDERS = ['openai', 'anthropic', 'gemini', 'ollama', 'openrouter'];
|
||||
const LIVE_PROVIDERS = ['openai', 'anthropic', 'gemini', 'ollama', 'openai_compatible', 'openrouter'];
|
||||
const BASE_DEFAULTS = { ollama: 'http://localhost:11434' };
|
||||
const KEY_HINTS = {
|
||||
openai: '{{ _("platform.openai.com → API keys") }}',
|
||||
anthropic: '{{ _("console.anthropic.com → API keys") }}',
|
||||
gemini: '{{ _("aistudio.google.com → Get API key") }}',
|
||||
ollama: '{{ _("No API key needed for local Ollama") }}',
|
||||
openrouter: '{{ _("openrouter.ai → Keys") }}',
|
||||
openai: '{{ _("platform.openai.com → API keys") }}',
|
||||
anthropic: '{{ _("console.anthropic.com → API keys") }}',
|
||||
gemini: '{{ _("aistudio.google.com → Get API key") }}',
|
||||
ollama: '{{ _("No API key needed for local Ollama") }}',
|
||||
openai_compatible: '{{ _("Bearer token for your self-hosted server (vLLM, LM Studio, etc.)") }}',
|
||||
openrouter: '{{ _("openrouter.ai → Keys") }}',
|
||||
};
|
||||
|
||||
window.llmDisclaimerToggle = function (cb) {
|
||||
@@ -393,20 +407,31 @@
|
||||
};
|
||||
|
||||
window.llmOnProviderChange = function (provider) {
|
||||
const fetchGroup = document.getElementById('llm-fetch-group');
|
||||
const baseGroup = document.getElementById('llm-base-group');
|
||||
const modelSelGrp = document.getElementById('llm-model-select-group');
|
||||
const baseField = document.querySelector('[name="llm-llm_api_base"]');
|
||||
const hint = document.getElementById('llm-key-hint');
|
||||
const fetchGroup = document.getElementById('llm-fetch-group');
|
||||
const baseGroup = document.getElementById('llm-base-group');
|
||||
const modelSelGrp = document.getElementById('llm-model-select-group');
|
||||
const localAdvGrp = document.getElementById('llm-local-advanced-group');
|
||||
const baseField = document.querySelector('[name="llm-llm_api_base"]');
|
||||
const kindField = document.querySelector('[name="llm-llm_provider_kind"]');
|
||||
const hint = document.getElementById('llm-key-hint');
|
||||
|
||||
fetchGroup.style.display = LIVE_PROVIDERS.includes(provider) ? '' : 'none';
|
||||
|
||||
const needsBase = provider === 'ollama';
|
||||
const needsBase = provider === 'ollama' || provider === 'openai_compatible';
|
||||
baseGroup.style.display = needsBase ? '' : 'none';
|
||||
if (BASE_DEFAULTS[provider] !== undefined) {
|
||||
if (!baseField.value) baseField.value = BASE_DEFAULTS[provider];
|
||||
}
|
||||
|
||||
// Persist the dropdown selection so the backend can branch on provider kind
|
||||
// (currently only 'openai_compatible' triggers the local-multiplier code path).
|
||||
if (kindField) kindField.value = provider || '';
|
||||
|
||||
// Show the local-endpoint advanced settings (token multiplier) only for the
|
||||
// OpenAI-compatible self-hosted option. Cloud providers and Ollama get the
|
||||
// original tight caps and don't see this section at all.
|
||||
if (localAdvGrp) localAdvGrp.style.display = (provider === 'openai_compatible') ? '' : 'none';
|
||||
|
||||
hint.textContent = KEY_HINTS[provider] || '';
|
||||
modelSelGrp.style.display = 'none';
|
||||
document.getElementById('llm-fetch-status').textContent = '';
|
||||
@@ -444,7 +469,7 @@
|
||||
|
||||
if (!data.models || data.models.length === 0) {
|
||||
statusEl.style.color = '#e67e22';
|
||||
statusEl.textContent = '{{ _("No models returned — check your API key.") }}';
|
||||
statusEl.textContent = '{{ _("No models returned by the provider.") }}';
|
||||
selGroup.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -516,6 +541,11 @@
|
||||
if (m.startsWith('gemini/')) guessed = 'gemini';
|
||||
else if (m.startsWith('ollama/')) guessed = 'ollama';
|
||||
else if (m.startsWith('openrouter/')) guessed = 'openrouter';
|
||||
else if (m.startsWith('openai/')) {
|
||||
// openai/<model> + custom api_base = self-hosted OpenAI-compatible (vLLM etc.)
|
||||
const baseField = document.querySelector('[name="llm-llm_api_base"]');
|
||||
guessed = (baseField && baseField.value.trim()) ? 'openai_compatible' : 'openai';
|
||||
}
|
||||
else if (m.startsWith('claude')) guessed = 'anthropic';
|
||||
else if (m.startsWith('gpt') || m.startsWith('o1') || m.startsWith('o3')) guessed = 'openai';
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ window.watchOverviewI18n = {
|
||||
{#last_checked becomes fetch-start-time#}
|
||||
<td class="last-checked" data-timestamp="{{ watch.last_checked }}" data-fetchduration={{ watch.fetch_time }} data-eta_complete="{{ watch.last_checked+watch.fetch_time }}" data-label="{{ _('Last Checked') }}">
|
||||
<div class="spinner-wrapper" style="display:none;" >
|
||||
<span class="spinner"></span><span class="status-text"> {{ _('Checking now') }}</span>
|
||||
<span class="spinner"></span><span class="status-text"> {{ watch['__check_status'] or _('Checking now') }}</span>
|
||||
</div>
|
||||
<span class="innertext">{{watch|format_last_checked_time|safe}}</span>
|
||||
</td>
|
||||
|
||||
@@ -17,6 +17,7 @@ from wtforms import (
|
||||
Form,
|
||||
Field,
|
||||
FloatField,
|
||||
HiddenField,
|
||||
IntegerField,
|
||||
PasswordField,
|
||||
RadioField,
|
||||
@@ -279,12 +280,44 @@ class TimeBetweenCheckForm(Form):
|
||||
return True
|
||||
|
||||
|
||||
class LabelAfterInputTableWidget(widgets.TableWidget):
|
||||
"""
|
||||
Variant of WTForms' TableWidget that renders the input cell before the label cell,
|
||||
so each row is <td>input</td><th>label</th> instead of the default <th>label</th><td>input</td>.
|
||||
"""
|
||||
|
||||
def __call__(self, field, **kwargs):
|
||||
from markupsafe import Markup
|
||||
from wtforms.widgets import html_params
|
||||
|
||||
html = []
|
||||
if self.with_table_tag:
|
||||
kwargs.setdefault("id", field.id)
|
||||
html.append(f"<table {html_params(**kwargs)}>")
|
||||
hidden = ""
|
||||
for subfield in field:
|
||||
if subfield.type in ("HiddenField", "CSRFTokenField"):
|
||||
hidden += str(subfield)
|
||||
else:
|
||||
html.append(
|
||||
f"<tr><td>{hidden}{subfield}</td><th>{subfield.label}</th></tr>"
|
||||
)
|
||||
hidden = ""
|
||||
if self.with_table_tag:
|
||||
html.append("</table>")
|
||||
if hidden:
|
||||
html.append(hidden)
|
||||
return Markup("".join(html))
|
||||
|
||||
|
||||
class EnhancedFormField(FormField):
|
||||
"""
|
||||
An enhanced FormField that supports conditional validation with top-level error messages.
|
||||
Adds a 'top_errors' property for validation errors at the FormField level.
|
||||
"""
|
||||
|
||||
widget = LabelAfterInputTableWidget()
|
||||
|
||||
def __init__(self, form_class, label=None, validators=None, separator="-",
|
||||
conditional_field=None, conditional_message=None, conditional_test_function=None, **kwargs):
|
||||
"""
|
||||
@@ -1073,7 +1106,6 @@ class globalSettingsLLMForm(Form):
|
||||
_l('API Key'),
|
||||
validators=[validators.Optional()],
|
||||
render_kw={
|
||||
"placeholder": _l('Leave blank to use LITELLM_API_KEY env var'),
|
||||
"autocomplete": "off",
|
||||
"style": "width: 24em;",
|
||||
},
|
||||
@@ -1086,6 +1118,24 @@ class globalSettingsLLMForm(Form):
|
||||
"style": "width: 24em;",
|
||||
},
|
||||
)
|
||||
# Persisted by the Provider dropdown JS — lets the backend distinguish a self-hosted
|
||||
# OpenAI-compatible endpoint (vLLM, LM Studio, llama.cpp) from cloud OpenAI, so we can
|
||||
# apply reasoning-friendly token caps only when the user opted in.
|
||||
llm_provider_kind = HiddenField(
|
||||
validators=[validators.Optional()],
|
||||
default='',
|
||||
)
|
||||
# Multiplier applied to LLM max_tokens caps when provider_kind == 'openai_compatible'.
|
||||
# Reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought into
|
||||
# message.reasoning_content before the final answer lands in message.content.
|
||||
# Local self-hosted models cost no per-token money, so giving them headroom is cheap;
|
||||
# cloud providers stay on the original tight caps so existing users see no cost change.
|
||||
llm_local_token_multiplier = IntegerField(
|
||||
_l('Token multiplier for local reasoning models'),
|
||||
validators=[validators.Optional(), validators.NumberRange(min=1, max=20)],
|
||||
default=5,
|
||||
render_kw={"placeholder": "5", "style": "width: 6em;"},
|
||||
)
|
||||
llm_change_summary_default = TextAreaField(
|
||||
_l('Default AI Change Summary prompt'),
|
||||
validators=[validators.Optional(), validators.Length(max=2000)],
|
||||
|
||||
@@ -49,6 +49,9 @@ def completion(model: str, messages: list, api_key: str = None,
|
||||
|
||||
_retryable = (litellm.Timeout, litellm.APIConnectionError)
|
||||
|
||||
logger.trace("Sending payload to LLM.. ")
|
||||
logger.trace(messages)
|
||||
|
||||
for attempt in range(1, DEFAULT_RETRIES + 1):
|
||||
try:
|
||||
response = litellm.completion(**kwargs)
|
||||
|
||||
@@ -81,6 +81,11 @@ def _cached_system(text: str, model: str = '') -> dict:
|
||||
|
||||
LLM_DEFAULT_MAX_SUMMARY_TOKENS = 3000
|
||||
|
||||
# Output-token cap for the JSON-returning calls (intent eval, preview, setup/prefilter).
|
||||
# Mirrors client.py's _MAX_COMPLETION_TOKENS so the multiplier helper has a base value
|
||||
# to scale; cloud-LLM users hit this default unmodified, preserving prior cost defaults.
|
||||
JSON_RESPONSE_MAX_TOKENS = 400
|
||||
|
||||
# Default prompt used when the user hasn't configured llm_change_summary
|
||||
DEFAULT_CHANGE_SUMMARY_PROMPT = "Describe in plain English what changed — list what was added or removed as bullet points, including key details for each item. Be careful of content that merely just moved around, you should mention that it moved but dont report that it was added/removed etc. Be considerate of the style content you are summarising the change of, adjust your report accordingly. Do not quote non-English text verbatim; translate and summarise all content into English. Your entire response must be in English."
|
||||
|
||||
@@ -90,6 +95,37 @@ def _summary_max_tokens(diff: str, max_cap: int = LLM_DEFAULT_MAX_SUMMARY_TOKENS
|
||||
return max(400, min(len(diff) // 4, max_cap))
|
||||
|
||||
|
||||
def apply_local_token_multiplier(base_max_tokens: int, llm_cfg: dict) -> int:
|
||||
"""
|
||||
Scale max_tokens for self-hosted OpenAI-compatible endpoints (vLLM, LM Studio, llama.cpp).
|
||||
|
||||
Reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought into
|
||||
`message.reasoning_content` BEFORE the final answer lands in `message.content`.
|
||||
Without enough headroom the request truncates mid-thought (`finish_reason='length'`)
|
||||
and the answer never lands — callers see an empty string and silently fall through
|
||||
to safe defaults, hiding the problem.
|
||||
|
||||
Local self-hosted models cost no per-token money, so headroom is cheap; cloud
|
||||
providers (OpenAI, Anthropic, Gemini, OpenRouter) keep their original tight caps
|
||||
so existing users see no cost change.
|
||||
|
||||
Activated only when `llm_cfg['provider_kind'] == 'openai_compatible'`.
|
||||
Multiplier defaults to 5x and is user-configurable in Settings → AI → Provider.
|
||||
"""
|
||||
if (llm_cfg or {}).get('provider_kind') != 'openai_compatible':
|
||||
return base_max_tokens
|
||||
try:
|
||||
multiplier = int(llm_cfg.get('local_token_multiplier') or 5)
|
||||
except (TypeError, ValueError):
|
||||
multiplier = 5
|
||||
# Clamp to the same 1-20 range the form enforces. Defense-in-depth against
|
||||
# corrupted datastore values that bypassed form validation (manual JSON edits,
|
||||
# future migrations, plugins): a runaway multiplier could otherwise produce
|
||||
# absurdly large max_tokens caps and exhaust local-endpoint memory.
|
||||
multiplier = max(1, min(multiplier, 20))
|
||||
return base_max_tokens * multiplier
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Intent resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -338,6 +374,7 @@ def run_setup(watch, datastore, snapshot_text: str) -> None:
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
max_tokens=apply_local_token_multiplier(JSON_RESPONSE_MAX_TOKENS, cfg),
|
||||
extra_body=_thinking_extra_body(cfg['model'], int(datastore.data['settings']['application'].get('llm_thinking_budget', LLM_DEFAULT_THINKING_BUDGET) or 0)),
|
||||
)
|
||||
_check_token_budget(watch, cfg, tokens)
|
||||
@@ -431,9 +468,12 @@ def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
max_tokens=_summary_max_tokens(
|
||||
diff,
|
||||
max_cap=int(datastore.data['settings']['application'].get('llm_max_summary_tokens', LLM_DEFAULT_MAX_SUMMARY_TOKENS) or LLM_DEFAULT_MAX_SUMMARY_TOKENS),
|
||||
max_tokens=apply_local_token_multiplier(
|
||||
_summary_max_tokens(
|
||||
diff,
|
||||
max_cap=int(datastore.data['settings']['application'].get('llm_max_summary_tokens', LLM_DEFAULT_MAX_SUMMARY_TOKENS) or LLM_DEFAULT_MAX_SUMMARY_TOKENS),
|
||||
),
|
||||
cfg,
|
||||
),
|
||||
extra_body=_extra_body,
|
||||
)
|
||||
@@ -496,6 +536,7 @@ def preview_extract(watch, datastore, content: str) -> dict | None:
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
max_tokens=apply_local_token_multiplier(JSON_RESPONSE_MAX_TOKENS, cfg),
|
||||
extra_body=_thinking_extra_body(cfg['model'], int(datastore.data['settings']['application'].get('llm_thinking_budget', LLM_DEFAULT_THINKING_BUDGET) or 0)),
|
||||
)
|
||||
accumulate_global_tokens(datastore, tokens, model=cfg['model'])
|
||||
@@ -579,6 +620,7 @@ def evaluate_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
max_tokens=apply_local_token_multiplier(JSON_RESPONSE_MAX_TOKENS, cfg),
|
||||
extra_body=_thinking_extra_body(cfg['model'], int(datastore.data['settings']['application'].get('llm_thinking_budget', LLM_DEFAULT_THINKING_BUDGET) or 0)),
|
||||
)
|
||||
raw, tokens = _resp[0], _resp[1]
|
||||
|
||||
@@ -1064,6 +1064,7 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
Prepare watch data for commit.
|
||||
|
||||
Excludes processor_config_* keys (stored in separate files).
|
||||
Excludes __-prefixed keys (transient in-memory state — must not persist to disk).
|
||||
Normalizes browser_steps to empty list if no meaningful steps.
|
||||
"""
|
||||
import copy
|
||||
@@ -1077,8 +1078,11 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
else:
|
||||
snapshot = dict(self)
|
||||
|
||||
# Exclude processor config keys (stored separately)
|
||||
watch_dict = {k: copy.deepcopy(v) for k, v in snapshot.items() if not k.startswith('processor_config_')}
|
||||
# Exclude processor config keys (stored separately) and __-prefixed transient keys
|
||||
watch_dict = {
|
||||
k: copy.deepcopy(v) for k, v in snapshot.items()
|
||||
if not k.startswith('processor_config_') and not k.startswith('__')
|
||||
}
|
||||
|
||||
# Normalize browser_steps: if no meaningful steps, save as empty list
|
||||
if not self.has_browser_steps:
|
||||
|
||||
@@ -335,6 +335,13 @@ class watch_base(dict):
|
||||
if self.__watch_was_edited:
|
||||
return # Already marked as edited
|
||||
|
||||
# __-prefixed keys are transient in-memory state (e.g. __check_status set by
|
||||
# set_watch_minitext_status). They never persist to disk and must not trigger
|
||||
# the edited flag — otherwise just observing a check in progress would force
|
||||
# the next run to bypass the unchanged-content skip.
|
||||
if isinstance(key, str) and key.startswith('__'):
|
||||
return
|
||||
|
||||
# Import from shared schema utilities (no circular dependency)
|
||||
from .schema_utils import get_readonly_watch_fields
|
||||
readonly_fields = get_readonly_watch_fields()
|
||||
|
||||
@@ -13,6 +13,7 @@ import json
|
||||
import re
|
||||
from loguru import logger
|
||||
from changedetectionio.pluggy_interface import hookimpl
|
||||
from changedetectionio.llm.evaluator import apply_local_token_multiplier
|
||||
|
||||
# Injected at startup by inject_datastore_into_plugins()
|
||||
datastore = None
|
||||
@@ -234,7 +235,10 @@ def get_itemprop_availability_override(content, fetcher_name, fetcher_instance,
|
||||
],
|
||||
api_key=llm_cfg.get('api_key'),
|
||||
api_base=llm_cfg.get('api_base'),
|
||||
max_tokens=80,
|
||||
# 80 fits a {price, currency, availability} JSON answer comfortably for cloud
|
||||
# models. Local reasoning models burn most of that on chain-of-thought before
|
||||
# the JSON lands — the multiplier scales it up only when provider_kind says so.
|
||||
max_tokens=apply_local_token_multiplier(80, llm_cfg),
|
||||
)
|
||||
|
||||
accumulate_global_tokens(
|
||||
|
||||
@@ -892,10 +892,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1091,6 +1104,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1104,7 +1121,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3118,11 +3135,11 @@ msgid "API Key"
|
||||
msgstr "API klíč"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -908,10 +908,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1107,6 +1120,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1120,7 +1137,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3170,11 +3187,11 @@ msgid "API Key"
|
||||
msgstr "API-Schlüssel"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -890,10 +890,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1089,6 +1102,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1102,7 +1119,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3112,11 +3129,11 @@ msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -890,10 +890,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1089,6 +1102,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1102,7 +1119,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3112,11 +3129,11 @@ msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -928,10 +928,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1127,6 +1140,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1140,7 +1157,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3185,11 +3202,11 @@ msgid "API Key"
|
||||
msgstr "Clave API"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -896,10 +896,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1095,6 +1108,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1108,7 +1125,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3125,11 +3142,11 @@ msgid "API Key"
|
||||
msgstr "Clé API"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -892,10 +892,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1091,6 +1104,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1104,7 +1121,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3114,11 +3131,11 @@ msgid "API Key"
|
||||
msgstr "Chiave API"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -897,10 +897,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1096,6 +1109,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1109,7 +1126,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3131,11 +3148,11 @@ msgid "API Key"
|
||||
msgstr "APIキー"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
Binary file not shown.
@@ -898,10 +898,23 @@ msgstr "프로바이더 선택"
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr "로컬 / 자체 호스팅"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 또는 사용자 지정/자체 호스팅 엔드포인트에만 필요합니다. 클라우드 프로바이더는 비워 두세요."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr "사용 가능한 모델 불러오기"
|
||||
@@ -1097,6 +1110,10 @@ msgstr "aistudio.google.com → API 키 받기"
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr "로컬 Ollama에는 API 키가 필요 없습니다"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr "openrouter.ai → 키"
|
||||
@@ -1110,8 +1127,8 @@ msgid "Loading…"
|
||||
msgstr "불러오는 중..."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgstr "반환된 모델이 없습니다. API 키를 확인하세요."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "— choose a model —"
|
||||
@@ -3121,14 +3138,14 @@ msgstr "모델"
|
||||
msgid "API Key"
|
||||
msgstr "API 키"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgstr "LITELLM_API_KEY 환경 변수를 사용하려면 비워 두세요"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgstr "API 기본 URL"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Default AI Change Summary prompt"
|
||||
msgstr "기본 AI 변경 요약 프롬프트"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.3\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-05-02 18:29+0900\n"
|
||||
"POT-Creation-Date: 2026-05-12 11:08+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"
|
||||
@@ -889,10 +889,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1088,6 +1101,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1101,7 +1118,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3111,11 +3128,11 @@ msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -915,10 +915,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1114,6 +1127,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1127,7 +1144,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3162,11 +3179,11 @@ msgid "API Key"
|
||||
msgstr "Chave da API"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -925,10 +925,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1124,6 +1137,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1137,7 +1154,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3165,11 +3182,11 @@ msgid "API Key"
|
||||
msgstr "API Anahtarı"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -905,10 +905,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1104,6 +1117,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1117,7 +1134,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3144,11 +3161,11 @@ msgid "API Key"
|
||||
msgstr "Ключ API"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -894,10 +894,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1093,6 +1106,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1106,7 +1123,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3117,11 +3134,11 @@ msgid "API Key"
|
||||
msgstr "API密钥"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -893,10 +893,23 @@ msgstr ""
|
||||
msgid "Local / Self-hosted"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "OpenAI-compatible (vLLM, LM Studio, llama.cpp)"
|
||||
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 ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Local reasoning models (Qwen3, DeepSeek-R1, Gemma 3, etc.) emit chain-of-thought before the final answer. This "
|
||||
"multiplier scales every <code>max_tokens</code> cap for this endpoint to leave reasoning room. Defaults to "
|
||||
"%(default)s; raise it if responses come back truncated, lower it if you want tighter limits. Only applied to self-"
|
||||
"hosted OpenAI-compatible endpoints — cloud providers (OpenAI, Anthropic, Gemini) keep their original tight caps."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr ""
|
||||
@@ -1092,6 +1105,10 @@ msgstr ""
|
||||
msgid "No API key needed for local Ollama"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Bearer token for your self-hosted server (vLLM, LM Studio, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "openrouter.ai → Keys"
|
||||
msgstr ""
|
||||
@@ -1105,7 +1122,7 @@ msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "No models returned — check your API key."
|
||||
msgid "No models returned by the provider."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
@@ -3116,11 +3133,11 @@ msgid "API Key"
|
||||
msgstr "API 金鑰"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Leave blank to use LITELLM_API_KEY env var"
|
||||
msgid "API Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "API Base URL"
|
||||
msgid "Token multiplier for local reasoning models"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
|
||||
@@ -9,9 +9,16 @@ from changedetectionio.pluggy_interface import apply_update_handler_alter, apply
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Allow alphanumerics, space, and a small set of punctuation that appears in legitimate
|
||||
# status strings ("Querying AI/LLM (intent)..", "Fetching page.."). Anything that could
|
||||
# be HTML-active (<, >, &, ", ', =, ;, {, }, `, \) is stripped.
|
||||
_MINITEXT_STATUS_SAFE_RE = re.compile(r'[^A-Za-z0-9 ().,/:\-]')
|
||||
_MINITEXT_STATUS_MAX_LEN = 80
|
||||
|
||||
from loguru import logger
|
||||
|
||||
# Async version of update_worker
|
||||
@@ -20,6 +27,22 @@ from loguru import logger
|
||||
IN_PYTEST = "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ
|
||||
DEFER_SLEEP_TIME_ALREADY_QUEUED = 0.3 if IN_PYTEST else 10.0
|
||||
|
||||
|
||||
def set_watch_minitext_status(watch, status):
|
||||
"""
|
||||
Set a transient status line for a watch (e.g. "Fetching page..", "Querying AI/LLM..").
|
||||
|
||||
Writes to watch['__check_status'] so a client reloading the page can render the
|
||||
last known status, and fires the realtime signal so already-connected clients
|
||||
update live. __-prefixed key is filtered from disk by Watch._get_commit_data().
|
||||
|
||||
Status is sanitized to alphanumerics, space, and safe punctuation only.
|
||||
"""
|
||||
safe_status = _MINITEXT_STATUS_SAFE_RE.sub('', str(status))[:_MINITEXT_STATUS_MAX_LEN]
|
||||
watch['__check_status'] = safe_status
|
||||
signal('watch_small_status_comment').send(watch_uuid=watch['uuid'], status=safe_status)
|
||||
|
||||
|
||||
async def async_update_worker(worker_id, q, notification_q, app, datastore, executor=None):
|
||||
"""
|
||||
Async worker function that processes watch check jobs from the queue.
|
||||
@@ -159,8 +182,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
# Allow plugins to modify/wrap the update_handler
|
||||
update_handler = apply_update_handler_alter(update_handler, watch, datastore)
|
||||
|
||||
update_signal = signal('watch_small_status_comment')
|
||||
update_signal.send(watch_uuid=uuid, status="Fetching page..")
|
||||
set_watch_minitext_status(watch, "Fetching page..")
|
||||
|
||||
# All fetchers are now async, so call directly
|
||||
await update_handler.call_browser()
|
||||
@@ -446,6 +468,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
# Step 1: AI Change Intent — may suppress notification
|
||||
_llm_intent, _llm_intent_source = resolve_intent(watch, datastore)
|
||||
if _llm_intent:
|
||||
set_watch_minitext_status(watch, "AI/LLM (rules)..")
|
||||
_llm_result = await loop.run_in_executor(
|
||||
executor,
|
||||
lambda diff=_diff_text, snap=contents: evaluate_change(
|
||||
@@ -465,6 +488,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
|
||||
# Step 2: AI Change Summary — runs for any LLM-configured watch with a change
|
||||
if changed_detected:
|
||||
set_watch_minitext_status(watch, "AI/LLM (summary)..")
|
||||
_change_summary = await loop.run_in_executor(
|
||||
executor,
|
||||
lambda diff=_diff_text, snap=contents: summarise_change(
|
||||
@@ -669,6 +693,8 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
finally:
|
||||
# Send completion signal - retrieve by name to ensure thread-safe access
|
||||
if watch:
|
||||
# Clear transient in-memory status — check is done
|
||||
watch.pop('__check_status', None)
|
||||
watch_check_update = signal('watch_check_update')
|
||||
watch_check_update.send(watch_uuid=watch['uuid'])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user