Files
goaiapi/src/go/sse.go
XiuChengWu 1216ad0145 重构项目结构并纳入版本管理
- 源码移至 src/go/,编译产物输出到 build/(不入库)
- 新增 .gitignore:排除 data/、build/、*.exe、*.log、说明.md
- 新增 build.ps1:从 src/go 编译到 build/relay.exe
- 移除 说明.md 的版本跟踪(含上游密钥,仅保留在本地)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:03:45 +08:00

186 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"encoding/json"
"io"
"strings"
)
// streamRecorder 包在 resp.Body 外层:数据被客户端读取时同步流过,
// 一边原样转发(客户端实时收到每个 chunk一边把全量原文拼回 + 解析 SSE usage。
// 读到 EOF流结束时回调 onDone由它落库。
//
// 这是流式代理的标准做法:用一个 io.ReadCloser 包装上游 body
// 不做 io.ReadAll避免缓冲整条流导致客户端要等所有 chunk 到齐才收到第一个字。
type streamRecorder struct {
src io.ReadCloser
raw bytes.Buffer // 全量原文(拼回所有 chunk
parser *sseParser
onDone func(raw []byte, in, out int, model string)
done bool
}
func newStreamRecorder(src io.ReadCloser, format string, onDone func(raw []byte, in, out int, model string)) *streamRecorder {
return &streamRecorder{
src: src,
parser: newSSEParser(format),
onDone: onDone,
}
}
func (s *streamRecorder) Read(p []byte) (int, error) {
n, err := s.src.Read(p)
if n > 0 {
// 原样留一份,并喂给 SSE 解析器累积 usage
s.raw.Write(p[:n])
s.parser.feed(p[:n])
}
if err == io.EOF && !s.done {
s.done = true
s.parser.flush()
in, out, model := s.parser.result()
s.onDone(s.raw.Bytes(), in, out, model)
}
return n, err
}
func (s *streamRecorder) Close() error {
// 客户端提前断开等情况:若还没触发 onDone这里兜底落库一次。
if !s.done {
s.done = true
s.parser.flush()
in, out, model := s.parser.result()
s.onDone(s.raw.Bytes(), in, out, model)
}
return s.src.Close()
}
// sseParser 按行累积 SSE 数据,跨 chunk 解析 OpenAI / Claude 两家流式 usage。
//
// OpenAI 流式usage 出现在末尾带 stream_options.include_usage 的 chunk 里,
// 形如 data: {...,"usage":{"prompt_tokens":..,"completion_tokens":..}}
// 没开 include_usage 则 usage 落空(如实记 0
// Claude 流式message_start 事件给 input_tokensusage.input_tokens
// message_delta 事件累加 output_tokensusage.output_tokens需跨事件攒。
type sseParser struct {
format string
buf bytes.Buffer // 未成行的残留字节
in int
out int
model string
}
func newSSEParser(format string) *sseParser {
return &sseParser{format: format}
}
// feed 接收任意切片的字节,按行切出完整的 SSE 行交给 parseLine。
func (p *sseParser) feed(b []byte) {
p.buf.Write(b)
data := p.buf.Bytes()
for {
i := bytes.IndexByte(data, '\n')
if i < 0 {
break
}
line := data[:i]
p.parseLine(line)
data = data[i+1:]
}
// 保留未成行的尾部
rest := make([]byte, len(data))
copy(rest, data)
p.buf.Reset()
p.buf.Write(rest)
}
// flush 处理流结束时缓冲里残留的最后一行(可能没有结尾换行)。
func (p *sseParser) flush() {
if p.buf.Len() > 0 {
p.parseLine(p.buf.Bytes())
p.buf.Reset()
}
}
// parseLine 解析单行 SSE。只关心 "data:" 行里的 JSON。
func (p *sseParser) parseLine(line []byte) {
s := strings.TrimRight(string(line), "\r")
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "data:") {
return
}
payload := strings.TrimSpace(strings.TrimPrefix(s, "data:"))
if payload == "" || payload == "[DONE]" {
return
}
var m map[string]json.RawMessage
if err := json.Unmarshal([]byte(payload), &m); err != nil {
return // 非 JSON 行,跳过
}
// model 字段(两家都有,取到就记)
if raw, ok := m["model"]; ok {
var mod string
if json.Unmarshal(raw, &mod) == nil && mod != "" {
p.model = mod
}
}
// usage 可能直接在顶层OpenAI 末尾 chunk
// 也可能在 message.usage 里Claude message_start
if raw, ok := m["usage"]; ok {
p.applyUsage(raw)
}
if raw, ok := m["message"]; ok {
var msg map[string]json.RawMessage
if json.Unmarshal(raw, &msg) == nil {
if u, ok := msg["usage"]; ok {
p.applyUsage(u)
}
if mraw, ok := msg["model"]; ok {
var mod string
if json.Unmarshal(mraw, &mod) == nil && mod != "" {
p.model = mod
}
}
}
}
}
// applyUsage 从一段 usage JSON 里抽 token 数,兼容两家命名。
// Claude 的 output_tokens 在 message_delta 里是累计值(取最大即可),
// input_tokens 在 message_start 给定后不再变OpenAI 末尾一次性给全量。
func (p *sseParser) applyUsage(raw json.RawMessage) {
var u struct {
// OpenAI
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
// Claude
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
if json.Unmarshal(raw, &u) != nil {
return
}
// 取较大值,避免后到的事件覆盖成 0Claude 各事件字段不全)。
if u.PromptTokens > p.in {
p.in = u.PromptTokens
}
if u.InputTokens > p.in {
p.in = u.InputTokens
}
if u.CompletionTokens > p.out {
p.out = u.CompletionTokens
}
if u.OutputTokens > p.out {
p.out = u.OutputTokens
}
}
func (p *sseParser) result() (in, out int, model string) {
return p.in, p.out, p.model
}