Skip to content

Commit 5d235c0

Browse files
Algorithm5838github-actions[bot]
authored andcommitted
refactor: render from output and derive content at save
1 parent f0554a1 commit 5d235c0

4 files changed

Lines changed: 542 additions & 75 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
@@ -1097,9 +1097,17 @@
10971097
const messages = history ? createMessagesList(history, history.currentId) : [];
10981098
let contents = [];
10991099
messages.forEach((message) => {
1100-
if (message?.role !== 'user' && message?.content) {
1100+
if (message?.role !== 'user') {
1101+
let text = message?.content || '';
1102+
if (!text && message?.output?.length) {
1103+
text = message.output
1104+
.filter((i) => i.type === 'message')
1105+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1106+
.join('\n');
1107+
}
1108+
if (!text) return;
11011109
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1102-
message.content
1110+
text
11031111
);
11041112
11051113
if (htmlGroups && htmlGroups.length > 0) {
@@ -1754,9 +1762,34 @@
17541762
const chatCompletionEventHandler = async (data, message, chatId) => {
17551763
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
17561764
1765+
const extractTextFromOutput = (o) =>
1766+
o
1767+
.filter((i) => i.type === 'message')
1768+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1769+
.filter(Boolean)
1770+
.join('\n');
1771+
1772+
const dispatchStreamingTTS = (text) => {
1773+
if (!$showCallOverlay) return;
1774+
const parts = getMessageContentParts(
1775+
text,
1776+
$config?.audio?.tts?.split_on ?? 'punctuation'
1777+
);
1778+
parts.pop();
1779+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1780+
message.lastSentence = parts[parts.length - 1];
1781+
eventTarget.dispatchEvent(
1782+
new CustomEvent('chat', {
1783+
detail: { id: message.id, content: parts[parts.length - 1] }
1784+
})
1785+
);
1786+
}
1787+
};
1788+
17571789
// Store raw OR-aligned output items from backend
17581790
if (output) {
17591791
message.output = output;
1792+
dispatchStreamingTTS(extractTextFromOutput(output));
17601793
}
17611794
17621795
if (error) {
@@ -1767,7 +1800,9 @@
17671800
message.sources = sources;
17681801
}
17691802
1770-
if (choices) {
1803+
// Only accumulate content from choices when there's no structured output
1804+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1805+
if (choices && !output) {
17711806
if (choices[0]?.message?.content) {
17721807
// Non-stream response
17731808
message.content += choices[0]?.message?.content;
@@ -1783,30 +1818,7 @@
17831818
navigator.vibrate(5);
17841819
}
17851820
1786-
// Emit chat event for TTS (only when call overlay is active)
1787-
if ($showCallOverlay) {
1788-
const messageContentParts = getMessageContentParts(
1789-
removeAllDetails(message.content),
1790-
$config?.audio?.tts?.split_on ?? 'punctuation'
1791-
);
1792-
messageContentParts.pop();
1793-
1794-
// dispatch only last sentence and make sure it hasn't been dispatched before
1795-
if (
1796-
messageContentParts.length > 0 &&
1797-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1798-
) {
1799-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1800-
eventTarget.dispatchEvent(
1801-
new CustomEvent('chat', {
1802-
detail: {
1803-
id: message.id,
1804-
content: messageContentParts[messageContentParts.length - 1]
1805-
}
1806-
})
1807-
);
1808-
}
1809-
}
1821+
dispatchStreamingTTS(removeAllDetails(message.content));
18101822
}
18111823
}
18121824
}
@@ -1819,30 +1831,7 @@
18191831
navigator.vibrate(5);
18201832
}
18211833
1822-
// Emit chat event for TTS (only when call overlay is active)
1823-
if ($showCallOverlay) {
1824-
const messageContentParts = getMessageContentParts(
1825-
removeAllDetails(message.content),
1826-
$config?.audio?.tts?.split_on ?? 'punctuation'
1827-
);
1828-
messageContentParts.pop();
1829-
1830-
// dispatch only last sentence and make sure it hasn't been dispatched before
1831-
if (
1832-
messageContentParts.length > 0 &&
1833-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1834-
) {
1835-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1836-
eventTarget.dispatchEvent(
1837-
new CustomEvent('chat', {
1838-
detail: {
1839-
id: message.id,
1840-
content: messageContentParts[messageContentParts.length - 1]
1841-
}
1842-
})
1843-
);
1844-
}
1845-
}
1834+
dispatchStreamingTTS(removeAllDetails(message.content));
18461835
}
18471836
18481837
if (selected_model_id) {

0 commit comments

Comments
 (0)