120 lines
3.0 KiB
Go
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
|
|
}
|