Skip to content

Commit b8d91f2

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

4 files changed

Lines changed: 543 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
@@ -4184,7 +4184,6 @@ async def flush_pending_delta_data(threshold: int = 0):
41844184

41854185
processed_data = {
41864186
'output': full_output(),
4187-
'content': serialize_output(full_output()),
41884187
}
41894188

41904189
# print(data)
@@ -4350,7 +4349,7 @@ async def flush_pending_delta_data(threshold: int = 0):
43504349
{
43514350
'type': 'chat:completion',
43524351
'data': {
4353-
'content': serialize_output(full_output() + pending_fc_items),
4352+
'output': full_output() + pending_fc_items,
43544353
},
43554354
}
43564355
)
@@ -4428,7 +4427,7 @@ async def flush_pending_delta_data(threshold: int = 0):
44284427
_pending_reasoning_details.extend(items)
44294428

44304429
if reasoning_content or reasoning_details_chunk:
4431-
data = {'content': serialize_output(full_output())}
4430+
data = {'output': full_output()}
44324431

44334432
if value:
44344433
if (
@@ -4587,7 +4586,7 @@ async def flush_pending_delta_data(threshold: int = 0):
45874586
)
45884587
else:
45894588
data = {
4590-
'content': serialize_output(full_output()),
4589+
'output': full_output(),
45914590
}
45924591

45934592
if delta:

src/lib/components/chat/Chat.svelte

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,9 +1169,17 @@
11691169
const messages = history ? createMessagesList(history, history.currentId) : [];
11701170
let contents = [];
11711171
messages.forEach((message) => {
1172-
if (message?.role !== 'user' && message?.content) {
1172+
if (message?.role !== 'user') {
1173+
let text = message?.content || '';
1174+
if (!text && message?.output?.length) {
1175+
text = message.output
1176+
.filter((i) => i.type === 'message')
1177+
.flatMap((i) => (i.content ?? []).map((p) => p.text ?? ''))
1178+
.join('\n');
1179+
}
1180+
if (!text) return;
11731181
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1174-
message.content
1182+
text
11751183
);
11761184
11771185
if (htmlGroups && htmlGroups.length > 0) {
@@ -1826,9 +1834,35 @@
18261834
const chatCompletionEventHandler = async (data, message, chatId) => {
18271835
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
18281836
1837+
const extractTextFromOutput = (o) =>
1838+
o
1839+
.filter((i) => i.type === 'message')
1840+
.flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim()))
1841+
.filter(Boolean)
1842+
.join('\n');
1843+
1844+
const dispatchStreamingTTS = (getText) => {
1845+
if (!$showCallOverlay) return;
1846+
const text = getText();
1847+
const parts = getMessageContentParts(
1848+
text,
1849+
$config?.audio?.tts?.split_on ?? 'punctuation'
1850+
);
1851+
parts.pop();
1852+
if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) {
1853+
message.lastSentence = parts[parts.length - 1];
1854+
eventTarget.dispatchEvent(
1855+
new CustomEvent('chat', {
1856+
detail: { id: message.id, content: parts[parts.length - 1] }
1857+
})
1858+
);
1859+
}
1860+
};
1861+
18291862
// Store raw OR-aligned output items from backend
18301863
if (output) {
18311864
message.output = output;
1865+
dispatchStreamingTTS(() => extractTextFromOutput(output));
18321866
}
18331867
18341868
if (error) {
@@ -1839,7 +1873,9 @@
18391873
message.sources = sources;
18401874
}
18411875
1842-
if (choices) {
1876+
// Only accumulate content from choices when there's no structured output
1877+
// (output-based rendering takes priority — avoids redundant serialize/parse round-trip)
1878+
if (choices && !output) {
18431879
if (choices[0]?.message?.content) {
18441880
// Non-stream response
18451881
message.content += choices[0]?.message?.content;
@@ -1855,30 +1891,7 @@
18551891
navigator.vibrate(5);
18561892
}
18571893
1858-
// Emit chat event for TTS (only when call overlay is active)
1859-
if ($showCallOverlay) {
1860-
const messageContentParts = getMessageContentParts(
1861-
removeAllDetails(message.content),
1862-
$config?.audio?.tts?.split_on ?? 'punctuation'
1863-
);
1864-
messageContentParts.pop();
1865-
1866-
// dispatch only last sentence and make sure it hasn't been dispatched before
1867-
if (
1868-
messageContentParts.length > 0 &&
1869-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1870-
) {
1871-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1872-
eventTarget.dispatchEvent(
1873-
new CustomEvent('chat', {
1874-
detail: {
1875-
id: message.id,
1876-
content: messageContentParts[messageContentParts.length - 1]
1877-
}
1878-
})
1879-
);
1880-
}
1881-
}
1894+
dispatchStreamingTTS(() => removeAllDetails(message.content));
18821895
}
18831896
}
18841897
}
@@ -1891,30 +1904,7 @@
18911904
navigator.vibrate(5);
18921905
}
18931906
1894-
// Emit chat event for TTS (only when call overlay is active)
1895-
if ($showCallOverlay) {
1896-
const messageContentParts = getMessageContentParts(
1897-
removeAllDetails(message.content),
1898-
$config?.audio?.tts?.split_on ?? 'punctuation'
1899-
);
1900-
messageContentParts.pop();
1901-
1902-
// dispatch only last sentence and make sure it hasn't been dispatched before
1903-
if (
1904-
messageContentParts.length > 0 &&
1905-
messageContentParts[messageContentParts.length - 1] !== message.lastSentence
1906-
) {
1907-
message.lastSentence = messageContentParts[messageContentParts.length - 1];
1908-
eventTarget.dispatchEvent(
1909-
new CustomEvent('chat', {
1910-
detail: {
1911-
id: message.id,
1912-
content: messageContentParts[messageContentParts.length - 1]
1913-
}
1914-
})
1915-
);
1916-
}
1917-
}
1907+
dispatchStreamingTTS(() => removeAllDetails(message.content));
19181908
}
19191909
19201910
if (selected_model_id) {

0 commit comments

Comments
 (0)