Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
306 lines
8.7 KiB
Go
306 lines
8.7 KiB
Go
package service
|
||
|
||
import (
|
||
"bufio"
|
||
"errors"
|
||
"net"
|
||
"net/http"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// openAICompactSSEKeepaliveKey 存放 body-signal compact 请求的下游 SSE 心跳器。
|
||
const openAICompactSSEKeepaliveKey = "openai_compact_sse_keepalive"
|
||
|
||
// openAICompactSSEKeepalive 在 compact 上游 unary 等待期间向下游写 SSE 注释行
|
||
// 心跳。上游 /responses/compact 在模型处理期间不发送任何字节(大上下文可长达
|
||
// 数分钟),下游若经过反向代理(Nginx/Cloudflare Tunnel 等),零字节静默会触发
|
||
// 代理的空闲/读超时并掐断连接,Codex 只会盲目重连并重复消耗上游 compact
|
||
// 配额(#3887)。SSE 注释行在 eventsource 解析层被直接忽略,不会进入客户端
|
||
// 事件流。
|
||
//
|
||
// 首拍延迟一个 interval:绝大多数硬错误(鉴权/参数/限流)在此之前返回,仍走
|
||
// 原 JSON+状态码链路(Codex 按 HTTP 状态码重试);首拍之后状态码固化为 200,
|
||
// 后续错误由写回方降级为 response.failed 流内终止事件。
|
||
type openAICompactSSEKeepalive struct {
|
||
mu sync.Mutex
|
||
writer gin.ResponseWriter
|
||
started bool
|
||
stopped bool
|
||
// bytes 是心跳已写出的注释字节数。心跳不构成语义响应,handler 的
|
||
// "Forward 期间是否已写响应"判定(failover 放弃换号的依据)必须扣除
|
||
// 这部分字节,见 OpenAICompactKeepaliveAdjustedWrittenSize。
|
||
bytes int
|
||
stop chan struct{}
|
||
}
|
||
|
||
// StartOpenAICompactSSEKeepalive 为已标记 body-signal 客户端流式的 compact
|
||
// 请求启动下游心跳,返回幂等的停止函数。interval<=0 或请求未标记时为 no-op。
|
||
//
|
||
// 同时把 c.Writer 替换为 openAICompactKeepaliveWriter:请求 goroutine 的任何
|
||
// 响应构造都会先在心跳互斥锁下停拍,未被显式拦截的写回路径(如 Forward
|
||
// 内部的本地拒绝)也不会与心跳 goroutine 产生数据竞争或字节交错。
|
||
func StartOpenAICompactSSEKeepalive(c *gin.Context, interval time.Duration) func() {
|
||
if c == nil || c.Writer == nil || interval <= 0 || !openAICompactClientWantsStream(c) {
|
||
return func() {}
|
||
}
|
||
originalWriter := c.Writer
|
||
k := &openAICompactSSEKeepalive{
|
||
writer: originalWriter,
|
||
stop: make(chan struct{}),
|
||
}
|
||
c.Set(openAICompactSSEKeepaliveKey, k)
|
||
wrappedWriter := &openAICompactKeepaliveWriter{ResponseWriter: originalWriter, k: k}
|
||
c.Writer = wrappedWriter
|
||
|
||
var reqDone <-chan struct{}
|
||
if c.Request != nil {
|
||
reqDone = c.Request.Context().Done()
|
||
}
|
||
go func() {
|
||
timer := time.NewTimer(interval)
|
||
defer timer.Stop()
|
||
for {
|
||
select {
|
||
case <-k.stop:
|
||
return
|
||
case <-reqDone:
|
||
return
|
||
case <-timer.C:
|
||
}
|
||
if !k.beat() {
|
||
return
|
||
}
|
||
timer.Reset(interval)
|
||
}
|
||
}()
|
||
return func() {
|
||
k.Stop()
|
||
// Do not leave a pooled middleware writer reachable through the compact
|
||
// wrapper after the request finishes.
|
||
if current, ok := c.Writer.(*openAICompactKeepaliveWriter); ok && current == wrappedWriter {
|
||
c.Writer = originalWriter
|
||
}
|
||
}
|
||
}
|
||
|
||
// beat 在锁内提交(首次)响应头并写出一条 SSE 注释行;返回 false 表示心跳已
|
||
// 停止或下游写入失败,goroutine 应退出。
|
||
func (k *openAICompactSSEKeepalive) beat() bool {
|
||
k.mu.Lock()
|
||
defer k.mu.Unlock()
|
||
if k.stopped {
|
||
return false
|
||
}
|
||
if !k.started {
|
||
header := k.writer.Header()
|
||
header.Set("Content-Type", "text/event-stream")
|
||
header.Set("Cache-Control", "no-cache")
|
||
header.Set("Connection", "keep-alive")
|
||
header.Set("X-Accel-Buffering", "no")
|
||
k.writer.WriteHeader(http.StatusOK)
|
||
k.started = true
|
||
}
|
||
n, err := k.writer.Write([]byte(": keepalive\n\n"))
|
||
k.bytes += n
|
||
if err != nil {
|
||
k.stopped = true
|
||
return false
|
||
}
|
||
k.writer.Flush()
|
||
return true
|
||
}
|
||
|
||
// Stop 停止心跳;幂等,可与写回路径并发调用。
|
||
func (k *openAICompactSSEKeepalive) Stop() {
|
||
k.mu.Lock()
|
||
k.markStoppedLocked()
|
||
k.mu.Unlock()
|
||
}
|
||
|
||
func (k *openAICompactSSEKeepalive) markStoppedLocked() {
|
||
if k.stopped {
|
||
return
|
||
}
|
||
k.stopped = true
|
||
close(k.stop)
|
||
}
|
||
|
||
// StopOpenAICompactSSEKeepaliveCommitted 停止当前请求的 compact 心跳(若有)
|
||
// 并报告响应头是否已被心跳提交为 200。写回方以此决定继续走原 JSON/状态码
|
||
// 链路,还是降级为流内终止事件。调用后不会再有心跳字节写出,且经由互斥锁
|
||
// 与心跳 goroutine 建立 happens-before,调用方可安全接管 ResponseWriter。
|
||
func StopOpenAICompactSSEKeepaliveCommitted(c *gin.Context) bool {
|
||
if c == nil {
|
||
return false
|
||
}
|
||
value, ok := c.Get(openAICompactSSEKeepaliveKey)
|
||
if !ok {
|
||
return false
|
||
}
|
||
k, ok := value.(*openAICompactSSEKeepalive)
|
||
if !ok || k == nil {
|
||
return false
|
||
}
|
||
k.mu.Lock()
|
||
k.markStoppedLocked()
|
||
committed := k.started
|
||
k.mu.Unlock()
|
||
return committed
|
||
}
|
||
|
||
// OpenAICompactKeepaliveAdjustedWrittenSize 返回排除 compact 心跳注释字节后
|
||
// 的响应已写字节数;无心跳的请求等价于 c.Writer.Size()。心跳字节不构成语义
|
||
// 响应——handler 以"Forward 前后 Size 是否变化"判定是否已向客户端写出响应
|
||
// (变化则放弃 failover 换号),该判定不得被心跳污染,否则 compact 请求
|
||
// 一旦在上游等待期间发过心跳,上游 429/5xx 就不再换号(#3887 加固审计)。
|
||
// 仅心跳字节时归一化为 -1(gin 的"未写出"哨兵值),与提交前的快照可比。
|
||
func OpenAICompactKeepaliveAdjustedWrittenSize(c *gin.Context) int {
|
||
if c == nil || c.Writer == nil {
|
||
return -1
|
||
}
|
||
streamKeepaliveBytes := 0
|
||
if value, ok := c.Get(openAIStreamKeepaliveBytesKey); ok {
|
||
streamKeepaliveBytes, _ = value.(int)
|
||
}
|
||
size := c.Writer.Size()
|
||
compactKeepaliveBytes := 0
|
||
if value, ok := c.Get(openAICompactSSEKeepaliveKey); ok {
|
||
if k, valid := value.(*openAICompactSSEKeepalive); valid && k != nil {
|
||
k.mu.Lock()
|
||
size = k.writer.Size()
|
||
compactKeepaliveBytes = k.bytes
|
||
k.mu.Unlock()
|
||
}
|
||
}
|
||
if size < 0 {
|
||
return size
|
||
}
|
||
keepaliveBytes := compactKeepaliveBytes + streamKeepaliveBytes
|
||
if keepaliveBytes <= 0 {
|
||
return size
|
||
}
|
||
if real := size - keepaliveBytes; real > 0 {
|
||
return real
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// openAICompactKeepaliveWriter 包装 gin.ResponseWriter:写侧方法先停拍心跳
|
||
// (互斥锁下建立 happens-before),读侧方法仅加锁不停拍——热路径的状态读取
|
||
// (如 Forward 前的 Size 快照)不能误杀心跳。心跳 goroutine 直接写内层
|
||
// writer(k.writer),不经过本包装器,不会递归。
|
||
type openAICompactKeepaliveWriter struct {
|
||
gin.ResponseWriter
|
||
k *openAICompactSSEKeepalive
|
||
}
|
||
|
||
// suspend 停拍心跳;幂等。任何响应构造(含 Header 访问——写响应必先操作
|
||
// 响应头)都视为请求侧接管 ResponseWriter。
|
||
func (w *openAICompactKeepaliveWriter) suspend() {
|
||
if w.k == nil {
|
||
return
|
||
}
|
||
w.k.Stop()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Header() http.Header {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return http.Header{}
|
||
}
|
||
return w.ResponseWriter.Header()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Write(data []byte) (int, error) {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return 0, nil
|
||
}
|
||
return w.ResponseWriter.Write(data)
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) WriteString(s string) (int, error) {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return 0, nil
|
||
}
|
||
return w.ResponseWriter.WriteString(s)
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) WriteHeader(code int) {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return
|
||
}
|
||
w.ResponseWriter.WriteHeader(code)
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) WriteHeaderNow() {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return
|
||
}
|
||
w.ResponseWriter.WriteHeaderNow()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Flush() {
|
||
w.suspend()
|
||
if w.ResponseWriter == nil {
|
||
return
|
||
}
|
||
w.ResponseWriter.Flush()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||
if w.ResponseWriter == nil {
|
||
return nil, nil, errors.New("response writer released")
|
||
}
|
||
return w.ResponseWriter.Hijack()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) CloseNotify() <-chan bool {
|
||
if w.ResponseWriter == nil {
|
||
ch := make(chan bool)
|
||
close(ch)
|
||
return ch
|
||
}
|
||
return w.ResponseWriter.CloseNotify()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Pusher() http.Pusher {
|
||
if w.ResponseWriter == nil {
|
||
return nil
|
||
}
|
||
return w.ResponseWriter.Pusher()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Status() int {
|
||
if w.k == nil || w.ResponseWriter == nil {
|
||
return 0
|
||
}
|
||
w.k.mu.Lock()
|
||
defer w.k.mu.Unlock()
|
||
return w.ResponseWriter.Status()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Size() int {
|
||
if w.k == nil || w.ResponseWriter == nil {
|
||
return 0
|
||
}
|
||
w.k.mu.Lock()
|
||
defer w.k.mu.Unlock()
|
||
return w.ResponseWriter.Size()
|
||
}
|
||
|
||
func (w *openAICompactKeepaliveWriter) Written() bool {
|
||
if w.k == nil || w.ResponseWriter == nil {
|
||
return false
|
||
}
|
||
w.k.mu.Lock()
|
||
defer w.k.mu.Unlock()
|
||
return w.ResponseWriter.Written()
|
||
}
|