From 1fa83635c0ad6adfc9ce7f81868245abb657704e Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Fri, 14 Aug 2026 11:00:13 -0700 Subject: [PATCH] [fix] plugin: accept version pinned model names The pattern that validates the model name rejected "@", so names like gemini-1.5-pro@001 were refused with "no valid model configured". Those names are ordinary on the gateways this plugin can be pointed at, which made the api_key support less useful than it looks. Verified against a LiteLLM gateway serving an alias named gemma4-e4b@001: the model list, the request and the streamed answer all carry the pinned name. Co-Authored-By: Claude Opus 5 --- searx/plugins/ai_summary.py | 6 +++++- tests/unit/test_plugin_ai_summary.py | 26 +++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/searx/plugins/ai_summary.py b/searx/plugins/ai_summary.py index d99386629..c919fd604 100644 --- a/searx/plugins/ai_summary.py +++ b/searx/plugins/ai_summary.py @@ -45,7 +45,11 @@ if t.TYPE_CHECKING: from . import PluginCfg VALID_ROLES = ("user", "assistant") -MODEL_NAME_REGEXP = re.compile(r"[A-Za-z0-9._:/-]{1,128}") +# Model names differ per provider: "gemma3:4b" (Ollama), "bedrock/anthropic. +# claude-3-5-sonnet" (a gateway's routing prefix), "gemini-1.5-pro@001" (a +# pinned version). The pattern accepts those and rejects anything that could +# change the meaning of the request body it is placed into. +MODEL_NAME_REGEXP = re.compile(r"[A-Za-z0-9._:/@-]{1,128}") log = logging.getLogger("searx.plugins.ai_summary") diff --git a/tests/unit/test_plugin_ai_summary.py b/tests/unit/test_plugin_ai_summary.py index dec1807c5..9143c95b0 100644 --- a/tests/unit/test_plugin_ai_summary.py +++ b/tests/unit/test_plugin_ai_summary.py @@ -324,9 +324,29 @@ class PluginAISummary(SearxTestCase): self.assertEqual(res.status_code, 400) def test_endpoint_invalid_model_pref(self): - self.client.set_cookie("ai_summary_model", "bad model name!") - res = self.client.post("/ai_summary", json={"messages": [{"role": "user", "content": "hi"}]}) - self.assertEqual(res.status_code, 400) + # an empty preference is not invalid -- it falls back to the + # administrator's default, which is covered by the tests above + for model in ["bad model name!", "model;rm -rf", 'model"quoted', "model\nname", "x" * 129]: + self.client.set_cookie("ai_summary_model", model) + res = self.client.post("/ai_summary", json={"messages": [{"role": "user", "content": "hi"}]}) + self.assertEqual(res.status_code, 400, model) + + def test_endpoint_accepts_provider_model_names(self): + # model names differ per provider: a version pin (@), a gateway's + # routing prefix (/) and an Ollama tag (:) are all valid names + for model in [ + "gemma3:4b", + "llama3.2:3b", + "gemini-1.5-pro@001", + "bedrock/anthropic.claude-3-5-sonnet", + "azure/my-deployment", + ]: + self.mock_upstream(sse_stream_mock([])) + self.client.set_cookie("ai_summary_model", model) + res = self.client.post("/ai_summary", json={"messages": [{"role": "user", "content": "hi"}]}) + self.assertEqual(res.status_code, 200, model) + lines = [json.loads(line) for line in res.data.decode().splitlines() if line] + self.assertEqual(lines[-1], {"done": True, "model": model}) def test_endpoint_no_server_configured(self): self.setattr4test(searx.get_setting("ai_summary"), "base_url", "")