Skip to content

Commit b01d2d3

Browse files
committed
fix: respect content edits for assistant messages with output
1 parent a37f609 commit b01d2d3

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

backend/open_webui/utils/middleware.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
convert_logit_bias_input_to_json,
9797
get_content_from_message,
9898
convert_output_to_messages,
99+
filter_output_by_content,
99100
)
100101
from open_webui.utils.tools import (
101102
get_tools,
@@ -434,9 +435,9 @@ def serialize_output(output: list) -> str:
434435
)
435436

436437
if status == "completed" or duration is not None or not is_last_item:
437-
content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n'
438+
content = f'{content}<details type="reasoning" done="true" id="{item.get("id", "")}" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n'
438439
else:
439-
content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n'
440+
content = f'{content}<details type="reasoning" done="false" id="{item.get("id", "")}">\n<summary>Thinking…</summary>\n{display}\n</details>\n'
440441

441442
elif item_type == "open_webui:code_interpreter":
442443
content_stripped, original_whitespace = split_content_and_whitespace(
@@ -476,9 +477,9 @@ def serialize_output(output: list) -> str:
476477
output_attr = f' output="{html.escape(output_json)}"'
477478

478479
if status == "completed" or duration is not None or not is_last_item:
479-
content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n'
480+
content += f'<details type="code_interpreter" done="true" id="{item.get("id", "")}" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n'
480481
else:
481-
content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n'
482+
content += f'<details type="code_interpreter" done="false" id="{item.get("id", "")}"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n'
482483

483484
return content.strip()
484485

@@ -2069,11 +2070,40 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]:
20692070

20702071
for message in messages:
20712072
if message.get("role") == "assistant" and message.get("output"):
2072-
# Use output items for clean OpenAI-format messages
2073-
output_messages = convert_output_to_messages(message["output"], raw=True)
2073+
# Drop output items for <details> blocks removed from content
2074+
output = filter_output_by_content(
2075+
message["output"], message.get("content", "")
2076+
)
2077+
2078+
# Use content for text (respects edits), output for structured items
2079+
content = re.sub(
2080+
r"<details\b[^>]*>.*?</details>", "",
2081+
message.get("content", ""), flags=re.S,
2082+
).strip()
2083+
non_message_items = [
2084+
i for i in output if i.get("type") != "message"
2085+
]
2086+
output_messages = convert_output_to_messages(non_message_items, raw=True)
2087+
20742088
if output_messages:
2089+
# Prepend edited text to first assistant message
2090+
for om in output_messages:
2091+
if om.get("role") == "assistant":
2092+
om["content"] = (
2093+
(content + "\n" + om["content"]).strip()
2094+
if om.get("content")
2095+
else content
2096+
)
2097+
content = ""
2098+
break
2099+
if content:
2100+
output_messages.insert(
2101+
0, {"role": "assistant", "content": content}
2102+
)
20752103
processed.extend(output_messages)
2076-
continue
2104+
elif content:
2105+
processed.append({"role": "assistant", "content": content})
2106+
continue
20772107

20782108
# Strip 'output' field before adding (LLM shouldn't see it)
20792109
clean_message = {k: v for k, v in message.items() if k != "output"}

backend/open_webui/utils/misc.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,59 @@ def flush_pending():
275275
return messages
276276

277277

278+
def filter_output_by_content(output: list, content: str) -> list:
279+
"""
280+
Drop output items whose <details> block was removed from content.
281+
Matches by id attribute for all details-backed types.
282+
For legacy content that lacks id= on a given type, all items of
283+
that type are kept (safe default).
284+
"""
285+
# All IDs present in remaining <details> blocks
286+
present_ids = set(re.findall(r'<details\s[^>]*\bid="([^"]+)"', content))
287+
288+
# Which <details> types exist in content at all
289+
types_present = set(
290+
m.group(1) for m in re.finditer(r'<details\s[^>]*\btype="(\w+)"', content)
291+
)
292+
293+
# Of those, which carry id= attributes (post-fix content).
294+
# Legacy content without id= on a given type → keep all items of that type.
295+
types_with_ids = set()
296+
for m in re.finditer(r'<details\s[^>]*\btype="(\w+)"[^>]*\bid=', content):
297+
types_with_ids.add(m.group(1))
298+
for m in re.finditer(r'<details\s[^>]*\bid=[^>]*\btype="(\w+)"', content):
299+
types_with_ids.add(m.group(1))
300+
301+
# Map output item type → <details> type attribute value
302+
DETAILS_TYPE = {
303+
"function_call": "tool_calls",
304+
"function_call_output": "tool_calls",
305+
"reasoning": "reasoning",
306+
"open_webui:code_interpreter": "code_interpreter",
307+
}
308+
309+
filtered = []
310+
for item in output:
311+
item_type = item.get("type", "")
312+
details_type = DETAILS_TYPE.get(item_type)
313+
314+
if details_type is None:
315+
# Not a details-backed type (e.g. message) — pass through
316+
filtered.append(item)
317+
elif details_type not in types_present:
318+
pass # type completely gone from content — drop
319+
elif details_type not in types_with_ids:
320+
# Legacy content — has <details> but no id= — keep all
321+
filtered.append(item)
322+
else:
323+
item_id = item.get("call_id") or item.get("id", "")
324+
if item_id in present_ids:
325+
filtered.append(item)
326+
# else: user deleted the <details> block — drop
327+
328+
return filtered
329+
330+
278331
def get_last_user_message(messages: list[dict]) -> Optional[str]:
279332
message = get_last_user_message_item(messages)
280333
if message is None:

0 commit comments

Comments
 (0)