| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- package service
- import (
- "bufio"
- "errors"
- "net"
- "net/http"
- "sync"
- "time"
- "github.com/gin-gonic/gin"
- )
- const openAIImagesJSONKeepaliveKey = "openai_images_json_keepalive"
- // openAIImagesJSONKeepalive keeps non-streaming Images API requests alive while
- // an OAuth upstream is producing SSE internally. JSON permits leading
- // whitespace, so each heartbeat remains compatible with clients expecting one
- // final JSON document.
- //
- // Once the first heartbeat is sent, the HTTP status is committed as 200. Late
- // upstream errors are still returned as an OpenAI-compatible JSON error body,
- // matching the status tradeoff used by the compact SSE keepalive path.
- type openAIImagesJSONKeepalive struct {
- mu sync.Mutex
- writer gin.ResponseWriter
- started bool
- stopped bool
- bytes int
- stop chan struct{}
- }
- // StartOpenAIImagesJSONKeepalive starts whitespace heartbeats for a
- // non-streaming Images request. A non-positive interval disables the feature.
- func StartOpenAIImagesJSONKeepalive(c *gin.Context, interval time.Duration) func() {
- if c == nil || c.Writer == nil || interval <= 0 {
- return func() {}
- }
- originalWriter := c.Writer
- k := &openAIImagesJSONKeepalive{
- writer: originalWriter,
- stop: make(chan struct{}),
- }
- c.Set(openAIImagesJSONKeepaliveKey, k)
- wrappedWriter := &openAIImagesJSONKeepaliveWriter{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()
- if current, ok := c.Writer.(*openAIImagesJSONKeepaliveWriter); ok && current == wrappedWriter {
- c.Writer = originalWriter
- }
- }
- }
- func (k *openAIImagesJSONKeepalive) 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", "application/json; charset=utf-8")
- header.Set("Cache-Control", "no-cache")
- header.Set("X-Accel-Buffering", "no")
- k.writer.WriteHeader(http.StatusOK)
- k.started = true
- }
- n, err := k.writer.Write([]byte(" \n"))
- k.bytes += n
- if err != nil {
- k.stopped = true
- return false
- }
- k.writer.Flush()
- return true
- }
- func (k *openAIImagesJSONKeepalive) Stop() {
- k.mu.Lock()
- k.markStoppedLocked()
- k.mu.Unlock()
- }
- func (k *openAIImagesJSONKeepalive) markStoppedLocked() {
- if k.stopped {
- return
- }
- k.stopped = true
- close(k.stop)
- }
- // StopOpenAIImagesJSONKeepaliveCommitted stops heartbeats and reports whether
- // they already committed a 200 response.
- func StopOpenAIImagesJSONKeepaliveCommitted(c *gin.Context) bool {
- k := openAIImagesJSONKeepaliveFromContext(c)
- if k == nil {
- return false
- }
- k.mu.Lock()
- k.markStoppedLocked()
- committed := k.started
- k.mu.Unlock()
- return committed
- }
- // OpenAIImagesJSONKeepalivePresent reports whether the response writer belongs
- // to an Images JSON request, including fast responses before the first beat.
- func OpenAIImagesJSONKeepalivePresent(c *gin.Context) bool {
- return openAIImagesJSONKeepaliveFromContext(c) != nil
- }
- // OpenAIImagesJSONKeepaliveAdjustedWrittenSize excludes heartbeat whitespace
- // from response-size checks so account retry and failover remain available.
- func OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c *gin.Context) int {
- if c == nil || c.Writer == nil {
- return -1
- }
- k := openAIImagesJSONKeepaliveFromContext(c)
- if k == nil {
- return c.Writer.Size()
- }
- k.mu.Lock()
- defer k.mu.Unlock()
- size := k.writer.Size()
- if size < 0 {
- return size
- }
- if real := size - k.bytes; real > 0 {
- return real
- }
- return -1
- }
- func openAIImagesJSONKeepaliveFromContext(c *gin.Context) *openAIImagesJSONKeepalive {
- if c == nil {
- return nil
- }
- value, ok := c.Get(openAIImagesJSONKeepaliveKey)
- if !ok {
- return nil
- }
- k, _ := value.(*openAIImagesJSONKeepalive)
- return k
- }
- type openAIImagesJSONKeepaliveWriter struct {
- gin.ResponseWriter
- k *openAIImagesJSONKeepalive
- }
- func (w *openAIImagesJSONKeepaliveWriter) suspend() {
- if w.k != nil {
- w.k.Stop()
- }
- }
- func (w *openAIImagesJSONKeepaliveWriter) Header() http.Header {
- w.suspend()
- if w.ResponseWriter == nil {
- return http.Header{}
- }
- return w.ResponseWriter.Header()
- }
- func (w *openAIImagesJSONKeepaliveWriter) Write(data []byte) (int, error) {
- w.suspend()
- if w.ResponseWriter == nil {
- return 0, nil
- }
- return w.ResponseWriter.Write(data)
- }
- func (w *openAIImagesJSONKeepaliveWriter) WriteString(s string) (int, error) {
- w.suspend()
- if w.ResponseWriter == nil {
- return 0, nil
- }
- return w.ResponseWriter.WriteString(s)
- }
- func (w *openAIImagesJSONKeepaliveWriter) WriteHeader(code int) {
- w.suspend()
- if w.ResponseWriter != nil {
- w.ResponseWriter.WriteHeader(code)
- }
- }
- func (w *openAIImagesJSONKeepaliveWriter) WriteHeaderNow() {
- w.suspend()
- if w.ResponseWriter != nil {
- w.ResponseWriter.WriteHeaderNow()
- }
- }
- func (w *openAIImagesJSONKeepaliveWriter) Flush() {
- w.suspend()
- if w.ResponseWriter != nil {
- w.ResponseWriter.Flush()
- }
- }
- func (w *openAIImagesJSONKeepaliveWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
- if w.ResponseWriter == nil {
- return nil, nil, errors.New("response writer released")
- }
- return w.ResponseWriter.Hijack()
- }
- func (w *openAIImagesJSONKeepaliveWriter) CloseNotify() <-chan bool {
- if w.ResponseWriter == nil {
- ch := make(chan bool)
- close(ch)
- return ch
- }
- return w.ResponseWriter.CloseNotify()
- }
- func (w *openAIImagesJSONKeepaliveWriter) Pusher() http.Pusher {
- if w.ResponseWriter == nil {
- return nil
- }
- return w.ResponseWriter.Pusher()
- }
- func (w *openAIImagesJSONKeepaliveWriter) 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 *openAIImagesJSONKeepaliveWriter) 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 *openAIImagesJSONKeepaliveWriter) Written() bool {
- if w.k == nil || w.ResponseWriter == nil {
- return false
- }
- w.k.mu.Lock()
- defer w.k.mu.Unlock()
- return w.ResponseWriter.Written()
- }
|