Skip to content

Commit cfa6908

Browse files
committed
refac
1 parent eb3076c commit cfa6908

1 file changed

Lines changed: 59 additions & 1 deletion

File tree

backend/open_webui/utils/misc.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,64 @@ def get_content_from_message(message: dict) -> str | None:
130130
return None
131131

132132

133+
def reconcile_tool_pairs(messages: list[dict]) -> list[dict]:
134+
"""Drop unpaired tool_use / tool_result from a reconstructed conversation.
135+
136+
Stored output can be incomplete — a tool result may be missing (e.g. the
137+
knowledge base was updated mid-chat, or the call was interrupted), or a
138+
tool call may be missing while its result survived. Strict providers
139+
(Anthropic, AWS Bedrock Converse) reject either direction of mismatch.
140+
141+
Well-formed output is unaffected: every id pairs, so nothing is stripped.
142+
"""
143+
completed_tool_call_ids = {
144+
message['tool_call_id']
145+
for message in messages
146+
if message.get('role') == 'tool' and message.get('tool_call_id')
147+
}
148+
requested_tool_call_ids = {
149+
tool_call['id']
150+
for message in messages
151+
for tool_call in message.get('tool_calls') or ()
152+
if message.get('role') == 'assistant' and tool_call.get('id')
153+
}
154+
155+
reconciled_messages = []
156+
for message in messages:
157+
role = message.get('role')
158+
159+
# Orphan tool result — no assistant ever claimed this call_id.
160+
if role == 'tool' and message.get('tool_call_id') not in requested_tool_call_ids:
161+
continue
162+
163+
# Non-assistant or no tool_calls — pass through unchanged.
164+
if role != 'assistant' or not message.get('tool_calls'):
165+
reconciled_messages.append(message)
166+
continue
167+
168+
# Keep only tool_calls whose id received a tool-role response.
169+
valid_tool_calls = [
170+
tool_call
171+
for tool_call in message['tool_calls']
172+
if tool_call.get('id') in completed_tool_call_ids
173+
]
174+
175+
if valid_tool_calls:
176+
reconciled_messages.append({**message, 'tool_calls': valid_tool_calls})
177+
continue
178+
179+
# All tool_calls were orphans — keep the message only if it
180+
# carries meaningful text or reasoning content.
181+
content = message.get('content', '')
182+
has_meaningful_content = content.strip() if isinstance(content, str) else content
183+
if has_meaningful_content or message.get('reasoning_content'):
184+
reconciled_messages.append(
185+
{key: value for key, value in message.items() if key != 'tool_calls'}
186+
)
187+
188+
return reconciled_messages
189+
190+
133191
def convert_output_to_messages(
134192
output: list,
135193
raw: bool = False,
@@ -296,7 +354,7 @@ def flush_pending():
296354
# Flush remaining content/tool_calls
297355
flush_pending()
298356

299-
return messages
357+
return reconcile_tool_pairs(messages)
300358

301359

302360
def get_last_user_message(messages: list[dict]) -> str | None:

0 commit comments

Comments
 (0)