@@ -443,9 +443,21 @@ func NewOpenAIGatewayService(
443443 openAITokenProvider .SetAccountRuntimeBlocker (svc )
444444 }
445445 svc .logOpenAIWSModeBootstrap ()
446+ svc .logOpenAICompactNonstreamKeepaliveBootstrap ()
446447 return svc
447448}
448449
450+ func (s * OpenAIGatewayService ) logOpenAICompactNonstreamKeepaliveBootstrap () {
451+ interval := s .compactNonstreamKeepaliveInterval ()
452+ if interval <= 0 {
453+ return
454+ }
455+ logger .L ().With (
456+ zap .String ("component" , "service.openai_gateway" ),
457+ zap .Int ("interval_seconds" , int (interval .Seconds ())),
458+ ).Info ("OpenAI compact non-stream keepalive enabled" )
459+ }
460+
449461// ResolveChannelMapping 解析渠道级模型映射(代理到 ChannelService)
450462func (s * OpenAIGatewayService ) ResolveChannelMapping (ctx context.Context , groupID int64 , model string ) ChannelMappingResult {
451463 if s .channelService == nil {
@@ -2982,25 +2994,42 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
29822994 }
29832995
29842996 // Send request
2997+ stopCompactKeepalive := func () {}
2998+ if ! reqStream {
2999+ stopCompactKeepalive = s .startCompactNonstreamKeepalive (ctx , c )
3000+ }
3001+
29853002 upstreamStart := time .Now ()
29863003 resp , err := s .httpUpstream .Do (upstreamReq , proxyURL , account .ID , account .Concurrency )
29873004 SetOpsLatencyMs (c , OpsUpstreamLatencyMsKey , time .Since (upstreamStart ).Milliseconds ())
29883005 if err != nil {
3006+ stopCompactKeepalive ()
29893007 // Transport-level failure (proxy/DNS/TCP/TLS — no HTTP response). Convert to
29903008 // a failover so the handler switches to a healthy account, and temporarily
29913009 // unschedule the account on durable faults (e.g. rejected proxy credentials).
2992- return nil , s .handleOpenAIUpstreamTransportError (ctx , c , account , err , false )
3010+ transportErr := s .handleOpenAIUpstreamTransportError (ctx , c , account , err , false )
3011+ if openAICompactKeepaliveCommitted (c ) {
3012+ logOpenAICompactKeepaliveCommitted (ctx , c , account , nil )
3013+ writeOpenAICommittedTransportError (c )
3014+ return nil , fmt .Errorf ("upstream request failed after compact keepalive: %s" , sanitizeUpstreamErrorMessage (err .Error ()))
3015+ }
3016+ return nil , transportErr
29933017 }
29943018
29953019 // Handle error response
29963020 if resp .StatusCode >= 400 {
3021+ stopCompactKeepalive ()
29973022 respBody := s .readUpstreamErrorBody (resp )
29983023 _ = resp .Body .Close ()
29993024 resp .Body = io .NopCloser (bytes .NewReader (respBody ))
30003025
30013026 upstreamMsg := strings .TrimSpace (extractUpstreamErrorMessage (respBody ))
30023027 upstreamMsg = sanitizeUpstreamErrorMessage (upstreamMsg )
30033028 upstreamCode := extractUpstreamErrorCode (respBody )
3029+ if openAICompactKeepaliveCommitted (c ) {
3030+ logOpenAICompactKeepaliveCommitted (ctx , c , account , resp )
3031+ return s .handleErrorResponse (ctx , resp , c , account , body , billingModel )
3032+ }
30043033 if ! httpInvalidEncryptedContentRetryTried && resp .StatusCode == http .StatusBadRequest && upstreamCode == "invalid_encrypted_content" {
30053034 decoded , decodeErr := ensureReqBody ()
30063035 if decodeErr != nil {
@@ -3063,6 +3092,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
30633092 imageCount := 0
30643093 var imageOutputSizes []string
30653094 if reqStream {
3095+ stopCompactKeepalive ()
30663096 streamResult , err := s .handleStreamingResponse (ctx , resp , c , account , startTime , originalModel , upstreamModel )
30673097 if err != nil {
30683098 return nil , err
@@ -3073,7 +3103,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
30733103 imageCount = streamResult .imageCount
30743104 imageOutputSizes = streamResult .imageOutputSizes
30753105 } else {
3076- nonStreamResult , err := s .handleNonStreamingResponse (ctx , resp , c , account , originalModel , upstreamModel )
3106+ nonStreamResult , err := s .handleNonStreamingResponse (ctx , resp , c , account , originalModel , upstreamModel , stopCompactKeepalive )
30773107 if err != nil {
30783108 return nil , err
30793109 }
@@ -3686,6 +3716,16 @@ func (s *OpenAIGatewayService) startCompactNonstreamKeepalive(ctx context.Contex
36863716 if ctx == nil {
36873717 ctx = context .Background ()
36883718 }
3719+ path := ""
3720+ if c .Request != nil && c .Request .URL != nil {
3721+ path = strings .TrimSpace (c .Request .URL .Path )
3722+ }
3723+ log := logger .FromContext (ctx ).With (
3724+ zap .String ("component" , "service.openai_gateway" ),
3725+ zap .String ("request_path" , path ),
3726+ zap .Int ("interval_seconds" , int (interval .Seconds ())),
3727+ )
3728+ log .Info ("OpenAI compact non-stream keepalive started" )
36893729
36903730 headers := c .Writer .Header ()
36913731 headers .Set ("Content-Type" , "application/json" )
@@ -3701,15 +3741,23 @@ func (s *OpenAIGatewayService) startCompactNonstreamKeepalive(ctx context.Contex
37013741 defer wg .Done ()
37023742 ticker := time .NewTicker (interval )
37033743 defer ticker .Stop ()
3744+ flushedLogged := false
37043745 for {
37053746 select {
37063747 case <- stopCh :
37073748 return
37083749 case <- ctx .Done ():
37093750 return
37103751 case <- ticker .C :
3711- _ , _ = c .Writer .Write ([]byte ("\n " ))
3752+ if _ , err := c .Writer .Write ([]byte ("\n " )); err != nil {
3753+ log .Warn ("OpenAI compact non-stream keepalive write failed" , zap .Error (err ))
3754+ return
3755+ }
37123756 flusher .Flush ()
3757+ if ! flushedLogged {
3758+ log .Info ("OpenAI compact non-stream keepalive flushed" )
3759+ flushedLogged = true
3760+ }
37133761 }
37143762 }
37153763 }()
@@ -5385,17 +5433,22 @@ func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
53855433 }, true
53865434}
53875435
5388- func (s * OpenAIGatewayService ) handleNonStreamingResponse (ctx context.Context , resp * http.Response , c * gin.Context , account * Account , originalModel , mappedModel string ) (* openaiNonStreamingResult , error ) {
5389- body , err := ReadUpstreamResponseBody (resp .Body , s .cfg , c , openAITooLargeError )
5436+ func (s * OpenAIGatewayService ) handleNonStreamingResponse (ctx context.Context , resp * http.Response , c * gin.Context , account * Account , originalModel , mappedModel string , stopBeforeWrite ... func ()) (* openaiNonStreamingResult , error ) {
5437+ stop := compactStopFunc (stopBeforeWrite ... )
5438+ body , err := ReadUpstreamResponseBody (resp .Body , s .cfg , c , func (c * gin.Context ) {
5439+ stop ()
5440+ openAITooLargeError (c )
5441+ })
53905442 if err != nil {
5443+ stop ()
53915444 return nil , err
53925445 }
53935446
53945447 // Detect SSE responses for ALL account types via Content-Type header.
53955448 // Some OpenAI-compatible upstreams (including other sub2api instances)
53965449 // may return SSE even when stream=false was requested.
53975450 if isEventStreamResponse (resp .Header ) {
5398- return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel )
5451+ return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel , stop )
53995452 }
54005453 bodyLooksLikeSSE := bytes .Contains (body , []byte ("data:" )) || bytes .Contains (body , []byte ("event:" ))
54015454
@@ -5405,14 +5458,15 @@ func (s *OpenAIGatewayService) handleNonStreamingResponse(ctx context.Context, r
54055458 // positives on JSON responses that coincidentally contain "data:" or
54065459 // "event:" in their text content.
54075460 if account .Type == AccountTypeOAuth && bodyLooksLikeSSE {
5408- return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel )
5461+ return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel , stop )
54095462 }
54105463
54115464 usageValue , usageOK := extractOpenAIUsageFromJSONBytes (body )
54125465 if ! usageOK {
54135466 if bodyLooksLikeSSE {
5414- return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel )
5467+ return s .handleSSEToJSON (resp , c , body , originalModel , mappedModel , stop )
54155468 }
5469+ stop ()
54165470 return nil , fmt .Errorf ("parse response: invalid json response" )
54175471 }
54185472 usage := & usageValue
@@ -5431,6 +5485,7 @@ func (s *OpenAIGatewayService) handleNonStreamingResponse(ctx context.Context, r
54315485 }
54325486 }
54335487
5488+ stop ()
54345489 c .Data (resp .StatusCode , contentType , body )
54355490
54365491 return & openaiNonStreamingResult {
@@ -5442,12 +5497,20 @@ func (s *OpenAIGatewayService) handleNonStreamingResponse(ctx context.Context, r
54425497 }, nil
54435498}
54445499
5500+ func compactStopFunc (stops ... func ()) func () {
5501+ if len (stops ) == 0 || stops [0 ] == nil {
5502+ return func () {}
5503+ }
5504+ return stops [0 ]
5505+ }
5506+
54455507func isEventStreamResponse (header http.Header ) bool {
54465508 contentType := strings .ToLower (header .Get ("Content-Type" ))
54475509 return strings .Contains (contentType , "text/event-stream" )
54485510}
54495511
5450- func (s * OpenAIGatewayService ) handleSSEToJSON (resp * http.Response , c * gin.Context , body []byte , originalModel , mappedModel string ) (* openaiNonStreamingResult , error ) {
5512+ func (s * OpenAIGatewayService ) handleSSEToJSON (resp * http.Response , c * gin.Context , body []byte , originalModel , mappedModel string , stopBeforeWrite ... func ()) (* openaiNonStreamingResult , error ) {
5513+ stop := compactStopFunc (stopBeforeWrite ... )
54515514 bodyText := string (body )
54525515 finalResponse , ok := extractCodexFinalResponse (bodyText )
54535516
@@ -5479,6 +5542,7 @@ func (s *OpenAIGatewayService) handleSSEToJSON(resp *http.Response, c *gin.Conte
54795542 if msg == "" {
54805543 msg = "Upstream compact response failed"
54815544 }
5545+ stop ()
54825546 return nil , s .writeOpenAINonStreamingProtocolError (resp , c , msg )
54835547 }
54845548 usage = s .parseSSEUsageFromBody (bodyText )
@@ -5497,6 +5561,7 @@ func (s *OpenAIGatewayService) handleSSEToJSON(resp *http.Response, c *gin.Conte
54975561 contentType = "text/event-stream"
54985562 }
54995563 }
5564+ stop ()
55005565 c .Data (resp .StatusCode , contentType , body )
55015566
55025567 return & openaiNonStreamingResult {
0 commit comments