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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
11
static/ui.js
11
static/ui.js
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user