refactor: complete backend rewrite for multi-protocol proxy
Major rewrite of the Go backend to support:
- Three API format imports: openai, anthropic, compatible
- Three protocol conversions: Chat Completions, Responses, Messages
- Hub-and-spoke architecture with Chat as intermediate format
Deleted:
- opencat.go (old entry)
- store/, team/, pkg/team/, pkg/store/ (old data layer)
- internal/model/, internal/consts/ (old types)
- internal/service/team/, internal/controller/team/ (old handlers)
- llm/ (removed LLM client library, pure proxy mode)
- dist/, assets/ (old build artifacts)
Added:
- internal/store/ — 9 GORM models + multi-DB support
- internal/pkg/ — crypto (AES-GCM), apikey, jwt, ratelimit, resp, tokenizer
- internal/channel/ — channel selection, weighted LB, health checks
- internal/proxy/convert/ — 6 protocol conversion functions + SSE streaming
- internal/proxy/ — gateway with request dispatch and upstream selection
- internal/usage/ — async usage recorder with batch writes
- internal/api/ — management API (auth, users, keys, channels, models)
- Makefile for build/test/deploy
Fixed API to match frontend expectations:
- Login response wraps token in { data: { token } }
- GET /api/profile route added
- Profile response wraps user in { code, data }
- Role returned as number (10=admin, 1=user)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SSEWriter writes Server-Sent Events
|
||||
type SSEWriter struct {
|
||||
writer io.Writer
|
||||
flusher http.Flusher
|
||||
}
|
||||
|
||||
// NewSSEWriter creates a new SSE writer
|
||||
func NewSSEWriter(w http.ResponseWriter) *SSEWriter {
|
||||
flusher, _ := w.(http.Flusher)
|
||||
return &SSEWriter{
|
||||
writer: w,
|
||||
flusher: flusher,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteEvent writes a single SSE event
|
||||
func (w *SSEWriter) WriteEvent(event string, data interface{}) error {
|
||||
var dataStr string
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
dataStr = v
|
||||
default:
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dataStr = string(b)
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(w.writer, "event: %s\ndata: %s\n\n", event, dataStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.flusher != nil {
|
||||
w.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteChunk writes a streaming chunk in SSE format
|
||||
func (w *SSEWriter) WriteChunk(chunk interface{}) error {
|
||||
b, err := json.Marshal(chunk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w.writer, "data: %s\n\n", string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.flusher != nil {
|
||||
w.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteDone writes the [DONE] marker
|
||||
func (w *SSEWriter) WriteDone() error {
|
||||
_, err := fmt.Fprintf(w.writer, "data: [DONE]\n\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.flusher != nil {
|
||||
w.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSEParser parses Server-Sent Events from a reader
|
||||
type SSEParser struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// NewSSEParser creates a new SSE parser
|
||||
func NewSSEParser(r io.Reader) *SSEParser {
|
||||
return &SSEParser{
|
||||
reader: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
// SSEEvent represents a parsed SSE event
|
||||
type SSEEvent struct {
|
||||
Event string
|
||||
Data string
|
||||
}
|
||||
|
||||
// ReadEvent reads the next SSE event
|
||||
func (p *SSEParser) ReadEvent() (*SSEEvent, error) {
|
||||
event := &SSEEvent{}
|
||||
|
||||
for {
|
||||
line, err := p.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
|
||||
if line == "" {
|
||||
// Empty line means end of event
|
||||
if event.Data != "" || event.Event != "" {
|
||||
return event, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
event.Event = strings.TrimSpace(line[6:])
|
||||
} else if strings.HasPrefix(line, "data:") {
|
||||
data := strings.TrimSpace(line[5:])
|
||||
if event.Data != "" {
|
||||
event.Data += "\n" + data
|
||||
} else {
|
||||
event.Data = data
|
||||
}
|
||||
}
|
||||
// Ignore comments (lines starting with :) and unknown fields
|
||||
}
|
||||
}
|
||||
|
||||
// ParseChatStreamChunk parses an OpenAI Chat Completions streaming chunk
|
||||
func ParseChatStreamChunk(data string) (*ChatCompletionStreamChunk, error) {
|
||||
if data == "[DONE]" {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
var chunk ChatCompletionStreamChunk
|
||||
err := json.Unmarshal([]byte(data), &chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chunk, nil
|
||||
}
|
||||
|
||||
// ParseMessagesStreamEvent parses an Anthropic Messages streaming event
|
||||
func ParseMessagesStreamEvent(data string) (*AnthropicStreamEvent, error) {
|
||||
var event AnthropicStreamEvent
|
||||
err := json.Unmarshal([]byte(data), &event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// ParseResponsesStreamChunk parses an OpenAI Responses API streaming chunk
|
||||
func ParseResponsesStreamChunk(data string) (*ResponsesStreamEvent, error) {
|
||||
if data == "[DONE]" {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
var event ResponsesStreamEvent
|
||||
err := json.Unmarshal([]byte(data), &event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
Reference in New Issue
Block a user