Skip to content

Commit ceec26c

Browse files
committed
refactor: render from output and derive content at save
1 parent b809bd5 commit ceec26c

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
@@ -4097,7 +4097,6 @@ async def flush_pending_delta_data(threshold: int = 0):
40974097

40984098
processed_data = {
40994099
'output': full_output(),
4100-
'content': serialize_output(full_output()),
41014100
}
41024101

41034102
# print(data)
@@ -4263,7 +4262,7 @@ async def flush_pending_delta_data(threshold: int = 0):
42634262
{
42644263
'type': 'chat:completion',
42654264
'data': {
4266-
'content': serialize_output(full_output() + pending_fc_items),
4265+
'output': full_output() + pending_fc_items,
42674266
},
42684267
}
42694268
)
@@ -4341,7 +4340,7 @@ async def flush_pending_delta_data(threshold: int = 0):
43414340
_pending_reasoning_details.extend(items)
43424341

43434342
if reasoning_content or reasoning_details_chunk:
4344-
data = {'content': serialize_output(full_output())}
4343+
data = {'output': full_output()}
43454344

43464345
if value:
43474346
if (
@@ -4500,7 +4499,7 @@ async def flush_pending_delta_data(threshold: int = 0):
45004499
)
45014500
else:
45024501
data = {
4503-
'content': serialize_output(full_output()),
4502+
'output': full_output(),
45044503
}
45054504

45064505
if delta:

src/lib/components/chat/Chat.svelte

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,9 +1121,17 @@
11211121
const messages = history ? createMessagesList(history, history.currentId) : [];
11221122
let contents = [];
11231123
messages.forEach((message) => {
1124-
if (message?.role !== 'user' && message?.content) {
1124+
if (message?.role !== 'user') {
1125+
let text = message?.content || '';
1126+
if (!text && message?.output?.length) {
1127+
text = message.output
1128+
.filter((i) => i.type === 'message')
1129+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1130+
.join('\n');
1131+
}
1132+
if (!text) return;
11251133
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1126-
message.content
1134+
text
11271135
);
11281136
11291137
if (htmlGroups && htmlGroups.length > 0) {
@@ -1778,9 +1786,34 @@
17781786
const chatCompletionEventHandler = async (data, message, chatId) => {
17791787
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
17801788
1789+
const extractTextFromOutput = (o) =>
1790+
o
1791+
.filter((i) => i.type === 'message')
1792+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1793+
.filter(Boolean)
1794+
.join('\n');
1795+
1796+
const dispatchStreamingTTS = (text) => {
1797+
if (!$showCallOverlay) return;
1798+
const parts = getMessageContentParts(
1799+
text,
1800+
$config?.audio?.tts?.split_on ?? 'punctuation'
1801+
);
1802+
parts.pop();
1803+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1804+
message.lastSentence = parts[parts.length - 1];
1805+
eventTarget.dispatchEvent(
1806+
new CustomEvent('chat', {
1807+
detail: { id: message.id, content: parts[parts.length - 1] }
1808+
})
1809+
);
1810+
}
1811+
};
1812+
17811813
// Store raw OR-aligned output items from backend
17821814
if (output) {
17831815
message.output = output;
1816+
dispatchStreamingTTS(extractTextFromOutput(output));
17841817
}
17851818
17861819
if (error) {
@@ -1791,7 +1824,9 @@
17911824
message.sources = sources;
17921825
}
17931826
1794-
if (choices) {
1827+
// Only accumulate content from choices when there's no structured output
1828+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1829+
if (choices && !output) {
17951830
if (choices[0]?.message?.content) {
17961831
// Non-stream response
17971832
message.content += choices[0]?.message?.content;
@@ -1807,30 +1842,7 @@
18071842
navigator.vibrate(5);
18081843
}
18091844
1810-
// Emit chat event for TTS (only when call overlay is active)
1811-
if ($showCallOverlay) {
1812-
const messageContentParts = getMessageContentParts(
1813-
removeAllDetails(message.content),
1814-
$config?.audio?.tts?.split_on ?? 'punctuation'
1815-
);
1816-
messageContentParts.pop();
1817-
1818-
// dispatch only last sentence and make sure it hasn't been dispatched before
1819-
if (
1820-
messageContentParts.length > 0 &&
1821-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1822-
) {
1823-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1824-
eventTarget.dispatchEvent(
1825-
new CustomEvent('chat', {
1826-
detail: {
1827-
id: message.id,
1828-
content: messageContentParts[messageContentParts.length - 1]
1829-
}
1830-
})
1831-
);
1832-
}
1833-
}
1845+
dispatchStreamingTTS(removeAllDetails(message.content));
18341846
}
18351847
}
18361848
}
@@ -1843,30 +1855,7 @@
18431855
navigator.vibrate(5);
18441856
}
18451857
1846-
// Emit chat event for TTS (only when call overlay is active)
1847-
if ($showCallOverlay) {
1848-
const messageContentParts = getMessageContentParts(
1849-
removeAllDetails(message.content),
1850-
$config?.audio?.tts?.split_on ?? 'punctuation'
1851-
);
1852-
messageContentParts.pop();
1853-
1854-
// dispatch only last sentence and make sure it hasn't been dispatched before
1855-
if (
1856-
messageContentParts.length > 0 &&
1857-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1858-
) {
1859-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1860-
eventTarget.dispatchEvent(
1861-
new CustomEvent('chat', {
1862-
detail: {
1863-
id: message.id,
1864-
content: messageContentParts[messageContentParts.length - 1]
1865-
}
1866-
})
1867-
);
1868-
}
1869-
}
1858+
dispatchStreamingTTS(removeAllDetails(message.content));
18701859
}
18711860
18721861
if (selected_model_id) {

0 commit comments

Comments
 (0)