@@ -44,6 +44,23 @@ const (
4444
4545var dataTag = []byte ("data:" )
4646
47+ const codexIncompleteStreamMessage = "stream error: stream disconnected before completion: stream closed before response.completed"
48+
49+ type codexIncompleteStreamError struct {
50+ statusErr
51+ }
52+
53+ func newCodexIncompleteStreamError () codexIncompleteStreamError {
54+ return codexIncompleteStreamError {statusErr : statusErr {
55+ code : http .StatusRequestTimeout ,
56+ msg : codexIncompleteStreamMessage ,
57+ }}
58+ }
59+
60+ func (codexIncompleteStreamError ) IsRequestScoped () bool {
61+ return true
62+ }
63+
4764// Streamed Codex responses may emit response.output_item.done events while leaving
4865// response.completed.response.output empty. Keep the stream path aligned with the
4966// already-patched non-stream path by reconstructing response.output from those items.
@@ -116,6 +133,50 @@ func codexTerminalStreamContextLengthErr(eventData []byte) (statusErr, bool) {
116133}
117134
118135func codexTerminalStreamErr (eventData []byte ) (statusErr , []byte , bool ) {
136+ body , ok := codexTerminalFailureBody (eventData )
137+ if ! ok || ! codexTerminalStreamErrShouldHandle (body ) {
138+ return statusErr {}, nil , false
139+ }
140+ return newCodexStatusErr (http .StatusBadRequest , body ), body , true
141+ }
142+
143+ func codexTerminalFailureErr (eventData []byte ) (statusErr , []byte , bool ) {
144+ if streamErr , body , ok := codexTerminalStreamErr (eventData ); ok {
145+ return streamErr , body , true
146+ }
147+ body , ok := codexTerminalFailureBody (eventData )
148+ if ! ok {
149+ return statusErr {}, nil , false
150+ }
151+ return newCodexStatusErr (codexTerminalFailureStatus (body ), body ), body , true
152+ }
153+
154+ func codexTerminalFailureStatus (body []byte ) int {
155+ for _ , path := range []string {"error.status_code" , "error.status" } {
156+ if status := int (gjson .GetBytes (body , path ).Int ()); status >= 400 && status <= 599 {
157+ return status
158+ }
159+ }
160+
161+ errorType := strings .ToLower (strings .TrimSpace (gjson .GetBytes (body , "error.type" ).String ()))
162+ errorCode := strings .ToLower (strings .TrimSpace (gjson .GetBytes (body , "error.code" ).String ()))
163+ switch {
164+ case errorType == "invalid_request_error" , errorType == "bad_request_error" :
165+ return http .StatusBadRequest
166+ case errorType == "authentication_error" , errorCode == "invalid_api_key" , errorCode == "unauthorized" :
167+ return http .StatusUnauthorized
168+ case errorType == "permission_error" , errorCode == "forbidden" , errorCode == "permission_denied" :
169+ return http .StatusForbidden
170+ case errorType == "not_found_error" , errorCode == "not_found" , errorCode == "model_not_found" :
171+ return http .StatusNotFound
172+ case errorType == "rate_limit_error" , errorCode == "rate_limit_exceeded" :
173+ return http .StatusTooManyRequests
174+ default :
175+ return http .StatusBadGateway
176+ }
177+ }
178+
179+ func codexTerminalFailureBody (eventData []byte ) ([]byte , bool ) {
119180 eventType := gjson .GetBytes (eventData , "type" ).String ()
120181 var body []byte
121182 switch eventType {
@@ -130,15 +191,12 @@ func codexTerminalStreamErr(eventData []byte) (statusErr, []byte, bool) {
130191 body = codexTerminalErrorBody (eventData , "error" )
131192 }
132193 default :
133- return statusErr {}, nil , false
194+ return nil , false
134195 }
135196 if len (body ) == 0 {
136- return statusErr {}, nil , false
197+ body = [] byte ( `{"error":{"message":"upstream stream failed without error details"}}` )
137198 }
138- if ! codexTerminalStreamErrShouldHandle (body ) {
139- return statusErr {}, nil , false
140- }
141- return newCodexStatusErr (http .StatusBadRequest , body ), body , true
199+ return body , true
142200}
143201
144202func codexTerminalStreamErrShouldHandle (body []byte ) bool {
@@ -854,11 +912,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
854912 err = newCodexStatusErr (httpResp .StatusCode , b )
855913 return resp , err
856914 }
857- data , err := io .ReadAll (httpResp .Body )
858- if err != nil {
859- helps .RecordAPIResponseError (ctx , e .cfg , err )
860- return resp , err
861- }
915+ data , errRead := io .ReadAll (httpResp .Body )
862916 upstreamData := applyCodexIdentityConfuseResponsePayload (data , identityState )
863917 helps .AppendAPIResponseChunk (ctx , e .cfg , upstreamData )
864918
@@ -873,7 +927,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
873927 eventData := bytes .TrimSpace (line [5 :])
874928 eventType := gjson .GetBytes (eventData , "type" ).String ()
875929
876- if streamErr , terminalBody , ok := codexTerminalStreamErr (eventData ); ok {
930+ if streamErr , terminalBody , ok := codexTerminalFailureErr (eventData ); ok {
877931 if errClearReplay := clearCodexReasoningReplayOnInvalidSignature (ctx , replayScope , streamErr .StatusCode (), terminalBody ); errClearReplay != nil {
878932 return resp , errClearReplay
879933 }
@@ -895,7 +949,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
895949 continue
896950 }
897951
898- if eventType != "response.completed" {
952+ if eventType != "response.completed" && eventType != "response.incomplete" {
899953 continue
900954 }
901955
@@ -926,15 +980,25 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
926980 }
927981 completedData = completedDataPatched
928982 }
929- cacheCodexReasoningReplayFromCompleted (replayScope , completedData )
983+ if eventType == "response.completed" {
984+ cacheCodexReasoningReplayFromCompleted (replayScope , completedData )
985+ }
930986
931987 var param any
932988 clientCompletedData := applyCodexIdentityExposeResponsePayload (completedData , identityState )
933989 out := sdktranslator .TranslateNonStream (ctx , to , responseFormat , req .Model , originalPayload , body , clientCompletedData , & param )
934990 resp = cliproxyexecutor.Response {Payload : out , Headers : httpResp .Header .Clone ()}
935991 return resp , nil
936992 }
937- err = statusErr {code : 408 , msg : "stream error: stream disconnected before completion: stream closed before response.completed" }
993+ if errRead != nil {
994+ if errCtx := ctx .Err (); errCtx != nil {
995+ helps .RecordAPIResponseError (ctx , e .cfg , errCtx )
996+ err = errCtx
997+ return resp , err
998+ }
999+ helps .RecordAPIResponseError (ctx , e .cfg , errRead )
1000+ }
1001+ err = newCodexIncompleteStreamError ()
9381002 return resp , err
9391003}
9401004
@@ -1159,10 +1223,12 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
11591223 line := applyCodexIdentityConfuseResponsePayload (scanner .Bytes (), identityState )
11601224 helps .AppendAPIResponseChunk (ctx , e .cfg , line )
11611225 translatedLine := bytes .Clone (line )
1226+ terminalSuccess := false
11621227
11631228 if bytes .HasPrefix (line , dataTag ) {
11641229 data := bytes .TrimSpace (line [5 :])
1165- if streamErr , terminalBody , ok := codexTerminalStreamErr (data ); ok {
1230+ eventType := gjson .GetBytes (data , "type" ).String ()
1231+ if streamErr , terminalBody , ok := codexTerminalFailureErr (data ); ok {
11661232 if errClearReplay := clearCodexReasoningReplayOnInvalidSignature (ctx , replayScope , streamErr .StatusCode (), terminalBody ); errClearReplay != nil {
11671233 helps .RecordAPIResponseError (ctx , e .cfg , errClearReplay )
11681234 reporter .PublishFailure (ctx , errClearReplay )
@@ -1180,16 +1246,19 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
11801246 }
11811247 return
11821248 }
1183- switch gjson . GetBytes ( data , "type" ). String () {
1249+ switch eventType {
11841250 case "response.output_item.done" :
11851251 collectCodexOutputItemDone (data , outputItemsByIndex , & outputItemsFallback )
1186- case "response.completed" :
1252+ case "response.completed" , "response.incomplete" :
1253+ terminalSuccess = true
11871254 if detail , ok := helps .ParseCodexUsage (data ); ok {
11881255 reporter .Publish (ctx , detail )
11891256 }
11901257 publishCodexImageToolUsage (ctx , reporter , body , data )
11911258 data = patchCodexCompletedOutput (data , outputItemsByIndex , outputItemsFallback )
1192- cacheCodexReasoningReplayFromCompleted (replayScope , data )
1259+ if eventType == "response.completed" {
1260+ cacheCodexReasoningReplayFromCompleted (replayScope , data )
1261+ }
11931262 translatedLine = append ([]byte ("data: " ), data ... )
11941263 }
11951264 }
@@ -1203,14 +1272,22 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
12031272 return
12041273 }
12051274 }
1275+ if terminalSuccess {
1276+ return
1277+ }
12061278 }
12071279 if errScan := scanner .Err (); errScan != nil {
1208- helps .RecordAPIResponseError (ctx , e .cfg , errScan )
1209- reporter .PublishFailure (ctx , errScan )
1210- select {
1211- case out <- cliproxyexecutor.StreamChunk {Err : errScan }:
1212- case <- ctx .Done ():
1280+ if ctx .Err () != nil {
1281+ return
12131282 }
1283+ helps .RecordAPIResponseError (ctx , e .cfg , errScan )
1284+ }
1285+ streamErr := newCodexIncompleteStreamError ()
1286+ helps .RecordAPIResponseError (ctx , e .cfg , streamErr )
1287+ reporter .PublishFailure (ctx , streamErr )
1288+ select {
1289+ case out <- cliproxyexecutor.StreamChunk {Err : streamErr }:
1290+ case <- ctx .Done ():
12141291 }
12151292 }()
12161293 return & cliproxyexecutor.StreamResult {Headers : httpResp .Header .Clone (), Chunks : out }, nil
0 commit comments