[mod] plugin: AI tab only when activated, user API key, grounding on
Three changes to the ai_summary plugin: - The *AI Summary* preferences tab is only rendered when the plugin is activated in settings.yml. An instance that does not offer AI summaries no longer shows an AI tab at all. The gate is the administrator setting, not the user opt-out, because the per user on/off switch lives inside that tab -- hiding it on opt-out would leave no way to opt back in. - Users can configure an API key for their own LLM server (ai_summary_api_key). The administrator key is still only sent to base_url and the user key only to a server the user configured, so neither key can be captured through the other. The setting is marked secret: credentials are excluded from the preferences URL, which users copy around to transfer or share their preferences. - Grounding summaries on the search results is now the default; the extra cost of the longer prompt is moderate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ce400f993c
commit
8edc368752
+11
-8
@@ -56,11 +56,12 @@ class SettingsAISummary(msgspec.Struct, kw_only=True, forbid_unknown_fields=True
|
||||
llama.cpp started with ``--api-key``, or an LLM server behind an
|
||||
authenticating reverse proxy.
|
||||
|
||||
There is intentionally no user preference for the API key, and the key is
|
||||
**only** sent to :py:obj:`SettingsAISummary.base_url`: a user who points
|
||||
the ``ai_summary_server`` preference at a server of their own gets no
|
||||
``Authorization`` header, so the key can't be captured by a third party
|
||||
(see :py:obj:`searx.plugins.ai_summary._server_api_key`)."""
|
||||
This key is **only** sent to :py:obj:`SettingsAISummary.base_url`: a user
|
||||
who points the ``ai_summary_server`` preference at a server of their own
|
||||
gets no ``Authorization`` header from it, so the key can't be captured by
|
||||
a third party. For their own server, users configure their own key in the
|
||||
``ai_summary_api_key`` preference (see
|
||||
:py:obj:`searx.plugins.ai_summary._server_api_key`)."""
|
||||
|
||||
model: str = ""
|
||||
"""Name of the default model (e.g. ``llama3.2:3b``). If empty, the first
|
||||
@@ -73,10 +74,12 @@ class SettingsAISummary(msgspec.Struct, kw_only=True, forbid_unknown_fields=True
|
||||
and :py:obj:`SettingsAISummary.base_url` is set, the list is requested
|
||||
once at application setup from the LLM server (``GET /v1/models``)."""
|
||||
|
||||
grounding: bool = False
|
||||
grounding: bool = True
|
||||
"""Default of the ``ai_summary_grounding`` user preference: ground the
|
||||
summary on the search results. Users can still opt in/out in the
|
||||
preferences unless that preference is locked."""
|
||||
summary on the search results. Grounded summaries are more accurate and
|
||||
more current at a moderate extra cost (the search results are sent along
|
||||
with the query, so the prompt is longer). Users can still opt in/out in
|
||||
the preferences unless that preference is locked."""
|
||||
|
||||
connect_timeout: float = 5.0
|
||||
"""Timeout (seconds) to establish a TCP connection to the LLM server."""
|
||||
|
||||
+20
-16
@@ -36,9 +36,10 @@ applied to reach an LLM server in the local network.
|
||||
|
||||
A server that requires authentication (e.g. vLLM or llama.cpp started with
|
||||
``--api-key``, or an LLM server behind an authenticating reverse proxy) is
|
||||
configured with an ``api_key``. The key is administrator configuration only:
|
||||
it is never exposed in the preferences and it is only sent to the server in
|
||||
``base_url``, never to a server a user configured (:py:obj:`_server_api_key`).
|
||||
configured with an ``api_key``. The administrator's key is only sent to the
|
||||
server in ``base_url``, never to a server a user configured; for their own
|
||||
server users configure their own key in the ``ai_summary_api_key`` preference
|
||||
(:py:obj:`_server_api_key`).
|
||||
|
||||
Configuration of the defaults (:py:obj:`searx.ai_summary.SettingsAISummary`):
|
||||
|
||||
@@ -133,20 +134,22 @@ def _server_id(url: str) -> tuple[str, str, int, str] | None:
|
||||
return (parsed.scheme, parsed.hostname.lower(), port, path)
|
||||
|
||||
|
||||
def _server_api_key(cfg: SettingsAISummary, server: str) -> str:
|
||||
"""The API key to send to ``server``: the administrator's
|
||||
:py:obj:`cfg.api_key <searx.ai_summary.SettingsAISummary.api_key>` if
|
||||
``server`` *is* the administrator's server, an empty string otherwise.
|
||||
def _server_api_key(cfg: SettingsAISummary, server: str, user_api_key: str = "") -> str:
|
||||
"""The API key to send to ``server``:
|
||||
|
||||
Users can point the ``ai_summary_server`` preference at a server of their
|
||||
own; without this check such a server would be sent the instance's API
|
||||
key, which would hand every user of the instance a way to capture it."""
|
||||
if not cfg.api_key:
|
||||
return ""
|
||||
- the administrator's :py:obj:`cfg.api_key
|
||||
<searx.ai_summary.SettingsAISummary.api_key>` if ``server`` *is* the
|
||||
administrator's server (:py:obj:`cfg.base_url
|
||||
<searx.ai_summary.SettingsAISummary.base_url>`),
|
||||
- otherwise the user's own ``ai_summary_api_key`` preference, which belongs
|
||||
to the server in the user's own ``ai_summary_server`` preference.
|
||||
|
||||
The administrator's key is never sent to a server a user configured --
|
||||
that would hand every user of the instance a way to capture it."""
|
||||
server_id = _server_id(server)
|
||||
if server_id is None or server_id != _server_id(cfg.base_url):
|
||||
return ""
|
||||
return cfg.api_key
|
||||
if server_id is not None and server_id == _server_id(cfg.base_url):
|
||||
return cfg.api_key
|
||||
return user_api_key
|
||||
|
||||
|
||||
def _user_server(request: "SXNG_Request", cfg: SettingsAISummary) -> str:
|
||||
@@ -308,7 +311,8 @@ def ai_summary_view() -> flask.Response:
|
||||
|
||||
# open the upstream connection before streaming, a connection error is
|
||||
# reported as HTTP 502 instead of a line in an already started stream
|
||||
client = _get_client(server, cfg, _server_api_key(cfg, server))
|
||||
user_api_key = str(sxng_request.preferences.get_value("ai_summary_api_key") or "").strip()
|
||||
client = _get_client(server, cfg, _server_api_key(cfg, server, user_api_key))
|
||||
stream_ctx = client.stream("POST", "/chat/completions", json=chat_payload)
|
||||
upstream = None
|
||||
try:
|
||||
|
||||
+13
-2
@@ -49,10 +49,14 @@ class ValidationException(Exception):
|
||||
class Setting:
|
||||
"""Base class of user settings"""
|
||||
|
||||
def __init__(self, default_value: t.Any, locked: bool = False):
|
||||
def __init__(self, default_value: t.Any, locked: bool = False, secret: bool = False):
|
||||
super().__init__()
|
||||
self.value: t.Any = default_value
|
||||
self.locked: bool = locked
|
||||
self.secret: bool = secret
|
||||
"""The value is a credential: it is not included in the preferences URL
|
||||
(:py:obj:`Preferences.get_as_url_params`), which users copy around to
|
||||
transfer or share their preferences."""
|
||||
|
||||
def parse(self, data: str):
|
||||
"""Parse ``data`` and store the result at ``self.value``
|
||||
@@ -468,6 +472,13 @@ class Preferences:
|
||||
"",
|
||||
locked="ai_summary_server" in self.cfg.lock,
|
||||
),
|
||||
'ai_summary_api_key': StringSetting(
|
||||
"",
|
||||
locked="ai_summary_api_key" in self.cfg.lock,
|
||||
# a user's API key is only sent to a server the user configured
|
||||
# themselves, and it is never part of the preferences URL
|
||||
secret=True,
|
||||
),
|
||||
'ai_summary_model': StringSetting(
|
||||
"",
|
||||
locked="ai_summary_model" in self.cfg.lock,
|
||||
@@ -512,7 +523,7 @@ class Preferences:
|
||||
"""Return preferences as URL parameters"""
|
||||
settings_kv = {}
|
||||
for k, v in self.key_value_settings.items():
|
||||
if v.locked:
|
||||
if v.locked or v.secret:
|
||||
continue
|
||||
if isinstance(v, MultipleChoiceSetting):
|
||||
settings_kv[k] = ','.join(v.get_value())
|
||||
|
||||
+1
-1
@@ -313,7 +313,7 @@ plugins:
|
||||
#
|
||||
# # Ground summaries on the search results by default (users can still opt
|
||||
# # in/out in their preferences).
|
||||
# grounding: false
|
||||
# grounding: true
|
||||
|
||||
|
||||
categories_as_tabs:
|
||||
|
||||
@@ -251,7 +251,10 @@
|
||||
|
||||
{# tab: ai #}
|
||||
|
||||
{%- if plugins_storage | selectattr('preference_section', 'equalto', 'ai') | list -%}
|
||||
{#- the tab is only shown when the administrator activated the plugin in
|
||||
settings.yml, an instance without an AI summary has no AI tab -#}
|
||||
{%- if 'ai_summary' in plugins_active_by_default
|
||||
and plugins_storage | selectattr('preference_section', 'equalto', 'ai') | list -%}
|
||||
{{- tab_header('maintab', 'ai', _('AI Summary')) -}}
|
||||
{{- plugin_preferences('ai') -}}
|
||||
{%- include 'simple/preferences/ai_summary.html' -%}
|
||||
|
||||
@@ -16,6 +16,19 @@
|
||||
</div>{{- '' -}}
|
||||
</fieldset>{{- '' -}}
|
||||
{%- endif -%}
|
||||
{%- if 'ai_summary_api_key' not in locked_preferences -%}
|
||||
<fieldset>{{- '' -}}
|
||||
<legend id="pref_ai_summary_api_key">{{- _('AI server API key') -}}</legend>{{- '' -}}
|
||||
<div class="value">{{- '' -}}
|
||||
<input name="ai_summary_api_key" aria-labelledby="pref_ai_summary_api_key" type="password"
|
||||
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||
value="{{ preferences.get_value('ai_summary_api_key') }}">{{- '' -}}
|
||||
</div>{{- '' -}}
|
||||
<div class="description">
|
||||
{{- _('Only needed if the server URL above is your own server and it requires authentication. Leave empty for a server without authentication and for the default server of this instance (which uses the key of the administrator, never yours). The key is stored in a cookie in your browser and is not part of the preferences URL.') -}}
|
||||
</div>{{- '' -}}
|
||||
</fieldset>{{- '' -}}
|
||||
{%- endif -%}
|
||||
{%- if 'ai_summary_model' not in locked_preferences -%}
|
||||
<fieldset>{{- '' -}}
|
||||
<legend id="pref_ai_summary_model">{{- _('AI summary model') -}}</legend>{{- '' -}}
|
||||
|
||||
@@ -980,6 +980,9 @@ def preferences():
|
||||
shortcuts = {y: x for x, y in engine_shortcuts.items()},
|
||||
themes = themes,
|
||||
plugins_storage = searx.plugins.STORAGE.info,
|
||||
# plugins the administrator activated in settings.yml; a plugin that is
|
||||
# not activated does not get a preferences tab of its own
|
||||
plugins_active_by_default = {plg.id for plg in searx.plugins.STORAGE if plg.active},
|
||||
current_doi_resolver = get_doi_resolver(),
|
||||
allowed_plugins = allowed_plugins,
|
||||
preferences_url_params = sxng_request.preferences.get_as_url_params(),
|
||||
|
||||
Reference in New Issue
Block a user