fix: strip think tags when model emits leading whitespace before <think> (#327)

Remove ^ anchor from think/Gemma regexes in ui.js; trimStart() before startsWith checks in messages.js streaming path. Fixes MiniMax M2.7 and any model emitting leading newlines before <think>. 10 new tests, 768 total.
This commit is contained in:
nesquena-hermes
2026-04-12 14:07:00 -07:00
committed by GitHub
parent 7552cd3e9b
commit 9c44d0cf3e
5 changed files with 132 additions and 10 deletions

View File

@@ -526,7 +526,7 @@
<div class="settings-section-title">System</div>
<div class="settings-section-meta">Instance version and access controls.</div>
</div>
<span class="settings-version-badge">v0.50.3</span>
<span class="settings-version-badge">v0.50.5</span>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>

View File

@@ -116,18 +116,21 @@ async function send(){
function _streamDisplay(){
const raw=assistantText;
for(const {open,close} of _thinkPairs){
if(raw.startsWith(open)){
const ci=raw.indexOf(close,open.length);
// Trim leading whitespace before checking for the open tag — some models
// (e.g. MiniMax) emit newlines before <think>.
const trimmed=raw.trimStart();
if(trimmed.startsWith(open)){
const ci=trimmed.indexOf(close,open.length);
if(ci!==-1){
// Thinking block complete — strip it, show the rest
return raw.slice(ci+close.length).replace(/^\s+/,'');
return trimmed.slice(ci+close.length).replace(/^\s+/,'');
}
// Still inside thinking block — show placeholder
return '';
}
// Hide partial tag prefixes while streaming so users don't see
// `<thi`, `<think`, etc. before the model finishes the token.
if(open.startsWith(raw)) return '';
if(open.startsWith(trimmed)) return '';
}
return raw;
}

View File

@@ -801,19 +801,20 @@ function renderMessages(){
if(!thinkingText && m.reasoning){
thinkingText=m.reasoning;
}
// Parse inline thinking tags from plain text: <think>...</think> (DeepSeek, QwQ, etc.)
// Parse inline thinking tags from plain text: <think>...</think> (DeepSeek, QwQ, MiniMax, etc.)
// and Gemma 4 channel tokens: <|channel>thought\n...<channel|>
// Note: no ^ anchor — some models emit leading whitespace/newlines before <think>.
if(!thinkingText && typeof content==='string'){
const thinkMatch=content.match(/^<think>([\s\S]*?)<\/think>\s*/);
const thinkMatch=content.match(/<think>([\s\S]*?)<\/think>/);
if(thinkMatch){
thinkingText=thinkMatch[1].trim();
content=content.slice(thinkMatch[0].length);
content=content.replace(/<think>[\s\S]*?<\/think>\s*/,'').trimStart();
}
if(!thinkingText){
const gemmaMatch=content.match(/^<\|channel>thought\n([\s\S]*?)<channel\|>\s*/);
const gemmaMatch=content.match(/<\|channel>thought\n([\s\S]*?)<channel\|>/);
if(gemmaMatch){
thinkingText=gemmaMatch[1].trim();
content=content.slice(gemmaMatch[0].length);
content=content.replace(/<\|channel>thought\n[\s\S]*?<channel\|>\s*/,'').trimStart();
}
}
}