export interface ThoughtStream { content: string; reasoning: string; isThinking: boolean; hasReasoning: boolean; } const OPEN = ''; const CLOSE = ''; function trimBoundary(value: string): string { return value.replace(/^\n+|\n+$/g, ''); } function partialMarkerLength(value: string, marker: string): number { const maximum = Math.min(value.length, marker.length - 1); for (let length = maximum; length > 0; length -= 1) { if (value.endsWith(marker.slice(0, length))) return length; } return 0; } export function parseThoughtStream(value: string): ThoughtStream { const opening = value.indexOf(OPEN); const closing = value.indexOf(CLOSE, opening >= 0 ? opening + OPEN.length : 0); if (opening >= 0) { if (closing >= 0) { return { content: trimBoundary(value.slice(0, opening) + value.slice(closing + CLOSE.length)), reasoning: trimBoundary(value.slice(opening + OPEN.length, closing)), isThinking: false, hasReasoning: true, }; } const partialClose = partialMarkerLength(value, CLOSE); return { content: trimBoundary(value.slice(0, opening)), reasoning: trimBoundary(value.slice(opening + OPEN.length, value.length - partialClose)), isThinking: true, hasReasoning: true, }; } // Some llama.cpp chat templates put the opening marker in the prompt, so // only the closing marker appears in generated text. if (closing >= 0) { return { content: trimBoundary(value.slice(closing + CLOSE.length)), reasoning: trimBoundary(value.slice(0, closing)), isThinking: false, hasReasoning: true, }; } const partialOpen = partialMarkerLength(value, OPEN); return { content: value.slice(0, value.length - partialOpen), reasoning: '', isThinking: false, hasReasoning: false, }; }