Skip to content

Commit 5a8aa81

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

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

41034103
processed_data = {
41044104
'output': full_output(),
4105-
'content': serialize_output(full_output()),
41064105
}
41074106

41084107
# print(data)
@@ -4268,7 +4267,7 @@ async def flush_pending_delta_data(threshold: int = 0):
42684267
{
42694268
'type': 'chat:completion',
42704269
'data': {
4271-
'content': serialize_output(full_output() + pending_fc_items),
4270+
'output': full_output() + pending_fc_items,
42724271
},
42734272
}
42744273
)
@@ -4346,7 +4345,7 @@ async def flush_pending_delta_data(threshold: int = 0):
43464345
_pending_reasoning_details.extend(items)
43474346

43484347
if reasoning_content or reasoning_details_chunk:
4349-
data = {'content': serialize_output(full_output())}
4348+
data = {'output': full_output()}
43504349

43514350
if value:
43524351
if (
@@ -4505,7 +4504,7 @@ async def flush_pending_delta_data(threshold: int = 0):
45054504
)
45064505
else:
45074506
data = {
4508-
'content': serialize_output(full_output()),
4507+
'output': full_output(),
45094508
}
45104509

45114510
if delta:

src/lib/components/chat/Chat.svelte

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,9 +1126,17 @@
11261126
const messages = history ? createMessagesList(history, history.currentId) : [];
11271127
let contents = [];
11281128
messages.forEach((message) => {
1129-
if (message?.role !== 'user' && message?.content) {
1129+
if (message?.role !== 'user') {
1130+
let text = message?.content || '';
1131+
if (!text && message?.output?.length) {
1132+
text = message.output
1133+
.filter((i) => i.type === 'message')
1134+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1135+
.join('\n');
1136+
}
1137+
if (!text) return;
11301138
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1131-
message.content
1139+
text
11321140
);
11331141
11341142
if (htmlGroups && htmlGroups.length > 0) {
@@ -1783,9 +1791,34 @@
17831791
const chatCompletionEventHandler = async (data, message, chatId) => {
17841792
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
17851793
1794+
const extractTextFromOutput = (o) =>
1795+
o
1796+
.filter((i) => i.type === 'message')
1797+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1798+
.filter(Boolean)
1799+
.join('\n');
1800+
1801+
const dispatchStreamingTTS = (text) => {
1802+
if (!$showCallOverlay) return;
1803+
const parts = getMessageContentParts(
1804+
text,
1805+
$config?.audio?.tts?.split_on ?? 'punctuation'
1806+
);
1807+
parts.pop();
1808+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1809+
message.lastSentence = parts[parts.length - 1];
1810+
eventTarget.dispatchEvent(
1811+
new CustomEvent('chat', {
1812+
detail: { id: message.id, content: parts[parts.length - 1] }
1813+
})
1814+
);
1815+
}
1816+
};
1817+
17861818
// Store raw OR-aligned output items from backend
17871819
if (output) {
17881820
message.output = output;
1821+
dispatchStreamingTTS(extractTextFromOutput(output));
17891822
}
17901823
17911824
if (error) {
@@ -1796,7 +1829,9 @@
17961829
message.sources = sources;
17971830
}
17981831
1799-
if (choices) {
1832+
// Only accumulate content from choices when there's no structured output
1833+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1834+
if (choices && !output) {
18001835
if (choices[0]?.message?.content) {
18011836
// Non-stream response
18021837
message.content += choices[0]?.message?.content;
@@ -1812,30 +1847,7 @@
18121847
navigator.vibrate(5);
18131848
}
18141849
1815-
// Emit chat event for TTS (only when call overlay is active)
1816-
if ($showCallOverlay) {
1817-
const messageContentParts = getMessageContentParts(
1818-
removeAllDetails(message.content),
1819-
$config?.audio?.tts?.split_on ?? 'punctuation'
1820-
);
1821-
messageContentParts.pop();
1822-
1823-
// dispatch only last sentence and make sure it hasn't been dispatched before
1824-
if (
1825-
messageContentParts.length > 0 &&
1826-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1827-
) {
1828-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1829-
eventTarget.dispatchEvent(
1830-
new CustomEvent('chat', {
1831-
detail: {
1832-
id: message.id,
1833-
content: messageContentParts[messageContentParts.length - 1]
1834-
}
1835-
})
1836-
);
1837-
}
1838-
}
1850+
dispatchStreamingTTS(removeAllDetails(message.content));
18391851
}
18401852
}
18411853
}
@@ -1848,30 +1860,7 @@
18481860
navigator.vibrate(5);
18491861
}
18501862
1851-
// Emit chat event for TTS (only when call overlay is active)
1852-
if ($showCallOverlay) {
1853-
const messageContentParts = getMessageContentParts(
1854-
removeAllDetails(message.content),
1855-
$config?.audio?.tts?.split_on ?? 'punctuation'
1856-
);
1857-
messageContentParts.pop();
1858-
1859-
// dispatch only last sentence and make sure it hasn't been dispatched before
1860-
if (
1861-
messageContentParts.length > 0 &&
1862-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1863-
) {
1864-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1865-
eventTarget.dispatchEvent(
1866-
new CustomEvent('chat', {
1867-
detail: {
1868-
id: message.id,
1869-
content: messageContentParts[messageContentParts.length - 1]
1870-
}
1871-
})
1872-
);
1873-
}
1874-
}
1863+
dispatchStreamingTTS(removeAllDetails(message.content));
18751864
}
18761865
18771866
if (selected_model_id) {

0 commit comments

Comments
 (0)