返回 DeepSeek-Reasonix
think.go
根目录 / internal / provider / openai / think.go
1 package openai
2
3 import "strings"
4
5 const (
6 thinkOpen = "<think>"
7 thinkClose = "</think>"
8 )
9
10 type thinkState int
11
12 const (
13 thinkProbe thinkState = iota
14 thinkInside
15 thinkPassthrough
16 )
17
18 // thinkSplitter peels a leading <think>...</think> block out of the content
19 // stream into reasoning text. MiniMax-M3 inlines its chain-of-thought this way
20 // instead of populating reasoning_content. It only arms on a <think> at the very
21 // start of the turn, so an answer that merely mentions the tag is never hijacked.
22 type thinkSplitter struct {
23 state thinkState
24 buf string
25 }
26
27 func (t *thinkSplitter) push(s string) (reasoning, text string) {
28 switch t.state {
29 case thinkPassthrough:
30 return "", s
31 case thinkInside:
32 return t.scanClose(s)
33 }
34
35 t.buf += s
36 trimmed := strings.TrimLeft(t.buf, " \t\r\n")
37 if len(trimmed) < len(thinkOpen) {
38 if strings.HasPrefix(thinkOpen, trimmed) {
39 return "", "" // still could become <think> once more arrives
40 }
41 return "", t.drainPassthrough()
42 }
43 if strings.HasPrefix(trimmed, thinkOpen) {
44 t.state = thinkInside
45 t.buf = ""
46 return t.scanClose(trimmed[len(thinkOpen):])
47 }
48 return "", t.drainPassthrough()
49 }
50
51 func (t *thinkSplitter) scanClose(s string) (reasoning, text string) {
52 t.buf += s
53 if idx := strings.Index(t.buf, thinkClose); idx >= 0 {
54 r := t.buf[:idx]
55 rest := strings.TrimLeft(t.buf[idx+len(thinkClose):], " \t\r\n")
56 t.buf = ""
57 t.state = thinkPassthrough
58 return r, rest
59 }
60 keep := markerSuffixLen(t.buf, thinkClose)
61 r := t.buf[:len(t.buf)-keep]
62 t.buf = t.buf[len(t.buf)-keep:]
63 return r, ""
64 }
65
66 // flush emits whatever is buffered when the stream ends mid-decision: an
67 // unterminated <think> block is reasoning; anything else is text.
68 func (t *thinkSplitter) flush() (reasoning, text string) {
69 if t.buf == "" {
70 return "", ""
71 }
72 out := t.buf
73 t.buf = ""
74 if t.state == thinkInside {
75 return out, ""
76 }
77 return "", out
78 }
79
80 func (t *thinkSplitter) drainPassthrough() string {
81 t.state = thinkPassthrough
82 out := t.buf
83 t.buf = ""
84 return out
85 }
86
87 // markerSuffixLen returns the length of the longest proper suffix of s that is a
88 // prefix of marker — the tail to hold back in case the rest of the tag arrives
89 // in the next delta.
90 func markerSuffixLen(s, marker string) int {
91 max := len(marker) - 1
92 if max > len(s) {
93 max = len(s)
94 }
95 for n := max; n > 0; n-- {
96 if strings.HasPrefix(marker, s[len(s)-n:]) {
97 return n
98 }
99 }
100 return 0
101 }
102
102 lines GO