mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-07 18:05:30 +08:00
- Stream: strip both and [reference:N] markers to prevent leaking partial link metadata during incremental output - Non-stream: convert citation/reference markers to Markdown links for Claude Messages, Gemini generateContent, and OpenAI Chat/Responses - Remove StripReferenceMarkers option from call sites; behavior is now determined automatically by stream vs non-stream context - Extend JS runtime stripReferenceMarkersText() to also match [citation:N] - Add tests for streaming marker stripping and non-stream link conversion Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
105 lines
2.7 KiB
Go
105 lines
2.7 KiB
Go
package shared
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"ds2api/internal/sse"
|
|
)
|
|
|
|
type StreamAccumulator struct {
|
|
ThinkingEnabled bool
|
|
SearchEnabled bool
|
|
StripReferenceMarkers bool
|
|
|
|
RawThinking strings.Builder
|
|
Thinking strings.Builder
|
|
ToolDetectionThinking strings.Builder
|
|
RawText strings.Builder
|
|
Text strings.Builder
|
|
}
|
|
|
|
type StreamPartDelta struct {
|
|
Type string
|
|
RawText string
|
|
VisibleText string
|
|
CitationOnly bool
|
|
}
|
|
|
|
type StreamAccumulatorResult struct {
|
|
ContentSeen bool
|
|
Parts []StreamPartDelta
|
|
}
|
|
|
|
func (a *StreamAccumulator) Apply(parsed sse.LineResult) StreamAccumulatorResult {
|
|
out := StreamAccumulatorResult{}
|
|
for _, p := range parsed.ToolDetectionThinkingParts {
|
|
trimmed := sse.TrimContinuationOverlapFromBuilder(&a.ToolDetectionThinking, p.Text)
|
|
if trimmed != "" {
|
|
a.ToolDetectionThinking.WriteString(trimmed)
|
|
}
|
|
}
|
|
for _, p := range parsed.Parts {
|
|
if p.Type == "thinking" {
|
|
delta := a.applyThinkingPart(p.Text)
|
|
if delta.RawText != "" {
|
|
out.ContentSeen = true
|
|
}
|
|
if delta.RawText != "" || delta.VisibleText != "" {
|
|
out.Parts = append(out.Parts, delta)
|
|
}
|
|
continue
|
|
}
|
|
delta := a.applyTextPart(p.Text)
|
|
if delta.RawText != "" {
|
|
out.ContentSeen = true
|
|
}
|
|
if delta.RawText != "" || delta.VisibleText != "" || delta.CitationOnly {
|
|
out.Parts = append(out.Parts, delta)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *StreamAccumulator) applyThinkingPart(text string) StreamPartDelta {
|
|
rawTrimmed := sse.TrimContinuationOverlapFromBuilder(&a.RawThinking, text)
|
|
if rawTrimmed != "" {
|
|
a.RawThinking.WriteString(rawTrimmed)
|
|
}
|
|
delta := StreamPartDelta{Type: "thinking", RawText: rawTrimmed}
|
|
if !a.ThinkingEnabled || rawTrimmed == "" {
|
|
return delta
|
|
}
|
|
cleanedText := CleanVisibleOutput(rawTrimmed, a.StripReferenceMarkers)
|
|
if cleanedText == "" {
|
|
return delta
|
|
}
|
|
trimmed := sse.TrimContinuationOverlapFromBuilder(&a.Thinking, cleanedText)
|
|
if trimmed == "" {
|
|
return delta
|
|
}
|
|
a.Thinking.WriteString(trimmed)
|
|
delta.VisibleText = trimmed
|
|
return delta
|
|
}
|
|
|
|
func (a *StreamAccumulator) applyTextPart(text string) StreamPartDelta {
|
|
rawTrimmed := sse.TrimContinuationOverlapFromBuilder(&a.RawText, text)
|
|
if rawTrimmed == "" {
|
|
return StreamPartDelta{Type: "text"}
|
|
}
|
|
a.RawText.WriteString(rawTrimmed)
|
|
delta := StreamPartDelta{Type: "text", RawText: rawTrimmed}
|
|
if a.SearchEnabled && sse.IsCitation(rawTrimmed) {
|
|
delta.CitationOnly = true
|
|
return delta
|
|
}
|
|
cleanedText := CleanVisibleOutput(rawTrimmed, a.StripReferenceMarkers)
|
|
trimmed := sse.TrimContinuationOverlapFromBuilder(&a.Text, cleanedText)
|
|
if trimmed == "" {
|
|
return delta
|
|
}
|
|
a.Text.WriteString(trimmed)
|
|
delta.VisibleText = trimmed
|
|
return delta
|
|
}
|