@@ -205,6 +205,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
205205 if ! ok {
206206 return
207207 }
208+ // body-signal compact:上游 unary 等待期间向下游发 SSE 注释行心跳,防止
209+ // 反向代理空闲超时掐断长压缩连接(#3887)。首拍延迟一个心跳间隔,快速
210+ // 失败仍走 JSON+状态码链路;未标记客户端流式或间隔为 0 时是 no-op。
211+ stopCompactKeepalive := service .StartOpenAICompactSSEKeepalive (c , h .openAICompactKeepaliveInterval ())
212+ defer stopCompactKeepalive ()
208213
209214 // 校验请求体 JSON 合法性
210215 if ! gjson .ValidBytes (body ) {
@@ -402,7 +407,9 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
402407 // Forward request
403408 service .SetOpsLatencyMs (c , service .OpsRoutingLatencyMsKey , time .Since (routingStart ).Milliseconds ())
404409 forwardStart := time .Now ()
405- writerSizeBeforeForward := c .Writer .Size ()
410+ // 用扣除 compact 心跳字节的口径快照:心跳注释不构成语义响应,
411+ // 不能因心跳字节变化而放弃 failover 换号(#3887)。
412+ writerSizeBeforeForward := service .OpenAICompactKeepaliveAdjustedWrittenSize (c )
406413 result , err := func () (* service.OpenAIForwardResult , error ) {
407414 defer func () {
408415 if accountReleaseFunc != nil {
@@ -436,7 +443,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
436443 } else {
437444 var failoverErr * service.UpstreamFailoverError
438445 if errors .As (err , & failoverErr ) {
439- if c . Writer . Size ( ) != writerSizeBeforeForward {
446+ if service . OpenAICompactKeepaliveAdjustedWrittenSize ( c ) != writerSizeBeforeForward {
440447 h .handleFailoverExhausted (c , failoverErr , true )
441448 return
442449 }
@@ -637,6 +644,13 @@ func (h *OpenAIGatewayHandler) logOpenAIRemoteCompactOutcome(c *gin.Context, sta
637644 if status >= 200 && status < 300 {
638645 outcome = "succeeded"
639646 }
647+ // compact 心跳提交后失败的 wire 状态码固化为 200,真实结局以流内错误
648+ // 标记为准(response.failed 降级路径会 MarkOpsStreamError)。
649+ if outcome == "succeeded" && c != nil {
650+ if _ , hasStreamErr := service .GetOpsStreamError (c ); hasStreamErr {
651+ outcome = "failed"
652+ }
653+ }
640654 latencyMs := time .Since (startedAt ).Milliseconds ()
641655 if latencyMs < 0 {
642656 latencyMs = 0
@@ -1943,6 +1957,11 @@ func (h *OpenAIGatewayHandler) mapUpstreamError(statusCode int) (int, string, st
19431957
19441958// handleStreamingAwareError handles errors that may occur after streaming has started
19451959func (h * OpenAIGatewayHandler ) handleStreamingAwareError (c * gin.Context , status int , errType , message string , streamStarted bool ) {
1960+ // body-signal compact 心跳可能已把响应头提交为 200:先停心跳(建立
1961+ // happens-before,接管 ResponseWriter),并升级为流内错误处理。
1962+ if service .StopOpenAICompactSSEKeepaliveCommitted (c ) {
1963+ streamStarted = true
1964+ }
19461965 if streamStarted {
19471966 // /v1/responses 的严格 SDK(Codex CLI)要求终止事件必须属于
19481967 // response.completed/failed/incomplete/cancelled 集合。
@@ -1975,6 +1994,10 @@ func (h *OpenAIGatewayHandler) ensureForwardErrorResponse(c *gin.Context, stream
19751994 if c == nil || c .Writer == nil {
19761995 return false
19771996 }
1997+ // 先停 compact 心跳再读 Writer 状态,避免与心跳 goroutine 竞争。
1998+ if service .StopOpenAICompactSSEKeepaliveCommitted (c ) {
1999+ streamStarted = true
2000+ }
19782001 if service .IsResponseCommitted (c ) {
19792002 return false
19802003 }
@@ -2010,7 +2033,9 @@ func openAIForwardErrorAlreadyCommunicated(c *gin.Context, writerSizeBeforeForwa
20102033 if err == nil || c == nil || c .Writer == nil {
20112034 return false
20122035 }
2013- if c .Writer .Size () == writerSizeBeforeForward {
2036+ // 与快照同口径:排除 compact 心跳字节,避免"仅心跳写出"被误判为
2037+ // 响应已写出(#3887)。
2038+ if service .OpenAICompactKeepaliveAdjustedWrittenSize (c ) == writerSizeBeforeForward {
20142039 return false
20152040 }
20162041
@@ -2036,6 +2061,14 @@ func openAIForwardErrorAlreadyCommunicated(c *gin.Context, writerSizeBeforeForwa
20362061
20372062// errorResponse returns OpenAI API format error response
20382063func (h * OpenAIGatewayHandler ) errorResponse (c * gin.Context , status int , errType , message string ) {
2064+ // body-signal compact 心跳可能已把响应头提交为 200:JSON 错误体会与已
2065+ // 提交的 SSE 流交错,必须降级为 response.failed 终止事件(#3887)。
2066+ if service .StopOpenAICompactSSEKeepaliveCommitted (c ) {
2067+ service .MarkOpsStreamError (c , errType , message , status )
2068+ if writeResponsesFailedSSE (c , errType , message ) {
2069+ return
2070+ }
2071+ }
20392072 c .JSON (status , gin.H {
20402073 "error" : gin.H {
20412074 "type" : errType ,
@@ -2044,6 +2077,15 @@ func (h *OpenAIGatewayHandler) errorResponse(c *gin.Context, status int, errType
20442077 })
20452078}
20462079
2080+ // openAICompactKeepaliveInterval 复用流式 keepalive 配置作为 compact 下游
2081+ // 心跳间隔;0 表示禁用(与流式路径语义一致)。
2082+ func (h * OpenAIGatewayHandler ) openAICompactKeepaliveInterval () time.Duration {
2083+ if h .cfg == nil || h .cfg .Gateway .StreamKeepaliveInterval <= 0 {
2084+ return 0
2085+ }
2086+ return time .Duration (h .cfg .Gateway .StreamKeepaliveInterval ) * time .Second
2087+ }
2088+
20472089func setOpenAIClientTransportHTTP (c * gin.Context ) {
20482090 service .SetOpenAIClientTransport (c , service .OpenAIClientTransportHTTP )
20492091}
@@ -2306,6 +2348,16 @@ func (h *OpenAIGatewayHandler) rejectIfCyberSessionBlocked(c *gin.Context, apiKe
23062348 if ! h .gatewayService .IsCyberSessionBlocked (c .Request .Context (), key ) {
23072349 return false
23082350 }
2351+ // body-signal compact 心跳可能已把响应头提交为 200(cyber 检查在用户槽位
2352+ // 长等待之后执行):以 response.failed 终止事件回传;未提交时停拍后照常
2353+ // 写 JSON(#3887)。
2354+ if service .StopOpenAICompactSSEKeepaliveCommitted (c ) {
2355+ service .MarkOpsStreamError (c , "permission_error" , cyberSessionBlockedClientMsg , http .StatusForbidden )
2356+ if writeResponsesFailedSSE (c , "permission_error" , cyberSessionBlockedClientMsg ) {
2357+ h .enqueueCyberSessionBlockedOpsEntry (c , apiKey , model , key )
2358+ return true
2359+ }
2360+ }
23092361 switch format {
23102362 case cyberBlockFormatAnthropic :
23112363 c .JSON (http .StatusForbidden , gin.H {"type" : "error" , "error" : gin.H {
0 commit comments