veylant/internal/provider/anthropic/sse.go
2026-02-23 13:35:04 +01:00

120 lines
3.0 KiB
Go

package anthropic
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// Anthropic SSE event types handled by PipeAnthropicSSE.
const (
eventContentBlockDelta = "content_block_delta"
eventMessageStop = "message_stop"
)
// anthropicStreamEvent is the data payload for a content_block_delta SSE event.
type anthropicStreamEvent struct {
Type string `json:"type"`
Delta anthropicDelta `json:"delta"`
}
type anthropicDelta struct {
Type string `json:"type"`
Text string `json:"text"`
}
// openAIChunk is the minimal OpenAI-compatible SSE chunk emitted to the client.
type openAIChunk struct {
ID string `json:"id"`
Object string `json:"object"`
Choices []openAIChunkChoice `json:"choices"`
}
type openAIChunkChoice struct {
Index int `json:"index"`
Delta openAIChunkDelta `json:"delta"`
}
type openAIChunkDelta struct {
Content string `json:"content"`
}
// PipeAnthropicSSE reads Anthropic's event-typed SSE stream and re-emits each
// text chunk as an OpenAI-compatible SSE event. It terminates on message_stop
// or when the upstream body is exhausted.
//
// Anthropic SSE format:
//
// event: content_block_delta
// data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}
//
// Re-emitted as OpenAI-compat:
//
// data: {"id":"chatcmpl-anthropic","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"}}]}
func PipeAnthropicSSE(body *bufio.Scanner, w http.ResponseWriter) error {
flusher, ok := w.(http.Flusher)
if !ok {
return fmt.Errorf("response writer does not support flushing (SSE not possible)")
}
var currentEvent string
for body.Scan() {
line := body.Text()
// Track the event type from "event: ..." lines.
if strings.HasPrefix(line, "event: ") {
currentEvent = strings.TrimPrefix(line, "event: ")
continue
}
// Skip blank lines and lines without a data prefix.
if line == "" || !strings.HasPrefix(line, "data: ") {
continue
}
rawData := strings.TrimPrefix(line, "data: ")
switch currentEvent {
case eventContentBlockDelta:
var evt anthropicStreamEvent
if err := json.Unmarshal([]byte(rawData), &evt); err != nil {
continue // skip malformed events gracefully
}
if evt.Delta.Type != "text_delta" || evt.Delta.Text == "" {
continue
}
chunk := openAIChunk{
ID: "chatcmpl-anthropic",
Object: "chat.completion.chunk",
Choices: []openAIChunkChoice{{
Index: 0,
Delta: openAIChunkDelta{Content: evt.Delta.Text},
}},
}
chunkJSON, err := json.Marshal(chunk)
if err != nil {
continue
}
if _, err := fmt.Fprintf(w, "data: %s\n\n", chunkJSON); err != nil {
return fmt.Errorf("writing SSE chunk: %w", err)
}
flusher.Flush()
case eventMessageStop:
if _, err := fmt.Fprintf(w, "data: [DONE]\n\n"); err != nil {
return fmt.Errorf("writing SSE done: %w", err)
}
flusher.Flush()
return nil
}
}
if err := body.Err(); err != nil {
return fmt.Errorf("reading Anthropic SSE body: %w", err)
}
return nil
}