File size: 1,864 Bytes
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
export interface ThoughtStream {
  content: string;
  reasoning: string;
  isThinking: boolean;
  hasReasoning: boolean;
}

const OPEN = '<think>';
const CLOSE = '</think>';

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,
  };
}