@@ -36,6 +36,48 @@ const (
3636
3737var dataTag = []byte ("data:" )
3838
39+ // Streamed Codex responses may emit response.output_item.done events while leaving
40+ // response.completed.response.output empty. Keep the stream path aligned with the
41+ // already-patched non-stream path by reconstructing response.output from those items.
42+ func collectCodexOutputItemDone (eventData []byte , outputItemsByIndex map [int64 ][]byte , outputItemsFallback * [][]byte ) {
43+ itemResult := gjson .GetBytes (eventData , "item" )
44+ if ! itemResult .Exists () || itemResult .Type != gjson .JSON {
45+ return
46+ }
47+ outputIndexResult := gjson .GetBytes (eventData , "output_index" )
48+ if outputIndexResult .Exists () {
49+ outputItemsByIndex [outputIndexResult .Int ()] = []byte (itemResult .Raw )
50+ return
51+ }
52+ * outputItemsFallback = append (* outputItemsFallback , []byte (itemResult .Raw ))
53+ }
54+
55+ func patchCodexCompletedOutput (eventData []byte , outputItemsByIndex map [int64 ][]byte , outputItemsFallback [][]byte ) []byte {
56+ outputResult := gjson .GetBytes (eventData , "response.output" )
57+ shouldPatchOutput := (! outputResult .Exists () || ! outputResult .IsArray () || len (outputResult .Array ()) == 0 ) && (len (outputItemsByIndex ) > 0 || len (outputItemsFallback ) > 0 )
58+ if ! shouldPatchOutput {
59+ return eventData
60+ }
61+
62+ completedDataPatched := eventData
63+ completedDataPatched , _ = sjson .SetRawBytes (completedDataPatched , "response.output" , []byte (`[]` ))
64+
65+ indexes := make ([]int64 , 0 , len (outputItemsByIndex ))
66+ for idx := range outputItemsByIndex {
67+ indexes = append (indexes , idx )
68+ }
69+ sort .Slice (indexes , func (i , j int ) bool {
70+ return indexes [i ] < indexes [j ]
71+ })
72+ for _ , idx := range indexes {
73+ completedDataPatched , _ = sjson .SetRawBytes (completedDataPatched , "response.output.-1" , outputItemsByIndex [idx ])
74+ }
75+ for _ , item := range outputItemsFallback {
76+ completedDataPatched , _ = sjson .SetRawBytes (completedDataPatched , "response.output.-1" , item )
77+ }
78+ return completedDataPatched
79+ }
80+
3981// CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint).
4082// If api_key is unavailable on auth, it falls back to legacy via ClientAdapter.
4183type CodexExecutor struct {
@@ -414,20 +456,28 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
414456 scanner := bufio .NewScanner (httpResp .Body )
415457 scanner .Buffer (nil , 52_428_800 ) // 50MB
416458 var param any
459+ outputItemsByIndex := make (map [int64 ][]byte )
460+ var outputItemsFallback [][]byte
417461 for scanner .Scan () {
418462 line := scanner .Bytes ()
419463 helps .AppendAPIResponseChunk (ctx , e .cfg , line )
464+ translatedLine := bytes .Clone (line )
420465
421466 if bytes .HasPrefix (line , dataTag ) {
422467 data := bytes .TrimSpace (line [5 :])
423- if gjson .GetBytes (data , "type" ).String () == "response.completed" {
468+ switch gjson .GetBytes (data , "type" ).String () {
469+ case "response.output_item.done" :
470+ collectCodexOutputItemDone (data , outputItemsByIndex , & outputItemsFallback )
471+ case "response.completed" :
424472 if detail , ok := helps .ParseCodexUsage (data ); ok {
425473 reporter .Publish (ctx , detail )
426474 }
475+ data = patchCodexCompletedOutput (data , outputItemsByIndex , outputItemsFallback )
476+ translatedLine = append ([]byte ("data: " ), data ... )
427477 }
428478 }
429479
430- chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , originalPayload , body , bytes . Clone ( line ) , & param )
480+ chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , originalPayload , body , translatedLine , & param )
431481 for i := range chunks {
432482 out <- cliproxyexecutor.StreamChunk {Payload : chunks [i ]}
433483 }
0 commit comments