feat(server): API relay gateway backend M0-M4
Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with quotas, proxy gateway with weighted channel failover and health checks, usage/billing ledger, cross-protocol conversion (Anthropic Messages / OpenAI Chat Completions / OpenAI Responses), and channel/model admin API. Channels declare native API formats and auto-convert the rest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b0c7439c01
commit
d0e31b198f
@@ -0,0 +1,79 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SSEEvent is a single SSE data frame.
|
||||
type SSEEvent struct {
|
||||
Data string // the JSON payload of the `data:` line
|
||||
Done bool // true when the payload is [DONE]
|
||||
}
|
||||
|
||||
// ReadSSE reads SSE frames from r, calling fn for each `data:` line.
|
||||
// It is used both for reading upstream streams and, via a pipe, for writing
|
||||
// converted streams to the client.
|
||||
func ReadSSE(r io.Reader, fn func(SSEEvent) error) error {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
line = trimCRLF(line)
|
||||
if !bytes.HasPrefix([]byte(line), []byte("data:")) {
|
||||
continue
|
||||
}
|
||||
data := line[len("data:"):]
|
||||
data = strings.TrimPrefix(data, " ")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
ev := SSEEvent{Data: data, Done: bytes.Equal([]byte(data), []byte("[DONE]"))}
|
||||
if err := fn(ev); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes an SSE event to w and flushes it.
|
||||
func Write(w http.ResponseWriter, ev SSEEvent) error {
|
||||
if ev.Done {
|
||||
if _, err := io.WriteString(w, "data: [DONE]\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := io.WriteString(w, "data: "+ev.Data+"\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteRaw writes a raw SSE frame string (with trailing newlines) and flushes.
|
||||
func WriteRaw(w http.ResponseWriter, frame []byte) error {
|
||||
if _, err := w.Write(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimCRLF(s string) string {
|
||||
for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user