openai_compact_stream_bridge.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. "github.com/google/uuid"
  11. "github.com/tidwall/gjson"
  12. "github.com/tidwall/sjson"
  13. )
  14. // openAICompactClientStreamKey 标记 body-signal compact 请求(Codex remote
  15. // compact v2,见 #3777)的原始 body 携带 stream:true。白名单归一化会删除
  16. // stream 字段并让上游走 unary /responses/compact(JSON),但客户端仍按
  17. // Responses SSE 协议消费响应:它必须收到 response.output_item.done(其中恰好
  18. // 一个 type=compaction 的 item)和 response.completed,否则报
  19. // "stream closed before response.completed" 并无限重连(#3875)。
  20. const openAICompactClientStreamKey = "openai_compact_client_stream"
  21. // MarkOpenAICompactClientStream 由 handler 在 body-signal 提升时调用,记录
  22. // 客户端的原始 stream 意图,供响应写回阶段决定是否合成 SSE。
  23. func MarkOpenAICompactClientStream(c *gin.Context) {
  24. if c == nil {
  25. return
  26. }
  27. c.Set(openAICompactClientStreamKey, true)
  28. }
  29. func OpenAICompactClientStreamKeyForTest() string {
  30. return openAICompactClientStreamKey
  31. }
  32. func openAICompactClientWantsStream(c *gin.Context) bool {
  33. if c == nil {
  34. return false
  35. }
  36. value, ok := c.Get(openAICompactClientStreamKey)
  37. if !ok {
  38. return false
  39. }
  40. wants, _ := value.(bool)
  41. return wants
  42. }
  43. // writeOpenAICompactSSEBridge 将 unary compact 的最终 JSON 响应按 Codex remote
  44. // compact v2 的消费协议合成为最小 Responses SSE 流写回客户端。仅当请求被标记
  45. // 为 body-signal 客户端流式、状态码为 2xx 且 body 是合法 JSON 对象时生效;
  46. // 返回 false 表示未写出任何内容,调用方应按原路径写回。
  47. //
  48. // 若下游心跳已把响应头提交为 200(见 openAICompactSSEKeepalive),则本函数
  49. // 必须接管一切写回:非 2xx 或不可合成的响应降级为 response.failed 终止事件,
  50. // 不能再返回 false(否则调用方的 JSON 写回会与已提交的 SSE 流交错)。
  51. func writeOpenAICompactSSEBridge(c *gin.Context, statusCode int, finalResponse []byte) bool {
  52. if c == nil || !openAICompactClientWantsStream(c) {
  53. return false
  54. }
  55. // 先停心跳再写回,避免注释行与最终事件交错;停止后经互斥锁与心跳
  56. // goroutine 建立 happens-before,可安全接管 ResponseWriter。
  57. committed := StopOpenAICompactSSEKeepaliveCommitted(c)
  58. if statusCode < 200 || statusCode >= 300 {
  59. if committed {
  60. writeOpenAICompactSSEFailure(c, statusCode, finalResponse)
  61. return true
  62. }
  63. return false
  64. }
  65. payload, ok := buildOpenAICompactSSEPayload(finalResponse)
  66. if !ok {
  67. if committed {
  68. writeOpenAICompactSSEFailure(c, http.StatusBadGateway, finalResponse)
  69. return true
  70. }
  71. return false
  72. }
  73. if !committed {
  74. header := c.Writer.Header()
  75. header.Set("Content-Type", "text/event-stream")
  76. header.Set("Cache-Control", "no-cache")
  77. header.Set("Connection", "keep-alive")
  78. header.Set("X-Accel-Buffering", "no")
  79. c.Writer.WriteHeader(statusCode)
  80. }
  81. _, _ = c.Writer.Write(payload)
  82. c.Writer.Flush()
  83. return true
  84. }
  85. // writeOpenAICompactSSEFailure 从上游错误 body 提取错误消息后,以
  86. // response.failed 终止事件回传。仅用于心跳已提交 200、无法再按 HTTP 状态码
  87. // 回传错误的场景。
  88. func writeOpenAICompactSSEFailure(c *gin.Context, statusCode int, errorBody []byte) {
  89. message := ""
  90. if len(errorBody) > 0 {
  91. message = sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(errorBody)))
  92. }
  93. if message == "" {
  94. message = "Upstream compact request failed with HTTP " + strconv.Itoa(statusCode)
  95. }
  96. writeOpenAICompactSSEFailureMessage(c, statusCode, "upstream_error", message)
  97. }
  98. // writeOpenAICompactSSEFailureMessage 写出 response.failed 终止事件。Codex 对
  99. // 流式 Responses 请求把 response.failed 作为合法终止事件处理(普通 error 帧
  100. // 不被识别,会退化为 "stream closed before response.completed" 盲重连)。
  101. // 同时标记流内错误,保证挂在 200 流上的失败仍进入 ops 错误看板。
  102. func writeOpenAICompactSSEFailureMessage(c *gin.Context, statusCode int, errType, message string) {
  103. if c == nil {
  104. return
  105. }
  106. MarkOpsStreamError(c, errType, message, statusCode)
  107. payload, err := json.Marshal(map[string]any{
  108. "type": "response.failed",
  109. "response": map[string]any{
  110. "id": "resp_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
  111. "object": "response",
  112. // 严格客户端把 created_at 当必填字段,缺失会反序列化失败,
  113. // 终止事件就白发了(退化成盲重连)。与 writeResponsesFailedSSE 对齐。
  114. "created_at": time.Now().Unix(),
  115. "status": "failed",
  116. "output": []any{},
  117. "error": map[string]any{
  118. "code": errType,
  119. "message": message,
  120. },
  121. },
  122. })
  123. if err != nil {
  124. return
  125. }
  126. _, _ = c.Writer.Write([]byte("event: response.failed\ndata: "))
  127. _, _ = c.Writer.Write(payload)
  128. _, _ = c.Writer.Write([]byte("\n\n"))
  129. c.Writer.Flush()
  130. }
  131. // buildOpenAICompactSSEPayload 把 compact 的 Response JSON 转成 SSE 事件序列:
  132. // 每个 output[] item 一条 response.output_item.done,最后一条 response.completed
  133. // 携带完整 response 对象。Codex 的 SSE 解析只从 output_item.done 收集 item,
  134. // 并要求 response.completed 的 response.id 必填、usage(若存在)必须携带
  135. // input_tokens/output_tokens/total_tokens 整数字段,否则整条 completed 事件
  136. // 解析失败,故此处做兜底修补。
  137. func buildOpenAICompactSSEPayload(finalResponse []byte) ([]byte, bool) {
  138. if len(finalResponse) == 0 || !gjson.ValidBytes(finalResponse) {
  139. return nil, false
  140. }
  141. if !gjson.ParseBytes(finalResponse).IsObject() {
  142. return nil, false
  143. }
  144. // SSE 的 data 行不允许出现裸换行:上游 JSON 可能是 pretty-printed 形态,
  145. // 嵌入前必须压缩为单行。
  146. var compacted bytes.Buffer
  147. if err := json.Compact(&compacted, finalResponse); err != nil {
  148. return nil, false
  149. }
  150. response := compacted.Bytes()
  151. root := gjson.ParseBytes(response)
  152. if strings.TrimSpace(root.Get("id").String()) == "" {
  153. next, err := sjson.SetBytes(response, "id", "resp_"+strings.ReplaceAll(uuid.NewString(), "-", ""))
  154. if err != nil {
  155. return nil, false
  156. }
  157. response = next
  158. }
  159. if usage := gjson.GetBytes(response, "usage"); usage.Exists() && !openAICompactUsageParsableByCodex(usage) {
  160. next, err := sjson.DeleteBytes(response, "usage")
  161. if err != nil {
  162. return nil, false
  163. }
  164. response = next
  165. }
  166. var buf bytes.Buffer
  167. outputIndex := 0
  168. appendEvent := func(eventType string, data []byte) {
  169. _, _ = buf.WriteString("event: ")
  170. _, _ = buf.WriteString(eventType)
  171. _, _ = buf.WriteString("\ndata: ")
  172. _, _ = buf.Write(data)
  173. _, _ = buf.WriteString("\n\n")
  174. }
  175. for _, item := range gjson.GetBytes(response, "output").Array() {
  176. if !item.IsObject() {
  177. continue
  178. }
  179. event, err := sjson.SetBytes([]byte(`{"type":"response.output_item.done"}`), "output_index", outputIndex)
  180. if err != nil {
  181. return nil, false
  182. }
  183. event, err = sjson.SetRawBytes(event, "item", []byte(item.Raw))
  184. if err != nil {
  185. return nil, false
  186. }
  187. appendEvent("response.output_item.done", event)
  188. outputIndex++
  189. }
  190. completed, err := sjson.SetRawBytes([]byte(`{"type":"response.completed"}`), "response", response)
  191. if err != nil {
  192. return nil, false
  193. }
  194. appendEvent("response.completed", completed)
  195. return buf.Bytes(), true
  196. }
  197. func openAICompactUsageParsableByCodex(usage gjson.Result) bool {
  198. if !usage.IsObject() {
  199. return false
  200. }
  201. for _, field := range []string{"input_tokens", "output_tokens", "total_tokens"} {
  202. if usage.Get(field).Type != gjson.Number {
  203. return false
  204. }
  205. }
  206. return true
  207. }