Skip to content

Commit 11f0853

Browse files
committed
refactor: render from output and derive content at save
1 parent dba7468 commit 11f0853

4 files changed

Lines changed: 543 additions & 82 deletions

File tree

backend/open_webui/utils/middleware.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4051,7 +4051,6 @@ async def flush_pending_delta_data(threshold: int = 0):
40514051

40524052
processed_data = {
40534053
'output': full_output(),
4054-
'content': serialize_output(full_output()),
40554054
}
40564055

40574056
# print(data)
@@ -4217,7 +4216,7 @@ async def flush_pending_delta_data(threshold: int = 0):
42174216
{
42184217
'type': 'chat:completion',
42194218
'data': {
4220-
'content': serialize_output(full_output() + pending_fc_items),
4219+
'output': full_output() + pending_fc_items,
42214220
},
42224221
}
42234222
)
@@ -4295,7 +4294,7 @@ async def flush_pending_delta_data(threshold: int = 0):
42954294
_pending_reasoning_details.extend(items)
42964295

42974296
if reasoning_content or reasoning_details_chunk:
4298-
data = {'content': serialize_output(full_output())}
4297+
data = {'output': full_output()}
42994298

43004299
if value:
43014300
if (
@@ -4452,7 +4451,7 @@ async def flush_pending_delta_data(threshold: int = 0):
44524451
)
44534452
else:
44544453
data = {
4455-
'content': serialize_output(full_output()),
4454+
'output': full_output(),
44564455
}
44574456

44584457
if delta:

src/lib/components/chat/Chat.svelte

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,9 +1095,17 @@
10951095
const messages = history ? createMessagesList(history, history.currentId) : [];
10961096
let contents = [];
10971097
messages.forEach((message) => {
1098-
if (message?.role !== 'user' && message?.content) {
1098+
if (message?.role !== 'user') {
1099+
let text = message?.content || '';
1100+
if (!text && message?.output?.length) {
1101+
text = message.output
1102+
.filter((i) => i.type === 'message')
1103+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1104+
.join('\n');
1105+
}
1106+
if (!text) return;
10991107
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1100-
message.content
1108+
text
11011109
);
11021110
11031111
if (htmlGroups && htmlGroups.length > 0) {
@@ -1752,9 +1760,34 @@
17521760
const chatCompletionEventHandler = async (data, message, chatId) => {
17531761
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
17541762
1763+
const extractTextFromOutput = (o) =>
1764+
o
1765+
.filter((i) => i.type === 'message')
1766+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1767+
.filter(Boolean)
1768+
.join('\n');
1769+
1770+
const dispatchStreamingTTS = (text) => {
1771+
if (!$showCallOverlay) return;
1772+
const parts = getMessageContentParts(
1773+
text,
1774+
$config?.audio?.tts?.split_on ?? 'punctuation'
1775+
);
1776+
parts.pop();
1777+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1778+
message.lastSentence = parts[parts.length - 1];
1779+
eventTarget.dispatchEvent(
1780+
new CustomEvent('chat', {
1781+
detail: { id: message.id, content: parts[parts.length - 1] }
1782+
})
1783+
);
1784+
}
1785+
};
1786+
17551787
// Store raw OR-aligned output items from backend
17561788
if (output) {
17571789
message.output = output;
1790+
dispatchStreamingTTS(extractTextFromOutput(output));
17581791
}
17591792
17601793
if (error) {
@@ -1765,7 +1798,9 @@
17651798
message.sources = sources;
17661799
}
17671800
1768-
if (choices) {
1801+
// Only accumulate content from choices when there's no structured output
1802+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1803+
if (choices && !output) {
17691804
if (choices[0]?.message?.content) {
17701805
// Non-stream response
17711806
message.content += choices[0]?.message?.content;
@@ -1781,30 +1816,7 @@
17811816
navigator.vibrate(5);
17821817
}
17831818
1784-
// Emit chat event for TTS (only when call overlay is active)
1785-
if ($showCallOverlay) {
1786-
const messageContentParts = getMessageContentParts(
1787-
removeAllDetails(message.content),
1788-
$config?.audio?.tts?.split_on ?? 'punctuation'
1789-
);
1790-
messageContentParts.pop();
1791-
1792-
// dispatch only last sentence and make sure it hasn't been dispatched before
1793-
if (
1794-
messageContentParts.length > 0 &&
1795-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1796-
) {
1797-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1798-
eventTarget.dispatchEvent(
1799-
new CustomEvent('chat', {
1800-
detail: {
1801-
id: message.id,
1802-
content: messageContentParts[messageContentParts.length - 1]
1803-
}
1804-
})
1805-
);
1806-
}
1807-
}
1819+
dispatchStreamingTTS(removeAllDetails(message.content));
18081820
}
18091821
}
18101822
}
@@ -1817,30 +1829,7 @@
18171829
navigator.vibrate(5);
18181830
}
18191831
1820-
// Emit chat event for TTS (only when call overlay is active)
1821-
if ($showCallOverlay) {
1822-
const messageContentParts = getMessageContentParts(
1823-
removeAllDetails(message.content),
1824-
$config?.audio?.tts?.split_on ?? 'punctuation'
1825-
);
1826-
messageContentParts.pop();
1827-
1828-
// dispatch only last sentence and make sure it hasn't been dispatched before
1829-
if (
1830-
messageContentParts.length > 0 &&
1831-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1832-
) {
1833-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1834-
eventTarget.dispatchEvent(
1835-
new CustomEvent('chat', {
1836-
detail: {
1837-
id: message.id,
1838-
content: messageContentParts[messageContentParts.length - 1]
1839-
}
1840-
})
1841-
);
1842-
}
1843-
}
1832+
dispatchStreamingTTS(removeAllDetails(message.content));
18441833
}
18451834
18461835
if (selected_model_id) {

0 commit comments

Comments
 (0)