From cfa6908d579e1f7f202a321289029996130b8411 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 19 May 2026 22:25:39 +0400 Subject: [PATCH] refac --- backend/open_webui/utils/misc.py | 60 +++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 58a038791d..b8336e8fe7 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -130,6 +130,64 @@ def get_content_from_message(message: dict) -> str | None: return None +def reconcile_tool_pairs(messages: list[dict]) -> list[dict]: + """Drop unpaired tool_use / tool_result from a reconstructed conversation. + + Stored output can be incomplete — a tool result may be missing (e.g. the + knowledge base was updated mid-chat, or the call was interrupted), or a + tool call may be missing while its result survived. Strict providers + (Anthropic, AWS Bedrock Converse) reject either direction of mismatch. + + Well-formed output is unaffected: every id pairs, so nothing is stripped. + """ + completed_tool_call_ids = { + message['tool_call_id'] + for message in messages + if message.get('role') == 'tool' and message.get('tool_call_id') + } + requested_tool_call_ids = { + tool_call['id'] + for message in messages + for tool_call in message.get('tool_calls') or () + if message.get('role') == 'assistant' and tool_call.get('id') + } + + reconciled_messages = [] + for message in messages: + role = message.get('role') + + # Orphan tool result — no assistant ever claimed this call_id. + if role == 'tool' and message.get('tool_call_id') not in requested_tool_call_ids: + continue + + # Non-assistant or no tool_calls — pass through unchanged. + if role != 'assistant' or not message.get('tool_calls'): + reconciled_messages.append(message) + continue + + # Keep only tool_calls whose id received a tool-role response. + valid_tool_calls = [ + tool_call + for tool_call in message['tool_calls'] + if tool_call.get('id') in completed_tool_call_ids + ] + + if valid_tool_calls: + reconciled_messages.append({**message, 'tool_calls': valid_tool_calls}) + continue + + # All tool_calls were orphans — keep the message only if it + # carries meaningful text or reasoning content. + content = message.get('content', '') + has_meaningful_content = content.strip() if isinstance(content, str) else content + if has_meaningful_content or message.get('reasoning_content'): + reconciled_messages.append( + {key: value for key, value in message.items() if key != 'tool_calls'} + ) + + return reconciled_messages + + def convert_output_to_messages( output: list, raw: bool = False, @@ -296,7 +354,7 @@ def convert_output_to_messages( # Flush remaining content/tool_calls flush_pending() - return messages + return reconcile_tool_pairs(messages) def get_last_user_message(messages: list[dict]) -> str | None: