const THINK_TAG_DEFS: ReadonlyArray<{ open: string; close: string }> = [
{ open: '', close: '' },
{ open: '', close: '' },
{ open: '', close: '' },
];
const THINK_BLOCK_RE = /<(thinking|redacted_thinking|think)>[\s\S]*?<\/\1>/gi;
const UNTERMINATED_THINK_BLOCK_RE = /<(?:thinking|redacted_thinking|think)>[\s\S]*$/i;
const STRAY_THINK_CLOSE_RE = /<\/(?:thinking|redacted_thinking|think)>/gi;
const OPEN_KEEP = Math.max(1, ...THINK_TAG_DEFS.map((d) => d.open.length - 1));
function closeKeepFor(close: string): number {
return Math.max(1, close.length - 1);
}
function asciiLowerCode(c: number): number {
return c >= 65 && c <= 90 ? c + 32 : c;
}
function findInsensitive(haystack: string, needle: string): number {
const nlen = needle.length;
if (nlen === 0 || haystack.length < nlen) return -1;
const lastStart = haystack.length - nlen;
for (let i = 0; i <= lastStart; i++) {
let j = 0;
for (; j < nlen; j++) {
if (asciiLowerCode(haystack.charCodeAt(i + j)) !== asciiLowerCode(needle.charCodeAt(j)))
break;
}
if (j === nlen) return i;
}
return -1;
}
export type InlineThinkingRouter = {
push: (delta: string) => { thinking: string[]; message: string[] };
reset: () => void;
end: () => { thinking: string[]; message: string[] };
};
export function createInlineThinkingRouter(): InlineThinkingRouter {
let inThinking = false;
let activeClose = '';
let carry = '';
const flush = (): { thinking: string[]; message: string[] } => {
const thinking: string[] = [];
const message: string[] = [];
const emitThink = (s: string) => {
if (s) thinking.push(s);
};
const emitMsg = (s: string) => {
if (s) message.push(s);
};
while (carry.length > 0) {
if (!inThinking) {
let bestIdx = -1;
let bestOpenLen = 0;
let bestClose = '';
for (const def of THINK_TAG_DEFS) {
const o = findInsensitive(carry, def.open);
if (o === -1) continue;
if (bestIdx === -1 || o < bestIdx) {
bestIdx = o;
bestOpenLen = def.open.length;
bestClose = def.close;
}
}
if (bestIdx === -1) {
if (carry.length > OPEN_KEEP) {
emitMsg(carry.slice(0, carry.length - OPEN_KEEP));
carry = carry.slice(carry.length - OPEN_KEEP);
}
break;
}
if (bestIdx > 0) emitMsg(carry.slice(0, bestIdx));
carry = carry.slice(bestIdx + bestOpenLen);
inThinking = true;
activeClose = bestClose;
continue;
}
const c = findInsensitive(carry, activeClose);
if (c === -1) {
const ck = closeKeepFor(activeClose);
if (carry.length > ck) {
emitThink(carry.slice(0, carry.length - ck));
carry = carry.slice(carry.length - ck);
}
break;
}
if (c > 0) emitThink(carry.slice(0, c));
carry = carry.slice(c + activeClose.length);
inThinking = false;
activeClose = '';
}
return { thinking, message };
};
return {
push(delta: string) {
carry += delta;
return flush();
},
reset() {
carry = '';
inThinking = false;
activeClose = '';
},
end() {
const thinking: string[] = [];
const message: string[] = [];
if (!carry) return { thinking, message };
if (inThinking) {
if (carry) thinking.push(carry);
} else if (carry) {
message.push(carry);
}
carry = '';
inThinking = false;
activeClose = '';
return { thinking, message };
},
};
}
export function splitThinkingTagsFromAssistantText(raw: string): {
thinkingBodies: string[];
visible: string;
} {
const thinkingBodies: string[] = [];
let work = raw;
let guard = 0;
const maxPasses = 64;
while (guard++ < maxPasses) {
let bestIdx = -1;
let bestDef: { open: string; close: string } | null = null;
for (const def of THINK_TAG_DEFS) {
const idx = findInsensitive(work, def.open);
if (idx === -1) continue;
if (bestIdx === -1 || idx < bestIdx) {
bestIdx = idx;
bestDef = def;
}
}
if (bestIdx === -1 || !bestDef) break;
const afterOpen = bestIdx + bestDef.open.length;
const tail = work.slice(afterOpen);
const closeRel = findInsensitive(tail, bestDef.close);
if (closeRel === -1) break;
const body = tail.slice(0, closeRel).trim();
if (body) thinkingBodies.push(body);
const end = afterOpen + closeRel + bestDef.close.length;
work = (work.slice(0, bestIdx) + work.slice(end)).replace(/^\s*\n+/, '').replace(/\n+\s*$/, '');
}
return { thinkingBodies, visible: work.trim() };
}
export function stripThinkingTagsKeepVisible(raw: string): string {
const text = String(raw || '');
if (!text) return '';
return text
.replace(THINK_BLOCK_RE, '')
.replace(UNTERMINATED_THINK_BLOCK_RE, '')
.replace(STRAY_THINK_CLOSE_RE, '');
}