@@ -143,6 +143,30 @@ function upstreamErrorEvent(
143143 } ;
144144}
145145
146+ function stopReasonFor ( finishReason : unknown ) : "max_tokens" | "content_filter" | undefined {
147+ return finishReason === "length"
148+ ? "max_tokens"
149+ : finishReason === "content_filter"
150+ ? "content_filter"
151+ : undefined ;
152+ }
153+
154+ function reasoningTextFrom ( record : Record < string , unknown > ) : string | undefined {
155+ return typeof record . reasoning_content === "string" && record . reasoning_content . length > 0
156+ ? record . reasoning_content
157+ : typeof record . reasoning === "string" && record . reasoning . length > 0
158+ ? record . reasoning
159+ : undefined ;
160+ }
161+
162+ function invalidChoicesEvent ( usage ?: OcxUsage ) : Extract < AdapterEvent , { type : "error" } > {
163+ return {
164+ type : "error" ,
165+ message : "upstream response contained invalid choices" ,
166+ ...( usage !== undefined ? { usage } : { } ) ,
167+ } ;
168+ }
169+
146170function developerSystemText ( message : OcxMessage ) : string | undefined {
147171 if ( message . role !== "developer" ) return undefined ;
148172 if ( typeof message . content === "string" ) return message . content ;
@@ -797,21 +821,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
797821 interface PendingToolCall { key : string ; id : string ; name : string ; args : string ; argsBytes : number }
798822 const pendingToolCalls : PendingToolCall [ ] = [ ] ;
799823 let toolCallSeq = 0 ;
824+ const closeToolCalls = ( ) : PendingToolCall [ ] => {
825+ const calls = [ ...pendingToolCalls ] ;
826+ for ( const call of calls ) budget . closeCall ( call . key ) ;
827+ pendingToolCalls . length = 0 ;
828+ return calls ;
829+ } ;
800830 const flushToolCalls = function * ( ) : Generator < AdapterEvent > {
801831 // Do not treat flushed tool calls as user-facing output for the finish-less EOF
802832 // fallback — incomplete tool args must stay on the truncation path.
803- for ( const call of pendingToolCalls ) {
833+ for ( const call of closeToolCalls ( ) ) {
804834 if ( ! call . id ) call . id = `call_${ ++ toolCallSeq } ` ;
805835 yield { type : "tool_call_start" , id : call . id , name : call . name } ;
806836 if ( call . args . length > 0 ) yield { type : "tool_call_delta" , arguments : call . args } ;
807837 yield { type : "tool_call_end" } ;
808- budget . closeCall ( call . key ) ;
809838 }
810- pendingToolCalls . length = 0 ;
811839 } ;
812- const discardToolCalls = ( ) : void => {
813- for ( const call of pendingToolCalls ) budget . closeCall ( call . key ) ;
814- pendingToolCalls . length = 0 ;
840+ const terminateWithError = function * (
841+ event : Extract < AdapterEvent , { type : "error" } > ,
842+ ) : Generator < AdapterEvent , "terminate" > {
843+ closeToolCalls ( ) ;
844+ yield event ;
845+ return "terminate" ;
815846 } ;
816847 let pendingUsage : OcxUsage | undefined ;
817848 // Track terminal signals so a socket EOF without any terminator can fail closed instead of
@@ -832,11 +863,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
832863 const payload = line . slice ( 6 ) . trim ( ) ;
833864 if ( payload === "[DONE]" ) {
834865 yield * flushToolCalls ( ) ;
835- const stopReason = finishReason === "length"
836- ? "max_tokens"
837- : finishReason === "content_filter"
838- ? "content_filter"
839- : undefined ;
866+ const stopReason = stopReasonFor ( finishReason ) ;
840867 yield { type : "done" , usage : pendingUsage , ...( stopReason ? { stopReason } : { } ) } ;
841868 return "terminate" ;
842869 }
@@ -855,9 +882,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
855882 if ( chunk . error !== undefined && chunk . error !== null ) {
856883 const event = upstreamErrorEvent ( chunk . error , pendingUsage ) ;
857884 debugProviderDiagnostic ( "openai-chat" , "stream-error" , { message : event . message } ) ;
858- discardToolCalls ( ) ;
859- yield event ;
860- return "terminate" ;
885+ return yield * terminateWithError ( event ) ;
861886 }
862887
863888 if ( chunk . usage ) {
@@ -870,24 +895,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
870895 const choices = chunk . choices ;
871896 if ( choices === undefined ) return "continue" ;
872897 if ( ! Array . isArray ( choices ) ) {
873- discardToolCalls ( ) ;
874- yield {
875- type : "error" ,
876- message : "upstream response contained invalid choices" ,
877- ...( pendingUsage ? { usage : pendingUsage } : { } ) ,
878- } ;
879- return "terminate" ;
898+ return yield * terminateWithError ( invalidChoicesEvent ( pendingUsage ) ) ;
880899 }
881900 if ( choices . length === 0 ) return "continue" ;
882901 const rawChoice = choices [ 0 ] ;
883902 if ( rawChoice === null || typeof rawChoice !== "object" || Array . isArray ( rawChoice ) ) {
884- discardToolCalls ( ) ;
885- yield {
886- type : "error" ,
887- message : "upstream response contained invalid choices" ,
888- ...( pendingUsage ? { usage : pendingUsage } : { } ) ,
889- } ;
890- return "terminate" ;
903+ return yield * terminateWithError ( invalidChoicesEvent ( pendingUsage ) ) ;
891904 }
892905 const choice = rawChoice as {
893906 delta ?: Record < string , unknown > ;
@@ -897,9 +910,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
897910 if ( choice . finish_reason === "error" ) {
898911 const event = upstreamErrorEvent ( choice . error , pendingUsage ) ;
899912 debugProviderDiagnostic ( "openai-chat" , "stream-error" , { message : event . message } ) ;
900- discardToolCalls ( ) ;
901- yield event ;
902- return "terminate" ;
913+ return yield * terminateWithError ( event ) ;
903914 }
904915 // Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
905916 // no delta) is a graceful close and must record finishReason even though we skip it below.
@@ -908,11 +919,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
908919 }
909920 const delta = choice . delta ;
910921 if ( delta ) {
911- const reasoningText = typeof delta . reasoning_content === "string" && delta . reasoning_content . length > 0
912- ? delta . reasoning_content
913- : typeof delta . reasoning === "string" && delta . reasoning . length > 0
914- ? delta . reasoning
915- : undefined ;
922+ const reasoningText = reasoningTextFrom ( delta ) ;
916923 if ( reasoningText !== undefined ) {
917924 yield { type : "reasoning_raw_delta" , text : reasoningText } ;
918925 }
@@ -1041,11 +1048,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10411048 }
10421049 yield * flushToolCalls ( ) ;
10431050 // Graceful close that omitted [DONE] but delivered finish_reason and/or answer text.
1044- const stopReason = finishReason === "length"
1045- ? "max_tokens"
1046- : finishReason === "content_filter"
1047- ? "content_filter"
1048- : undefined ;
1051+ const stopReason = stopReasonFor ( finishReason ) ;
10491052 yield { type : "done" , usage : pendingUsage , ...( stopReason ? { stopReason } : { } ) } ;
10501053 } catch ( error ) {
10511054 if ( isTranslatorBudgetExceededError ( error )
@@ -1063,7 +1066,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10631066 throw error ;
10641067 } finally {
10651068 budget . releaseRetained ( bufferBytes , { kind : "live_transient" } ) ;
1066- for ( const call of pendingToolCalls ) budget . closeCall ( call . key ) ;
1069+ closeToolCalls ( ) ;
10671070 reader . releaseLock ( ) ;
10681071 }
10691072 } ,
@@ -1097,18 +1100,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10971100 }
10981101 const rawChoice = choices [ 0 ] ;
10991102 if ( rawChoice === null || typeof rawChoice !== "object" || Array . isArray ( rawChoice ) ) {
1100- return [ { type : "error" , message : "upstream response contained invalid choices" , ... ( usage ? { usage } : { } ) } ] ;
1103+ return [ invalidChoicesEvent ( usage ) ] ;
11011104 }
11021105 const choice = rawChoice ;
11031106 if ( choice . finish_reason === "error" ) return [ upstreamErrorEvent ( choice . error , usage ) ] ;
11041107 if ( ! choice . message ) return [ { type : "error" , message : "upstream response contained no choices" , ...( usage ? { usage } : { } ) } ] ;
11051108
11061109 const msg = choice . message ;
1107- const reasoningText = typeof msg . reasoning_content === "string" && msg . reasoning_content . length > 0
1108- ? msg . reasoning_content
1109- : typeof msg . reasoning === "string" && msg . reasoning . length > 0
1110- ? msg . reasoning
1111- : undefined ;
1110+ const reasoningText = reasoningTextFrom ( msg ) ;
11121111 if ( reasoningText !== undefined ) {
11131112 events . push ( { type : "reasoning_raw_delta" , text : reasoningText } ) ;
11141113 }
@@ -1123,11 +1122,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
11231122 events . push ( { type : "tool_call_end" } ) ;
11241123 }
11251124 }
1126- const stopReason = choice . finish_reason === "length"
1127- ? "max_tokens"
1128- : choice . finish_reason === "content_filter"
1129- ? "content_filter"
1130- : undefined ;
1125+ const stopReason = stopReasonFor ( choice . finish_reason ) ;
11311126 events . push ( {
11321127 type : "done" ,
11331128 usage,
0 commit comments