const TAGS = ['think', 'reasoning', 'thinking', 'thought', 'REASONING_SCRATCHPAD'] as const export interface SplitReasoning { reasoning: string text: string } export function splitReasoning(input: string): SplitReasoning { let text = input const reasoning: string[] = [] for (const tag of TAGS) { const paired = new RegExp(`<${tag}>([\\s\\S]*?)\\s*`, 'gi') text = text.replace(paired, (_m, inner: string) => { const trimmed = inner.trim() if (trimmed) { reasoning.push(trimmed) } return '' }) // Anchor to start-of-input so a literal `` mid-prose (model quoting // the word, code blocks containing the tag, etc.) doesn't eat every // paragraph after it. Real unclosed reasoning blocks always lead the // message — that's how reasoning models stream. See test // "does not strip trailing prose after a stray mid-text mention". const unclosed = new RegExp(`^\\s*<${tag}>([\\s\\S]*)$`, 'i') text = text.replace(unclosed, (_m, inner: string) => { const trimmed = inner.trim() if (trimmed) { reasoning.push(trimmed) } return '' }) } return { reasoning: reasoning.join('\n\n').trim(), text: text.trim() } } export const hasReasoningTag = (input: string) => { for (const tag of TAGS) { if (input.includes(`<${tag}>`)) { return true } } return false }