Skip to content

Commit 5e150fd

Browse files
committed
refactor: render from output and derive content at save
1 parent 5822c6d commit 5e150fd

4 files changed

Lines changed: 533 additions & 80 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: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,9 +1800,34 @@
18001800
const chatCompletionEventHandler = async (data, message, chatId) => {
18011801
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
18021802
1803+
const extractTextFromOutput = (o) =>
1804+
o
1805+
.filter((i) => i.type === 'message')
1806+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1807+
.filter(Boolean)
1808+
.join('\n');
1809+
1810+
const dispatchStreamingTTS = (text) => {
1811+
if (!$showCallOverlay) return;
1812+
const parts = getMessageContentParts(
1813+
text,
1814+
$config?.audio?.tts?.split_on ?? 'punctuation'
1815+
);
1816+
parts.pop();
1817+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1818+
message.lastSentence = parts[parts.length - 1];
1819+
eventTarget.dispatchEvent(
1820+
new CustomEvent('chat', {
1821+
detail: { id: message.id, content: parts[parts.length - 1] }
1822+
})
1823+
);
1824+
}
1825+
};
1826+
18031827
// Store raw OR-aligned output items from backend
18041828
if (output) {
18051829
message.output = output;
1830+
dispatchStreamingTTS(extractTextFromOutput(output));
18061831
}
18071832
18081833
if (error) {
@@ -1813,7 +1838,9 @@
18131838
message.sources = sources;
18141839
}
18151840
1816-
if (choices) {
1841+
// Only accumulate content from choices when there's no structured output
1842+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1843+
if (choices && !output) {
18171844
if (choices[0]?.message?.content) {
18181845
// Non-stream response
18191846
message.content += choices[0]?.message?.content;
@@ -1829,30 +1856,7 @@
18291856
navigator.vibrate(5);
18301857
}
18311858
1832-
// Emit chat event for TTS (only when call overlay is active)
1833-
if ($showCallOverlay) {
1834-
const messageContentParts = getMessageContentParts(
1835-
removeAllDetails(message.content),
1836-
$config?.audio?.tts?.split_on ?? 'punctuation'
1837-
);
1838-
messageContentParts.pop();
1839-
1840-
// dispatch only last sentence and make sure it hasn't been dispatched before
1841-
if (
1842-
messageContentParts.length > 0 &&
1843-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1844-
) {
1845-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1846-
eventTarget.dispatchEvent(
1847-
new CustomEvent('chat', {
1848-
detail: {
1849-
id: message.id,
1850-
content: messageContentParts[messageContentParts.length - 1]
1851-
}
1852-
})
1853-
);
1854-
}
1855-
}
1859+
dispatchStreamingTTS(removeAllDetails(message.content));
18561860
}
18571861
}
18581862
}
@@ -1865,30 +1869,7 @@
18651869
navigator.vibrate(5);
18661870
}
18671871
1868-
// Emit chat event for TTS (only when call overlay is active)
1869-
if ($showCallOverlay) {
1870-
const messageContentParts = getMessageContentParts(
1871-
removeAllDetails(message.content),
1872-
$config?.audio?.tts?.split_on ?? 'punctuation'
1873-
);
1874-
messageContentParts.pop();
1875-
1876-
// dispatch only last sentence and make sure it hasn't been dispatched before
1877-
if (
1878-
messageContentParts.length > 0 &&
1879-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1880-
) {
1881-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1882-
eventTarget.dispatchEvent(
1883-
new CustomEvent('chat', {
1884-
detail: {
1885-
id: message.id,
1886-
content: messageContentParts[messageContentParts.length - 1]
1887-
}
1888-
})
1889-
);
1890-
}
1891-
}
1872+
dispatchStreamingTTS(removeAllDetails(message.content));
18921873
}
18931874
18941875
if (selected_model_id) {

0 commit comments

Comments
 (0)