@@ -36,6 +36,69 @@ 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+ indexes := make ([]int64 , 0 , len (outputItemsByIndex ))
63+ for idx := range outputItemsByIndex {
64+ indexes = append (indexes , idx )
65+ }
66+ sort .Slice (indexes , func (i , j int ) bool {
67+ return indexes [i ] < indexes [j ]
68+ })
69+
70+ items := make ([][]byte , 0 , len (outputItemsByIndex )+ len (outputItemsFallback ))
71+ for _ , idx := range indexes {
72+ items = append (items , outputItemsByIndex [idx ])
73+ }
74+ items = append (items , outputItemsFallback ... )
75+
76+ outputArray := []byte ("[]" )
77+ if len (items ) > 0 {
78+ var buf bytes.Buffer
79+ totalLen := 2
80+ for _ , item := range items {
81+ totalLen += len (item )
82+ }
83+ if len (items ) > 1 {
84+ totalLen += len (items ) - 1
85+ }
86+ buf .Grow (totalLen )
87+ buf .WriteByte ('[' )
88+ for i , item := range items {
89+ if i > 0 {
90+ buf .WriteByte (',' )
91+ }
92+ buf .Write (item )
93+ }
94+ buf .WriteByte (']' )
95+ outputArray = buf .Bytes ()
96+ }
97+
98+ completedDataPatched , _ := sjson .SetRawBytes (eventData , "response.output" , outputArray )
99+ return completedDataPatched
100+ }
101+
39102// CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint).
40103// If api_key is unavailable on auth, it falls back to legacy via ClientAdapter.
41104type CodexExecutor struct {
@@ -414,20 +477,28 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
414477 scanner := bufio .NewScanner (httpResp .Body )
415478 scanner .Buffer (nil , 52_428_800 ) // 50MB
416479 var param any
480+ outputItemsByIndex := make (map [int64 ][]byte )
481+ var outputItemsFallback [][]byte
417482 for scanner .Scan () {
418483 line := scanner .Bytes ()
419484 helps .AppendAPIResponseChunk (ctx , e .cfg , line )
485+ translatedLine := bytes .Clone (line )
420486
421487 if bytes .HasPrefix (line , dataTag ) {
422488 data := bytes .TrimSpace (line [5 :])
423- if gjson .GetBytes (data , "type" ).String () == "response.completed" {
489+ switch gjson .GetBytes (data , "type" ).String () {
490+ case "response.output_item.done" :
491+ collectCodexOutputItemDone (data , outputItemsByIndex , & outputItemsFallback )
492+ case "response.completed" :
424493 if detail , ok := helps .ParseCodexUsage (data ); ok {
425494 reporter .Publish (ctx , detail )
426495 }
496+ data = patchCodexCompletedOutput (data , outputItemsByIndex , outputItemsFallback )
497+ translatedLine = append ([]byte ("data: " ), data ... )
427498 }
428499 }
429500
430- chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , originalPayload , body , bytes . Clone ( line ) , & param )
501+ chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , originalPayload , body , translatedLine , & param )
431502 for i := range chunks {
432503 out <- cliproxyexecutor.StreamChunk {Payload : chunks [i ]}
433504 }
0 commit comments