From c7cce962d292bcef9296ad111ee07da0e2a3c473 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:28:18 +0200 Subject: [PATCH] fix: stop citing web search results as sources (#29631) A web search is not something that can be cited. The search engine returns a title, a link and a one line snippet for each hit, and the model never opens any of those pages. Emitting them as citation sources produced one tag per result, all named search_web with empty bodies, and the citation template then instructed the model to cite them by id. Models either hesitated visibly or attached an id to content from a different result, which the citations panel then resolved to a title that looked plausible, so the misattribution read as correct. Web search results now stay in the tool output the model reads, and stop being offered as things to cite. Where the model needs to cite a page it calls fetch_url, whose citation names the URL and already works. Web search results no longer appear in the citations panel. That is the point of the change: the panel was offering pages that nothing had read. Scoped to the native tool-calling path. The legacy handler cites every tool result as one opaque source and does not single out web search, so it is left alone rather than special-cased. --- backend/open_webui/utils/middleware.py | 44 ++++---------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index e6c561a8ca..c03e689ff2 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -400,9 +400,6 @@ def get_citation_source_from_tool_result( Returns a list of sources (usually one, but query_knowledge_files/query_chat_files may return multiple). """ - _EXPECTS_LIST = {'search_web', 'query_knowledge_files', 'query_chat_files'} - _EXPECTS_DICT = {'view_knowledge_file', 'view_file'} - try: try: tool_result = JSONCodec.loads(tool_result) @@ -411,41 +408,10 @@ def get_citation_source_from_tool_result( if isinstance(tool_result, dict) and 'error' in tool_result: return [] - # Validate tool_result type based on what the branch expects - if tool_name in _EXPECTS_LIST and not isinstance(tool_result, list): - return [] - elif tool_name in _EXPECTS_DICT and not isinstance(tool_result, dict): - return [] + if tool_name in ('view_knowledge_file', 'view_file'): + if not isinstance(tool_result, dict): + return [] - if tool_name == 'search_web': - # Parse JSON array: [{"title": "...", "link": "...", "snippet": "..."}] - results = tool_result - documents = [] - metadata = [] - - for result in results: - title = result.get('title', '') - link = result.get('link', '') - snippet = result.get('snippet', '') - - documents.append(f'{title}\n{snippet}') - metadata.append( - { - 'source': link, - 'name': title, - 'url': link, - } - ) - - return [ - { - 'source': {'name': 'search_web', 'id': 'search_web'}, - 'document': documents, - 'metadata': metadata, - } - ] - - elif tool_name in ('view_knowledge_file', 'view_file'): file_data = tool_result filename = file_data.get('filename', 'Unknown File') file_id = file_data.get('id', '') @@ -490,6 +456,9 @@ def get_citation_source_from_tool_result( ] elif tool_name in ('query_knowledge_files', 'query_chat_files'): + if not isinstance(tool_result, list): + return [] + chunks = tool_result # Group chunks by source for better citation display @@ -5772,7 +5741,6 @@ async def streaming_chat_response_handler(response, ctx): citations_enabled and tool_function_name in [ - 'search_web', 'fetch_url', 'view_file', 'view_knowledge_file',