Feat: Add toggle to exclude think/reason blocks from smooth streaming (#4849) * Feat: Add toggle to exclude think/reason blocks from smooth streaming * Fix: Adjust new setting name to properly reflect what it does * Sync data-i18n with text content * Add reasoning stream flags for Mistral, Claude and Google models * Update layout * Fix: Enhance reasoning handling in parseStreamData function --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

6c8eb4d9ac6b9e76fd5ed53ad6e2739f112131bf

Kristian Schlikow <Dakraid@users.noreply.github.com>

Signed
4 files changed, +55 -10Showing whitespace changes
public/css/toggle-dependent.css+2 -5
@@ -477,14 +477,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
477477 display: none !important;
478478}
479479
480480#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_speed_control {,
481+#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_no_think_control {
481482 display: none;
482483}
483484
484-#smooth_streaming:checked~#smooth_streaming_speed_control {
485- display: block;
486-}
487-
488485.mdhotkey_icon {
489486 opacity: 0.6;
490487}
public/index.html+9 -3
@@ -5029,14 +5029,21 @@
50295029 <small data-i18n="Clean-Up">Clean-Up</small>
50305030 </div>
50315031 </div>
50325032 <label id="smooth_streaming_control" class="checkbox_label flexWrap" for="smooth_streaming">
50335033 <input id="smooth_streaming" type="checkbox" />
50345034 <div class="flex-container alignItemsBaseline">
50355035 <small data-i18n="Smooth Streaming">
50365036 Smooth Streaming
50375037 </small>
50385038 </div>
5039- <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p">
5039+ </label>
5040+ <label id="smooth_streaming_no_think_control" class="checkbox_label" for="smooth_streaming_no_think" title="Bypass smooth streaming in reasoning blocks." data-i18n="[title]Bypass smooth streaming in reasoning blocks.">
5041+ <input id="smooth_streaming_no_think" type="checkbox" />
5042+ <small data-i18n="Exclude 'Thinking...'">
5043+ Exclude 'Thinking...'
5044+ </small>
5045+ </label>
5046+ <div id="smooth_streaming_speed_control" class="wide100p">
50405047 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
50415048 <div class="slider_hint">
50425049 <span data-i18n="Slow">Slow</span>
@@ -5044,7 +5051,6 @@
50445051 <span data-i18n="Fast">Fast</span>
50455052 </div>
50465053 </div>
5047- </label>
50485054 <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in">
50495055 <input id="stream_fade_in" type="checkbox" />
50505056 <small data-i18n="Stream Fade-In">Stream Fade-In</small>
public/scripts/power-user.js+7 -0
@@ -139,6 +139,7 @@ export const power_user = {
139139 chat_truncation: 100,
140140 streaming_fps: 30,
141141 smooth_streaming: false,
142+ smooth_streaming_no_think: false,
142143 smooth_streaming_speed: 50,
143144 stream_fade_in: false,
144145
@@ -1745,6 +1746,7 @@ export async function loadPowerUserSettings(settings, data) {
17451746 $('#streaming_fps_counter').val(power_user.streaming_fps);
17461747
17471748 $('#smooth_streaming').prop('checked', power_user.smooth_streaming);
1749+ $('#smooth_streaming_no_think').prop('checked', power_user.smooth_streaming_no_think);
17481750 $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed);
17491751
17501752 $('#stream_fade_in').prop('checked', power_user.stream_fade_in);
@@ -3550,6 +3552,11 @@ jQuery(() => {
35503552 saveSettingsDebounced();
35513553 });
35523554
3555+ $('#smooth_streaming_no_think').on('input', function () {
3556+ power_user.smooth_streaming_no_think = !!$(this).prop('checked');
3557+ saveSettingsDebounced();
3558+ });
3559+
35533560 $('#smooth_streaming_speed').on('input', function () {
35543561 power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val());
35553562 saveSettingsDebounced();
public/scripts/sse-stream.js+37 -2
@@ -105,7 +105,7 @@ function getDelay(s) {
105105/**
106106 * Parses the stream data and returns the parsed data and the chunk to be sent.
107107 * @param {object} json The JSON data.
108108 * @returns {AsyncGenerator<{data: object, chunk: string, reasoning?: boolean}>} The parsed data and the chunk to be sent.
109109 */
110110async function* parseStreamData(json) {
111111 // Cohere
@@ -133,6 +133,19 @@ async function* parseStreamData(json) {
133133 }
134134 return;
135135 }
136+ else if (typeof json.delta === 'object' && typeof json.delta.thinking === 'string') {
137+ if (json.delta.thinking.length > 0) {
138+ for (let i = 0; i < json.delta.thinking.length; i++) {
139+ const str = json.delta.thinking[i];
140+ yield {
141+ data: { ...json, delta: { thinking: str } },
142+ chunk: str,
143+ reasoning: true,
144+ };
145+ }
146+ }
147+ return;
148+ }
136149 // MakerSuite
137150 else if (Array.isArray(json.candidates)) {
138151 for (let i = 0; i < json.candidates.length; i++) {
@@ -159,9 +172,11 @@ async function* parseStreamData(json) {
159172 candidateClone.content.parts[j].text = str;
160173 candidateClone.content.parts = [candidateClone.content.parts[j]];
161174 const candidates = [candidateClone];
175+ const reasoning = json.candidates[i].content.parts[j].thought ?? false;
162176 yield {
163177 data: { ...json, candidates },
164178 chunk: str,
179+ reasoning,
165180 };
166181 }
167182 }
@@ -237,6 +252,7 @@ async function* parseStreamData(json) {
237252 yield {
238253 data: { ...json, choices },
239254 chunk: str,
255+ reasoning: true,
240256 };
241257 }
242258 return;
@@ -252,6 +268,7 @@ async function* parseStreamData(json) {
252268 yield {
253269 data: { ...json, choices },
254270 chunk: str,
271+ reasoning: true,
255272 };
256273 }
257274 return;
@@ -269,6 +286,24 @@ async function* parseStreamData(json) {
269286 }
270287 return;
271288 }
289+ else if (Array.isArray(json.choices[0].delta.content) && json.choices[0].delta.content.length > 0) {
290+ if (Array.isArray(json.choices[0].delta.content[0].thinking) && json.choices[0].delta.content[0].thinking.length > 0) {
291+ if (typeof json.choices[0].delta.content[0].thinking[0].text === 'string' && json.choices[0].delta.content[0].thinking[0].text.length > 0) {
292+ for (let j = 0; j < json.choices[0].delta.content[0].thinking[0].text.length; j++) {
293+ const str = json.choices[0].delta.content[0].thinking[0].text[j];
294+ const choiceClone = structuredClone(json.choices[0]);
295+ choiceClone.delta.content[0].thinking[0].text = str;
296+ const choices = [choiceClone];
297+ yield {
298+ data: { ...json, choices },
299+ chunk: str,
300+ reasoning: true,
301+ };
302+ }
303+ return;
304+ }
305+ }
306+ }
272307 }
273308 else if (typeof json.choices[0].message === 'object') {
274309 if (typeof json.choices[0].message.content === 'string' && json.choices[0].message.content.length > 0) {
@@ -317,7 +352,7 @@ export class SmoothEventSourceStream extends EventSourceStream {
317352 }
318353
319354 for await (const parsed of parseStreamData(json)) {
320355 !(power_user.smooth_streaming_no_think && parsed.reasoning) && hasFocus && await delay(getDelay(lastStr));
321356 controller.enqueue(new MessageEvent(event.type, { data: JSON.stringify(parsed.data) }));
322357 lastStr = parsed.chunk;
323358 }