Files
CrossRelay/internal/chatcmpl/stream.go
T
kate522 f8acf31ebc CrossRelay stage 0+1: Go gateway with Anthropic<->Chat Completions relay, Tauri shell, bilingual management UI
- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens,
  management API /api/* (providers CRUD, apply/restore, settings, backups, logs)
- Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming
  state machine keyed by content block index (text, tool_use, parallel calls)
- ~/.claude/settings.json guarded read/write: timestamped backups, managed env
  keys only, one-click restore
- Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout,
  stdin pipe watchdog prevents orphans, kills the child on exit
- React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query
- Tests: Go table-driven + httptest integration (all green), vitest + RTL
2026-10-18 17:19:13 +08:00

52 lines
1.1 KiB
Go

package chatcmpl
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strings"
)
// Stream reads Server-Sent Events carrying Chat Completions chunks.
type Stream struct {
scanner *bufio.Scanner
}
// NewStream wraps a reader of upstream SSE data.
func NewStream(r io.Reader) *Stream {
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
return &Stream{scanner: sc}
}
// Next returns the next chunk. It reports io.EOF once the upstream
// stream is finished (either by "[DONE]" or by closing).
func (s *Stream) Next() (*StreamChunk, error) {
for s.scanner.Scan() {
line := strings.TrimRight(s.scanner.Text(), "\r")
if line == "" || strings.HasPrefix(line, ":") {
continue
}
if !strings.HasPrefix(line, "data:") {
continue
}
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if data == "" {
continue
}
if data == "[DONE]" {
return nil, io.EOF
}
var chunk StreamChunk
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
return nil, fmt.Errorf("decode upstream chunk: %w", err)
}
return &chunk, nil
}
if err := s.scanner.Err(); err != nil {
return nil, err
}
return nil, io.EOF
}