openai_images_json_keepalive.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. package service
  2. import (
  3. "bufio"
  4. "errors"
  5. "net"
  6. "net/http"
  7. "sync"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. )
  11. const openAIImagesJSONKeepaliveKey = "openai_images_json_keepalive"
  12. // openAIImagesJSONKeepalive keeps non-streaming Images API requests alive while
  13. // an OAuth upstream is producing SSE internally. JSON permits leading
  14. // whitespace, so each heartbeat remains compatible with clients expecting one
  15. // final JSON document.
  16. //
  17. // Once the first heartbeat is sent, the HTTP status is committed as 200. Late
  18. // upstream errors are still returned as an OpenAI-compatible JSON error body,
  19. // matching the status tradeoff used by the compact SSE keepalive path.
  20. type openAIImagesJSONKeepalive struct {
  21. mu sync.Mutex
  22. writer gin.ResponseWriter
  23. started bool
  24. stopped bool
  25. bytes int
  26. stop chan struct{}
  27. }
  28. // StartOpenAIImagesJSONKeepalive starts whitespace heartbeats for a
  29. // non-streaming Images request. A non-positive interval disables the feature.
  30. func StartOpenAIImagesJSONKeepalive(c *gin.Context, interval time.Duration) func() {
  31. if c == nil || c.Writer == nil || interval <= 0 {
  32. return func() {}
  33. }
  34. originalWriter := c.Writer
  35. k := &openAIImagesJSONKeepalive{
  36. writer: originalWriter,
  37. stop: make(chan struct{}),
  38. }
  39. c.Set(openAIImagesJSONKeepaliveKey, k)
  40. wrappedWriter := &openAIImagesJSONKeepaliveWriter{ResponseWriter: originalWriter, k: k}
  41. c.Writer = wrappedWriter
  42. var reqDone <-chan struct{}
  43. if c.Request != nil {
  44. reqDone = c.Request.Context().Done()
  45. }
  46. go func() {
  47. timer := time.NewTimer(interval)
  48. defer timer.Stop()
  49. for {
  50. select {
  51. case <-k.stop:
  52. return
  53. case <-reqDone:
  54. return
  55. case <-timer.C:
  56. }
  57. if !k.beat() {
  58. return
  59. }
  60. timer.Reset(interval)
  61. }
  62. }()
  63. return func() {
  64. k.Stop()
  65. if current, ok := c.Writer.(*openAIImagesJSONKeepaliveWriter); ok && current == wrappedWriter {
  66. c.Writer = originalWriter
  67. }
  68. }
  69. }
  70. func (k *openAIImagesJSONKeepalive) beat() bool {
  71. k.mu.Lock()
  72. defer k.mu.Unlock()
  73. if k.stopped {
  74. return false
  75. }
  76. if !k.started {
  77. header := k.writer.Header()
  78. header.Set("Content-Type", "application/json; charset=utf-8")
  79. header.Set("Cache-Control", "no-cache")
  80. header.Set("X-Accel-Buffering", "no")
  81. k.writer.WriteHeader(http.StatusOK)
  82. k.started = true
  83. }
  84. n, err := k.writer.Write([]byte(" \n"))
  85. k.bytes += n
  86. if err != nil {
  87. k.stopped = true
  88. return false
  89. }
  90. k.writer.Flush()
  91. return true
  92. }
  93. func (k *openAIImagesJSONKeepalive) Stop() {
  94. k.mu.Lock()
  95. k.markStoppedLocked()
  96. k.mu.Unlock()
  97. }
  98. func (k *openAIImagesJSONKeepalive) markStoppedLocked() {
  99. if k.stopped {
  100. return
  101. }
  102. k.stopped = true
  103. close(k.stop)
  104. }
  105. // StopOpenAIImagesJSONKeepaliveCommitted stops heartbeats and reports whether
  106. // they already committed a 200 response.
  107. func StopOpenAIImagesJSONKeepaliveCommitted(c *gin.Context) bool {
  108. k := openAIImagesJSONKeepaliveFromContext(c)
  109. if k == nil {
  110. return false
  111. }
  112. k.mu.Lock()
  113. k.markStoppedLocked()
  114. committed := k.started
  115. k.mu.Unlock()
  116. return committed
  117. }
  118. // OpenAIImagesJSONKeepalivePresent reports whether the response writer belongs
  119. // to an Images JSON request, including fast responses before the first beat.
  120. func OpenAIImagesJSONKeepalivePresent(c *gin.Context) bool {
  121. return openAIImagesJSONKeepaliveFromContext(c) != nil
  122. }
  123. // OpenAIImagesJSONKeepaliveAdjustedWrittenSize excludes heartbeat whitespace
  124. // from response-size checks so account retry and failover remain available.
  125. func OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c *gin.Context) int {
  126. if c == nil || c.Writer == nil {
  127. return -1
  128. }
  129. k := openAIImagesJSONKeepaliveFromContext(c)
  130. if k == nil {
  131. return c.Writer.Size()
  132. }
  133. k.mu.Lock()
  134. defer k.mu.Unlock()
  135. size := k.writer.Size()
  136. if size < 0 {
  137. return size
  138. }
  139. if real := size - k.bytes; real > 0 {
  140. return real
  141. }
  142. return -1
  143. }
  144. func openAIImagesJSONKeepaliveFromContext(c *gin.Context) *openAIImagesJSONKeepalive {
  145. if c == nil {
  146. return nil
  147. }
  148. value, ok := c.Get(openAIImagesJSONKeepaliveKey)
  149. if !ok {
  150. return nil
  151. }
  152. k, _ := value.(*openAIImagesJSONKeepalive)
  153. return k
  154. }
  155. type openAIImagesJSONKeepaliveWriter struct {
  156. gin.ResponseWriter
  157. k *openAIImagesJSONKeepalive
  158. }
  159. func (w *openAIImagesJSONKeepaliveWriter) suspend() {
  160. if w.k != nil {
  161. w.k.Stop()
  162. }
  163. }
  164. func (w *openAIImagesJSONKeepaliveWriter) Header() http.Header {
  165. w.suspend()
  166. if w.ResponseWriter == nil {
  167. return http.Header{}
  168. }
  169. return w.ResponseWriter.Header()
  170. }
  171. func (w *openAIImagesJSONKeepaliveWriter) Write(data []byte) (int, error) {
  172. w.suspend()
  173. if w.ResponseWriter == nil {
  174. return 0, nil
  175. }
  176. return w.ResponseWriter.Write(data)
  177. }
  178. func (w *openAIImagesJSONKeepaliveWriter) WriteString(s string) (int, error) {
  179. w.suspend()
  180. if w.ResponseWriter == nil {
  181. return 0, nil
  182. }
  183. return w.ResponseWriter.WriteString(s)
  184. }
  185. func (w *openAIImagesJSONKeepaliveWriter) WriteHeader(code int) {
  186. w.suspend()
  187. if w.ResponseWriter != nil {
  188. w.ResponseWriter.WriteHeader(code)
  189. }
  190. }
  191. func (w *openAIImagesJSONKeepaliveWriter) WriteHeaderNow() {
  192. w.suspend()
  193. if w.ResponseWriter != nil {
  194. w.ResponseWriter.WriteHeaderNow()
  195. }
  196. }
  197. func (w *openAIImagesJSONKeepaliveWriter) Flush() {
  198. w.suspend()
  199. if w.ResponseWriter != nil {
  200. w.ResponseWriter.Flush()
  201. }
  202. }
  203. func (w *openAIImagesJSONKeepaliveWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
  204. if w.ResponseWriter == nil {
  205. return nil, nil, errors.New("response writer released")
  206. }
  207. return w.ResponseWriter.Hijack()
  208. }
  209. func (w *openAIImagesJSONKeepaliveWriter) CloseNotify() <-chan bool {
  210. if w.ResponseWriter == nil {
  211. ch := make(chan bool)
  212. close(ch)
  213. return ch
  214. }
  215. return w.ResponseWriter.CloseNotify()
  216. }
  217. func (w *openAIImagesJSONKeepaliveWriter) Pusher() http.Pusher {
  218. if w.ResponseWriter == nil {
  219. return nil
  220. }
  221. return w.ResponseWriter.Pusher()
  222. }
  223. func (w *openAIImagesJSONKeepaliveWriter) Status() int {
  224. if w.k == nil || w.ResponseWriter == nil {
  225. return 0
  226. }
  227. w.k.mu.Lock()
  228. defer w.k.mu.Unlock()
  229. return w.ResponseWriter.Status()
  230. }
  231. func (w *openAIImagesJSONKeepaliveWriter) Size() int {
  232. if w.k == nil || w.ResponseWriter == nil {
  233. return 0
  234. }
  235. w.k.mu.Lock()
  236. defer w.k.mu.Unlock()
  237. return w.ResponseWriter.Size()
  238. }
  239. func (w *openAIImagesJSONKeepaliveWriter) Written() bool {
  240. if w.k == nil || w.ResponseWriter == nil {
  241. return false
  242. }
  243. w.k.mu.Lock()
  244. defer w.k.mu.Unlock()
  245. return w.ResponseWriter.Written()
  246. }