Skip to content

Commit 8abc5cd

Browse files
committed
refactor: render from output and derive content at save
1 parent a72b654 commit 8abc5cd

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

40704070
processed_data = {
40714071
'output': full_output(),
4072-
'content': serialize_output(full_output()),
40734072
}
40744073

40754074
# print(data)
@@ -4235,7 +4234,7 @@ async def flush_pending_delta_data(threshold: int = 0):
42354234
{
42364235
'type': 'chat:completion',
42374236
'data': {
4238-
'content': serialize_output(full_output() + pending_fc_items),
4237+
'output': full_output() + pending_fc_items,
42394238
},
42404239
}
42414240
)
@@ -4313,7 +4312,7 @@ async def flush_pending_delta_data(threshold: int = 0):
43134312
_pending_reasoning_details.extend(items)
43144313

43154314
if reasoning_content or reasoning_details_chunk:
4316-
data = {'content': serialize_output(full_output())}
4315+
data = {'output': full_output()}
43174316

43184317
if value:
43194318
if (
@@ -4470,7 +4469,7 @@ async def flush_pending_delta_data(threshold: int = 0):
44704469
)
44714470
else:
44724471
data = {
4473-
'content': serialize_output(full_output()),
4472+
'output': full_output(),
44744473
}
44754474

44764475
if delta:

src/lib/components/chat/Chat.svelte

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,9 +1074,17 @@
10741074
const messages = history ? createMessagesList(history, history.currentId) : [];
10751075
let contents = [];
10761076
messages.forEach((message) => {
1077-
if (message?.role !== 'user' && message?.content) {
1077+
if (message?.role !== 'user') {
1078+
let text = message?.content || '';
1079+
if (!text && message?.output?.length) {
1080+
text = message.output
1081+
.filter((i) => i.type === 'message')
1082+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1083+
.join('\n');
1084+
}
1085+
if (!text) return;
10781086
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1079-
message.content
1087+
text
10801088
);
10811089
10821090
if (htmlGroups && htmlGroups.length > 0) {
@@ -1751,9 +1759,34 @@
17511759
const chatCompletionEventHandler = async (data, message, chatId) => {
17521760
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
17531761
1762+
const extractTextFromOutput = (o) =>
1763+
o
1764+
.filter((i) => i.type === 'message')
1765+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1766+
.filter(Boolean)
1767+
.join('\n');
1768+
1769+
const dispatchStreamingTTS = (text) => {
1770+
if (!$showCallOverlay) return;
1771+
const parts = getMessageContentParts(
1772+
text,
1773+
$config?.audio?.tts?.split_on ?? 'punctuation'
1774+
);
1775+
parts.pop();
1776+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1777+
message.lastSentence = parts[parts.length - 1];
1778+
eventTarget.dispatchEvent(
1779+
new CustomEvent('chat', {
1780+
detail: { id: message.id, content: parts[parts.length - 1] }
1781+
})
1782+
);
1783+
}
1784+
};
1785+
17541786
// Store raw OR-aligned output items from backend
17551787
if (output) {
17561788
message.output = output;
1789+
dispatchStreamingTTS(extractTextFromOutput(output));
17571790
}
17581791
17591792
if (error) {
@@ -1764,7 +1797,9 @@
17641797
message.sources = sources;
17651798
}
17661799
1767-
if (choices) {
1800+
// Only accumulate content from choices when there's no structured output
1801+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1802+
if (choices && !output) {
17681803
if (choices[0]?.message?.content) {
17691804
// Non-stream response
17701805
message.content += choices[0]?.message?.content;
@@ -1780,30 +1815,7 @@
17801815
navigator.vibrate(5);
17811816
}
17821817
1783-
// Emit chat event for TTS (only when call overlay is active)
1784-
if ($showCallOverlay) {
1785-
const messageContentParts = getMessageContentParts(
1786-
removeAllDetails(message.content),
1787-
$config?.audio?.tts?.split_on ?? 'punctuation'
1788-
);
1789-
messageContentParts.pop();
1790-
1791-
// dispatch only last sentence and make sure it hasn't been dispatched before
1792-
if (
1793-
messageContentParts.length > 0 &&
1794-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1795-
) {
1796-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1797-
eventTarget.dispatchEvent(
1798-
new CustomEvent('chat', {
1799-
detail: {
1800-
id: message.id,
1801-
content: messageContentParts[messageContentParts.length - 1]
1802-
}
1803-
})
1804-
);
1805-
}
1806-
}
1818+
dispatchStreamingTTS(removeAllDetails(message.content));
18071819
}
18081820
}
18091821
}
@@ -1816,30 +1828,7 @@
18161828
navigator.vibrate(5);
18171829
}
18181830
1819-
// Emit chat event for TTS (only when call overlay is active)
1820-
if ($showCallOverlay) {
1821-
const messageContentParts = getMessageContentParts(
1822-
removeAllDetails(message.content),
1823-
$config?.audio?.tts?.split_on ?? 'punctuation'
1824-
);
1825-
messageContentParts.pop();
1826-
1827-
// dispatch only last sentence and make sure it hasn't been dispatched before
1828-
if (
1829-
messageContentParts.length > 0 &&
1830-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1831-
) {
1832-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1833-
eventTarget.dispatchEvent(
1834-
new CustomEvent('chat', {
1835-
detail: {
1836-
id: message.id,
1837-
content: messageContentParts[messageContentParts.length - 1]
1838-
}
1839-
})
1840-
);
1841-
}
1842-
}
1831+
dispatchStreamingTTS(removeAllDetails(message.content));
18431832
}
18441833
18451834
if (selected_model_id) {

0 commit comments

Comments
 (0)