package claude import ( "fmt" "net/http" "strings" "time" "ds2api/internal/sse" streamengine "ds2api/internal/stream" ) type claudeStreamRuntime struct { w http.ResponseWriter rc *http.ResponseController canFlush bool model string toolNames []string messages []any toolsRaw any promptTokenText string thinkingEnabled bool searchEnabled bool bufferToolContent bool stripReferenceMarkers bool messageID string thinking strings.Builder text strings.Builder nextBlockIndex int thinkingBlockOpen bool thinkingBlockIndex int textBlockOpen bool textBlockIndex int ended bool upstreamErr string } func newClaudeStreamRuntime( w http.ResponseWriter, rc *http.ResponseController, canFlush bool, model string, messages []any, thinkingEnabled bool, searchEnabled bool, stripReferenceMarkers bool, toolNames []string, toolsRaw any, promptTokenText string, ) *claudeStreamRuntime { return &claudeStreamRuntime{ w: w, rc: rc, canFlush: canFlush, model: model, messages: messages, thinkingEnabled: thinkingEnabled, searchEnabled: searchEnabled, bufferToolContent: len(toolNames) > 0, stripReferenceMarkers: stripReferenceMarkers, toolNames: toolNames, toolsRaw: toolsRaw, promptTokenText: promptTokenText, messageID: fmt.Sprintf("msg_%d", time.Now().UnixNano()), thinkingBlockIndex: -1, textBlockIndex: -1, } } func (s *claudeStreamRuntime) onParsed(parsed sse.LineResult) streamengine.ParsedDecision { if !parsed.Parsed { return streamengine.ParsedDecision{} } if parsed.ErrorMessage != "" { s.upstreamErr = parsed.ErrorMessage return streamengine.ParsedDecision{Stop: true, StopReason: streamengine.StopReason("upstream_error")} } if parsed.Stop { return streamengine.ParsedDecision{Stop: true} } contentSeen := false for _, p := range parsed.Parts { cleanedText := cleanVisibleOutput(p.Text, s.stripReferenceMarkers) if cleanedText == "" { continue } if p.Type != "thinking" && s.searchEnabled && sse.IsCitation(cleanedText) { continue } contentSeen = true if p.Type == "thinking" { if !s.thinkingEnabled { continue } trimmed := sse.TrimContinuationOverlap(s.thinking.String(), cleanedText) if trimmed == "" { continue } s.thinking.WriteString(trimmed) s.closeTextBlock() if !s.thinkingBlockOpen { s.thinkingBlockIndex = s.nextBlockIndex s.nextBlockIndex++ s.send("content_block_start", map[string]any{ "type": "content_block_start", "index": s.thinkingBlockIndex, "content_block": map[string]any{ "type": "thinking", "thinking": "", }, }) s.thinkingBlockOpen = true } s.send("content_block_delta", map[string]any{ "type": "content_block_delta", "index": s.thinkingBlockIndex, "delta": map[string]any{ "type": "thinking_delta", "thinking": trimmed, }, }) continue } trimmed := sse.TrimContinuationOverlap(s.text.String(), cleanedText) if trimmed == "" { continue } s.text.WriteString(trimmed) if s.bufferToolContent { if hasUnclosedCodeFence(s.text.String()) { continue } continue } s.closeThinkingBlock() if !s.textBlockOpen { s.textBlockIndex = s.nextBlockIndex s.nextBlockIndex++ s.send("content_block_start", map[string]any{ "type": "content_block_start", "index": s.textBlockIndex, "content_block": map[string]any{ "type": "text", "text": "", }, }) s.textBlockOpen = true } s.send("content_block_delta", map[string]any{ "type": "content_block_delta", "index": s.textBlockIndex, "delta": map[string]any{ "type": "text_delta", "text": trimmed, }, }) } return streamengine.ParsedDecision{ContentSeen: contentSeen} } func hasUnclosedCodeFence(text string) bool { return strings.Count(text, "```")%2 == 1 }