-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
fix: strip inline <thought>/<think> tags from OpenAI-compatible provider content #4037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package thinking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/tidwall/gjson" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -39,3 +42,217 @@ func GetThinkingText(part gjson.Result) string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // thoughtTagPattern matches <thought>...</thought> and <think>...</think> tags, including | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // multiline content. It is used to extract thinking content that providers like MiniMax | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // embed inside XML tags within the regular content field. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var thoughtTagPattern = regexp.MustCompile(`<thought>([\s\S]*?)</thought>|<think>([\s\S]*?)</think>`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // StripThoughtTags removes <thought>...</thought> and <think>...</think> tags from text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // returning the extracted thinking content and the cleaned visible text. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Some OpenAI-compatible providers (e.g., MiniMax M3 via OpenCode Go) embed reasoning | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // content inside XML tags within the regular content field instead of using the standard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // reasoning_content field. This function separates the tagged thinking content so it can | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // be placed into proper Anthropic thinking blocks. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If no tags are found, thinking returns empty string and clean is the original text. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func StripThoughtTags(text string) (thinking, clean string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(text, "<thought") && !strings.Contains(text, "<think") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var thinkingParts []string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clean = thoughtTagPattern.ReplaceAllStringFunc(text, func(match string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract content between tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inner := match | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.HasPrefix(match, "<thought>") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inner = match[len("<thought>") : len(match)-len("</thought>")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if strings.HasPrefix(match, "<think>") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inner = match[len("<think>") : len(match)-len("</think>")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if trimmed := strings.TrimSpace(inner); trimmed != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinkingParts = append(thinkingParts, trimmed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return " " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(thinkingParts) > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinking = strings.Join(thinkingParts, "\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Collapse multiple spaces introduced by tag removal. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clean = strings.TrimSpace(clean) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clean = multiSpace.ReplaceAllString(clean, " ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return thinking, clean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // multiSpace matches two or more consecutive whitespace characters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var multiSpace = regexp.MustCompile(`\s{2,}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ThoughtTagStripper maintains state for stripping thought tags across streaming chunks. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Some providers emit <thought> tags that span multiple SSE delta chunks, requiring | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // stateful processing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ThoughtTagStripper struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buf strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inThought bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentTag string // "thought" or "think" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinkingBuf strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // NewThoughtTagStripper creates a new ThoughtTagStripper instance. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func NewThoughtTagStripper() *ThoughtTagStripper { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &ThoughtTagStripper{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Feed processes a streaming content chunk and returns any extracted thinking text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and the cleaned visible text. The thinking and visible outputs may each be empty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // if the chunk is entirely consumed by tag processing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *ThoughtTagStripper) Feed(chunk string) (thinking, visible string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.buf.WriteString(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full := s.buf.String() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.buf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var thinkingOut strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var visibleOut strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitVisible := func(text string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = strings.TrimSpace(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if visibleOut.Len() > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visibleOut.WriteByte(' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visibleOut.WriteString(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitThinking := func(text string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = strings.TrimSpace(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if thinkingOut.Len() > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinkingOut.WriteByte('\n') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinkingOut.WriteString(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i := 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i < len(full) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.inThought { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closeTag := "</" + s.currentTag + ">" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closeIdx := strings.Index(full[i:], closeTag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if closeIdx >= 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.WriteString(full[i : i+closeIdx]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitThinking(s.thinkingBuf.String()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += closeIdx + len(closeTag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.inThought = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.currentTag = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.WriteString(full[i:]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i = len(full) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the closing tag is split across SSE chunks, for example Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not in a thought — look for an opening tag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thoughtIdx := strings.Index(full[i:], "<thought>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinkIdx := strings.Index(full[i:], "<think>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openIdx := -1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagName := "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if thoughtIdx >= 0 && (thinkIdx < 0 || thoughtIdx <= thinkIdx) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openIdx = thoughtIdx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagName = "thought" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if thinkIdx >= 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openIdx = thinkIdx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagName = "think" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if openIdx < 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No opening tag in buffer. Check for a partial tag at the very end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that could complete on the next chunk. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remaining := full[i:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partialStart := lastPotentialTagPrefix(remaining) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if partialStart >= 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitVisible(remaining[:partialStart]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Keep the partial prefix in buffer for next chunk. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.buf.WriteString(remaining[partialStart:]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitVisible(remaining) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i = len(full) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Emit text before the tag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitVisible(full[i : i+openIdx]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Enter thought. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += openIdx + len("<"+tagName+">") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.inThought = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.currentTag = tagName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if closing tag is in the same buffer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closeTag := "</" + tagName + ">" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if closeIdx := strings.Index(full[i:], closeTag); closeIdx >= 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emitThinking(full[i : i+closeIdx]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += closeIdx + len(closeTag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.inThought = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.currentTag = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.WriteString(full[i:]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i = len(full) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return thinkingOut.String(), visibleOut.String() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
+211
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two critical issues in the streaming
We should rewrite func (s *ThoughtTagStripper) Feed(chunk string) (thinking, visible string) {
s.buf.WriteString(chunk)
full := s.buf.String()
s.buf.Reset()
var thinkingOut strings.Builder
var visibleOut strings.Builder
emitVisible := func(text string) {
visibleOut.WriteString(text)
}
emitThinking := func(text string) {
thinkingOut.WriteString(text)
}
i := 0
for i < len(full) {
if s.inThought {
closeTag := "</" + s.currentTag + ">"
closeIdx := strings.Index(full[i:], closeTag)
if closeIdx >= 0 {
s.thinkingBuf.WriteString(full[i : i+closeIdx])
emitThinking(s.thinkingBuf.String())
s.thinkingBuf.Reset()
i += closeIdx + len(closeTag)
s.inThought = false
s.currentTag = ""
} else {
remaining := full[i:]
partialStart := lastPotentialTagPrefix(remaining)
if partialStart >= 0 {
s.thinkingBuf.WriteString(remaining[:partialStart])
s.buf.WriteString(remaining[partialStart:])
} else {
s.thinkingBuf.WriteString(remaining)
}
i = len(full)
}
continue
}
// Not in a thought — look for an opening tag.
thoughtIdx := strings.Index(full[i:], "<thought>")
thinkIdx := strings.Index(full[i:], "<think>")
openIdx := -1
tagName := ""
if thoughtIdx >= 0 && (thinkIdx < 0 || thoughtIdx <= thinkIdx) {
openIdx = thoughtIdx
tagName = "thought"
} else if thinkIdx >= 0 {
openIdx = thinkIdx
tagName = "think"
}
if openIdx < 0 {
// No opening tag in buffer. Check for a partial tag at the very end
// that could complete on the next chunk.
remaining := full[i:]
partialStart := lastPotentialTagPrefix(remaining)
if partialStart >= 0 {
emitVisible(remaining[:partialStart])
// Keep the partial prefix in buffer for next chunk.
s.buf.WriteString(remaining[partialStart:])
} else {
emitVisible(remaining)
}
i = len(full)
continue
}
// Emit text before the tag.
emitVisible(full[i : i+openIdx])
// Enter thought.
i += openIdx + len("<"+tagName+">")
s.inThought = true
s.currentTag = tagName
// Check if closing tag is in the same buffer.
closeTag := "</" + tagName + ">"
if closeIdx := strings.Index(full[i:], closeTag); closeIdx >= 0 {
emitThinking(full[i : i+closeIdx])
i += closeIdx + len(closeTag)
s.inThought = false
s.currentTag = ""
} else {
remaining := full[i:]
partialStart := lastPotentialTagPrefix(remaining)
if partialStart >= 0 {
s.thinkingBuf.WriteString(remaining[:partialStart])
s.buf.WriteString(remaining[partialStart:])
} else {
s.thinkingBuf.WriteString(remaining)
}
i = len(full)
}
}
return thinkingOut.String(), visibleOut.String()
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // lastPotentialTagPrefix checks if the trailing portion of s looks like it could be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the start of <thought>, </thought>, <think>, or </think>. Returns the index within s | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // where the potential tag starts, or -1 if no partial tag prefix is detected. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func lastPotentialTagPrefix(s string) int { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastLT := strings.LastIndex(s, "<") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if lastLT < 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return -1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| candidate := s[lastLT:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, tag := range []string{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "<thought>", "</thought>", "<think>", "</think>", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.HasPrefix(tag, candidate) && len(candidate) < len(tag) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return lastLT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return -1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Flush returns any remaining buffered thinking text. Call this when the stream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ends to ensure no thinking content is lost. Also returns any remaining visible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // text in the buffer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *ThoughtTagStripper) Flush() (thinking, visible string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.inThought { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Unterminated thought tag — treat buffered content as thinking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thinking = strings.TrimSpace(s.thinkingBuf.String()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Any remaining non-tag content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !s.inThought { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visible = strings.TrimSpace(s.buf.String()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.buf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.inThought = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.currentTag = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return thinking, visible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+236
to
+250
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reset clears all internal state for reuse. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *ThoughtTagStripper) Reset() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.buf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.thinkingBuf.Reset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.inThought = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.currentTag = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When normal streaming content is passed through
Feed, thisTrimSpacestrips leading/trailing whitespace from every delta even when there are no thought tags. OpenAI-style streams commonly send tokens like"Hello"followed by" world"; after this change the emitted Anthropic text deltas become"Hello"and"world", so clients reconstruct"Helloworld"instead of the original text. The stripper should preserve visible bytes outside tags rather than trimming each chunk.Useful? React with 👍 / 👎.