@@ -25,11 +25,7 @@ import {
2525 ToolCall ,
2626 ToolCallDelta ,
2727} from '../../types/llm/response'
28- import {
29- StreamSource ,
30- postJson ,
31- postStream ,
32- } from '../../utils/llm/httpTransport'
28+ import { StreamSource , postStream } from '../../utils/llm/httpTransport'
3329import { parseJsonSseStream } from '../../utils/llm/sse'
3430
3531type CodexAdapterConfig = {
@@ -51,24 +47,65 @@ export class CodexMessageAdapter {
5147 options ?: LLMOptions ,
5248 headers ?: Record < string , string > ,
5349 ) : Promise < LLMResponseNonStreaming > {
54- const body = this . buildRequestBody ( { request, stream : false } )
55- const payload = await postJson < Response > ( this . endpoint , body , {
50+ // Codex Responses require stream: true; build a snapshot from the stream.
51+ const body = this . buildRequestBody ( { request, stream : true } )
52+ const stream = await postStream ( this . endpoint , body , {
5653 headers,
5754 signal : options ?. signal ,
5855 fetchFn : this . fetchFn ,
5956 } )
60- const content = extractResponseText ( payload )
61- const toolCalls = extractToolCalls ( payload )
62- const reasoningSummary = extractReasoningSummary ( payload )
57+
58+ let summaryText = ''
59+ let responsePayload : Response | undefined
60+ for await ( const chunk of parseJsonSseStream < ResponseStreamEvent > ( stream ) ) {
61+ if ( chunk . type === 'response.created' ) {
62+ responsePayload = chunk . response
63+ continue
64+ }
65+
66+ if ( chunk . type === 'error' ) {
67+ throw new Error ( chunk . message )
68+ }
69+
70+ if ( ! responsePayload ) {
71+ throw new Error (
72+ `Stream event received before response.created: ${ chunk . type } ` ,
73+ )
74+ }
75+
76+ if ( chunk . type === 'response.reasoning_summary_text.delta' ) {
77+ summaryText += chunk . delta
78+ continue
79+ }
80+
81+ if ( chunk . type === 'response.reasoning_summary_text.done' ) {
82+ if ( ! summaryText . length ) {
83+ summaryText = chunk . text
84+ }
85+ continue
86+ }
87+
88+ responsePayload = accumulateResponseSnapshot ( responsePayload , chunk )
89+ }
90+
91+ if ( ! responsePayload ) {
92+ throw new Error ( 'Stream ended without receiving a response payload' )
93+ }
94+
95+ const content = extractResponseText ( responsePayload )
96+ const toolCalls = extractToolCalls ( responsePayload )
97+ const reasoningSummary =
98+ extractReasoningSummary ( responsePayload ) ??
99+ ( summaryText . length ? summaryText : undefined )
63100
64101 return {
65- id : payload . id ,
66- created : payload . created_at ,
67- model : payload . model ,
102+ id : responsePayload . id ,
103+ created : responsePayload . created_at ,
104+ model : responsePayload . model ,
68105 object : 'chat.completion' ,
69106 choices : [
70107 {
71- finish_reason : toolCalls . length > 0 ? 'tool_calls' : 'stop' ,
108+ finish_reason : null ,
72109 message : {
73110 role : 'assistant' ,
74111 content,
@@ -77,8 +114,8 @@ export class CodexMessageAdapter {
77114 } ,
78115 } ,
79116 ] ,
80- system_fingerprint : getSystemFingerprint ( payload ) ,
81- usage : mapUsage ( payload . usage ) ,
117+ system_fingerprint : getSystemFingerprint ( responsePayload ) ,
118+ usage : mapUsage ( responsePayload . usage ) ,
82119 }
83120 }
84121
@@ -536,6 +573,103 @@ function getSystemFingerprint(payload: Response): string | undefined {
536573 return ( payload as { system_fingerprint ?: string } ) . system_fingerprint
537574}
538575
576+ function accumulateResponseSnapshot (
577+ snapshot : Response ,
578+ event : ResponseStreamEvent ,
579+ ) : Response {
580+ switch ( event . type ) {
581+ case 'response.output_item.added' : {
582+ snapshot . output . push ( event . item )
583+ return snapshot
584+ }
585+ case 'response.content_part.added' : {
586+ const output = snapshot . output [ event . output_index ]
587+ if ( ! output ) {
588+ throw new Error ( `missing output at index ${ event . output_index } ` )
589+ }
590+ const part = event . part
591+ if ( output . type === 'message' && part . type !== 'reasoning_text' ) {
592+ output . content . push ( part )
593+ } else if (
594+ output . type === 'reasoning' &&
595+ part . type === 'reasoning_text'
596+ ) {
597+ if ( ! output . content ) {
598+ output . content = [ ]
599+ }
600+ output . content . push ( part )
601+ }
602+ return snapshot
603+ }
604+ case 'response.output_text.delta' : {
605+ const output = snapshot . output [ event . output_index ]
606+ if ( ! output ) {
607+ throw new Error ( `missing output at index ${ event . output_index } ` )
608+ }
609+ if ( output . type === 'message' ) {
610+ const content = output . content [ event . content_index ]
611+ if ( ! content ) {
612+ throw new Error ( `missing content at index ${ event . content_index } ` )
613+ }
614+ if ( content . type !== 'output_text' ) {
615+ throw new Error (
616+ `expected content to be 'output_text', got ${ content . type } ` ,
617+ )
618+ }
619+ content . text += event . delta
620+ }
621+ return snapshot
622+ }
623+ case 'response.function_call_arguments.delta' : {
624+ const output = snapshot . output [ event . output_index ]
625+ if ( ! output ) {
626+ throw new Error ( `missing output at index ${ event . output_index } ` )
627+ }
628+ if ( output . type === 'function_call' ) {
629+ output . arguments += event . delta
630+ }
631+ return snapshot
632+ }
633+ case 'response.function_call_arguments.done' : {
634+ const output = snapshot . output [ event . output_index ]
635+ if ( ! output ) {
636+ throw new Error ( `missing output at index ${ event . output_index } ` )
637+ }
638+ if ( output . type === 'function_call' && ! output . arguments ?. length ) {
639+ output . arguments = event . arguments
640+ }
641+ return snapshot
642+ }
643+ case 'response.reasoning_text.delta' : {
644+ const output = snapshot . output [ event . output_index ]
645+ if ( ! output ) {
646+ throw new Error ( `missing output at index ${ event . output_index } ` )
647+ }
648+ if ( output . type === 'reasoning' ) {
649+ const content = output . content ?. [ event . content_index ]
650+ if ( ! content ) {
651+ throw new Error ( `missing content at index ${ event . content_index } ` )
652+ }
653+ if ( content . type !== 'reasoning_text' ) {
654+ const contentType = ( content as { type : string } ) . type
655+ throw new Error (
656+ `expected content to be 'reasoning_text', got ${ contentType } ` ,
657+ )
658+ }
659+ content . text += event . delta
660+ }
661+ return snapshot
662+ }
663+ case 'response.completed' :
664+ return event . response
665+ case 'response.incomplete' :
666+ return event . response
667+ case 'error' :
668+ return snapshot
669+ }
670+ return snapshot
671+ }
672+
539673function mapUsage ( usage ?: OpenAIResponseUsage ) : ResponseUsage | undefined {
540674 if ( ! usage ) {
541675 return undefined
0 commit comments