Skip to content

Commit ac67292

Browse files
committed
fix: reconcile output on save and fix continuation
1 parent effa8b8 commit ac67292

4 files changed

Lines changed: 36 additions & 37 deletions

File tree

backend/open_webui/routers/chats.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -889,9 +889,7 @@ async def update_chat_by_id(
889889
if chat:
890890
updated_chat = {**chat.chat, **form_data.chat}
891891

892-
# Reconcile output with content for any edited assistant messages so
893-
# the DB is always consistent and process_messages_with_output can use
894-
# output as the source of truth without re-parsing content at read time.
892+
# Reconcile output with any content edits so the DB stays consistent.
895893
messages = updated_chat.get('history', {}).get('messages', {})
896894
for mid, msg in messages.items():
897895
if msg.get('role') == 'assistant' and msg.get('output'):

backend/open_webui/utils/middleware.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,7 +2076,7 @@ def load_messages_from_db(
20762076
keeping only LLM-relevant fields (role, content, output).
20772077
20782078
If require_done is True, returns None when the anchor message is not
2079-
marked done prevents loading a partial mid-stream output as context.
2079+
marked done; prevents loading a partial mid-stream output as context.
20802080
"""
20812081
messages_map = Chats.get_messages_map_by_chat_id(chat_id)
20822082
if not messages_map:
@@ -2098,8 +2098,7 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]:
20982098
20992099
For assistant messages with 'output' field, produces properly formatted
21002100
OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM.
2101-
The DB is kept consistent at write-time (update_chat_by_id), so output can
2102-
be used directly as the source of truth here.
2101+
output is reconciled at write-time; use it as-is.
21032102
"""
21042103
processed = []
21052104

@@ -2163,10 +2162,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
21632162
db_messages = None
21642163

21652164
if chat_id and not chat_id.startswith('local:'):
2166-
# For "Continue", message_id is the existing assistant message already in the
2167-
# DB — load from it so the reconciled output is included in context.
2168-
# For a new generation, message_id is a fresh UUID not yet saved, so
2169-
# load_messages_from_db returns None and we fall through to parent_message_id.
2165+
# For "Continue", message_id already exists in the DB; load from it directly.
2166+
# For a new turn, message_id is a fresh UUID; load_messages_from_db returns None
2167+
# and falls back to parent_message_id.
21702168
if message_id:
21712169
db_messages = load_messages_from_db(chat_id, message_id, require_done=True)
21722170
if db_messages is None and parent_message_id:

backend/open_webui/utils/misc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def reconcile_output_with_content(message: dict) -> dict:
325325
content = message.get('content', '')
326326
output = message.get('output', [])
327327

328-
# Extract plain text by stripping <details> blocks that serialize_output wrote
328+
# Strip rendered <details> blocks to get the edited plain text
329329
edited_text = re.sub(r'<details\b[^>]*>.*?</details>', '', content, flags=re.S).strip()
330330

331331
# Drop output items for <details> blocks the user removed from content
@@ -344,7 +344,7 @@ def reconcile_output_with_content(message: dict) -> dict:
344344
else:
345345
reconciled_output.append(item)
346346

347-
# If there was no 'message' item, append one so the text is not lost
347+
# No message item yet; preserve the edited text
348348
if not text_applied and edited_text:
349349
reconciled_output.append({
350350
'type': 'message',

src/lib/components/chat/Messages/Markdown.svelte

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
1-
<script>
2-
import { onDestroy } from 'svelte';
1+
<script context="module">
32
import { marked } from 'marked';
4-
import { replaceTokens, processResponseContent } from '$lib/utils';
5-
import { user } from '$lib/stores';
63
74
import markedExtension from '$lib/utils/marked/extension';
85
import markedKatexExtension from '$lib/utils/marked/katex-extension';
96
import { disableSingleTilde } from '$lib/utils/marked/strikethrough-extension';
107
import { mentionExtension } from '$lib/utils/marked/mention-extension';
118
import colonFenceExtension from '$lib/utils/marked/colon-fence-extension';
12-
13-
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
149
import footnoteExtension from '$lib/utils/marked/footnote-extension';
1510
import citationExtension from '$lib/utils/marked/citation-extension';
1611
12+
const options = {
13+
throwOnError: false,
14+
breaks: true
15+
};
16+
17+
marked.use(markedKatexExtension(options));
18+
marked.use(markedExtension(options));
19+
marked.use(citationExtension(options));
20+
marked.use(footnoteExtension(options));
21+
marked.use(colonFenceExtension(options));
22+
marked.use(disableSingleTilde);
23+
marked.use({
24+
extensions: [
25+
mentionExtension({ triggerChar: '@' }),
26+
mentionExtension({ triggerChar: '#' }),
27+
mentionExtension({ triggerChar: '$' })
28+
]
29+
});
30+
</script>
31+
32+
<script>
33+
import { onDestroy } from 'svelte';
34+
import { replaceTokens, processResponseContent } from '$lib/utils';
35+
import { user } from '$lib/stores';
36+
37+
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
38+
1739
export let id = '';
1840
export let content;
1941
export let done = true;
@@ -40,25 +62,6 @@
4062
let lastContent = '';
4163
let lastParsedContent = '';
4264
43-
const options = {
44-
throwOnError: false,
45-
breaks: true
46-
};
47-
48-
marked.use(markedKatexExtension(options));
49-
marked.use(markedExtension(options));
50-
marked.use(citationExtension(options));
51-
marked.use(footnoteExtension(options));
52-
marked.use(colonFenceExtension(options));
53-
marked.use(disableSingleTilde);
54-
marked.use({
55-
extensions: [
56-
mentionExtension({ triggerChar: '@' }),
57-
mentionExtension({ triggerChar: '#' }),
58-
mentionExtension({ triggerChar: '$' })
59-
]
60-
});
61-
6265
const parseTokens = () => {
6366
if (content === lastContent) return;
6467
lastContent = content;

0 commit comments

Comments
 (0)