@@ -4,9 +4,17 @@ import { createAnthropicAdapter } from "./adapters/anthropic";
44import { createAzureAdapter } from "./adapters/azure" ;
55import { createGoogleAdapter } from "./adapters/google" ;
66import { createOpenAIChatAdapter } from "./adapters/openai-chat" ;
7- import { createResponsesPassthroughAdapter , FORWARD_HEADERS } from "./adapters/openai-responses" ;
7+ import { createResponsesPassthroughAdapter } from "./adapters/openai-responses" ;
88import { bridgeToResponsesSSE , buildResponseJSON , formatErrorResponse } from "./bridge" ;
9- import { buildWarmupCompletionFrames , pumpSseToWebSocket , type WsData } from "./ws-bridge" ;
9+ import {
10+ buildWarmupCompletionFrames ,
11+ buildWsErrorFrame ,
12+ selectForwardHeaders ,
13+ sendJsonFrame ,
14+ sendResponseToWebSocket ,
15+ sendTextFrame ,
16+ type WsData ,
17+ } from "./ws-bridge" ;
1018import type { ServerWebSocket } from "bun" ;
1119import { DEFAULT_SUBAGENT_MODELS , loadConfig , saveConfig , websocketsEnabled } from "./config" ;
1220import { parseRequest } from "./responses/parser" ;
@@ -88,7 +96,12 @@ export function resolveAdapter(providerConfig: OcxProviderConfig) {
8896 }
8997}
9098
91- async function handleResponses ( req : Request , config : OcxConfig , logCtx : { model : string ; provider : string } ) : Promise < Response > {
99+ async function handleResponses (
100+ req : Request ,
101+ config : OcxConfig ,
102+ logCtx : { model : string ; provider : string } ,
103+ options : { forceEmptyResponseId ?: boolean } = { } ,
104+ ) : Promise < Response > {
92105 let body : unknown ;
93106 try {
94107 body = await req . json ( ) ;
@@ -215,7 +228,16 @@ async function handleResponses(req: Request, config: OcxConfig, logCtx: { model:
215228 if ( t . freeform ) freeformToolNames . add ( t . name ) ;
216229 if ( t . toolSearch ) toolSearchToolNames . add ( t . name ) ;
217230 }
218- const sseStream = bridgeToResponsesSSE ( eventStream , parsed . modelId , toolNsMap , freeformToolNames , toolSearchToolNames , ( ) => upstream . abort ( ) ) ;
231+ const sseStream = bridgeToResponsesSSE (
232+ eventStream ,
233+ parsed . modelId ,
234+ toolNsMap ,
235+ freeformToolNames ,
236+ toolSearchToolNames ,
237+ ( ) => upstream . abort ( ) ,
238+ 2_000 ,
239+ options . forceEmptyResponseId ? { responseId : "" } : undefined ,
240+ ) ;
219241 return new Response ( sseStream , {
220242 headers : {
221243 "Content-Type" : "text/event-stream" ,
@@ -527,7 +549,7 @@ export function startServer(port?: number) {
527549 // Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
528550 // handshake-time only, so capture inbound headers and thread them into the pipeline.
529551 if ( url . pathname === "/v1/responses" && req . headers . get ( "upgrade" ) ?. toLowerCase ( ) === "websocket" ) {
530- if ( server . upgrade ( req , { data : { headers : req . headers } } ) ) return undefined as unknown as Response ;
552+ if ( server . upgrade ( req , { data : { headers : selectForwardHeaders ( req . headers ) } } ) ) return undefined as unknown as Response ;
531553 return formatErrorResponse ( 426 , "upgrade_required" , "WebSocket upgrade failed" ) ;
532554 }
533555
@@ -584,7 +606,7 @@ export function startServer(port?: number) {
584606 // Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
585607 // socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
586608 // Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
587- async message ( ws : ServerWebSocket < WsData > , raw : string | Buffer ) {
609+ message ( ws : ServerWebSocket < WsData > , raw : string | Buffer ) {
588610 let frame : Record < string , unknown > ;
589611 try {
590612 frame = JSON . parse ( typeof raw === "string" ? raw : raw . toString ( ) ) as Record < string , unknown > ;
@@ -593,43 +615,46 @@ export function startServer(port?: number) {
593615 }
594616 if ( frame . type === "response.processed" ) return ; // ack — no-op
595617 if ( frame . type !== "response.create" ) return ;
618+
619+ ws . data . cancel ?.( ) ;
620+ const turnId = ( ws . data . turnId ?? 0 ) + 1 ;
621+ ws . data . turnId = turnId ;
622+ const isCurrent = ( ) => ws . data . turnId === turnId ;
623+
596624 if ( frame . generate === false ) {
597625 for ( const payload of buildWarmupCompletionFrames ( frame ) ) {
598- if ( ws . readyState === 1 ) ws . send ( payload ) ;
626+ if ( ! isCurrent ( ) ) return ;
627+ sendTextFrame ( ws , payload ) ;
599628 }
600629 return ;
601630 }
631+
602632 const payload : Record < string , unknown > = { ...frame } ;
603633 delete payload . type ;
604- const logCtx = { model : "unknown" , provider : "unknown" } ;
605- const fwd = new Headers ( { "content-type" : "application/json" } ) ;
606- for ( const h of FORWARD_HEADERS ) {
607- const v = ws . data . headers ?. get ( h ) ;
608- if ( v ) fwd . set ( h , v ) ; // native passthrough auth is handshake-time only
609- }
610- const req = new Request ( "http://localhost/v1/responses" , {
611- method : "POST" ,
612- headers : fwd ,
613- body : JSON . stringify ( { ...payload , stream : true } ) ,
614- } ) ;
615- const response = await handleResponses ( req , config , logCtx ) ;
616- if ( response . ok && response . body ) {
617- await pumpSseToWebSocket ( ws , response . body ) ;
618- return ;
619- }
620- // Non-SSE (error or non-stream) → wrap as a terminal WS frame.
621- const text = await response . text ( ) ;
622- try {
623- const json = JSON . parse ( text ) as Record < string , unknown > ;
624- if ( response . ok ) {
625- if ( ws . readyState === 1 ) ws . send ( JSON . stringify ( { type : "response.completed" , response : json } ) ) ;
626- return ;
634+ void ( async ( ) => {
635+ const logCtx = { model : "unknown" , provider : "unknown" } ;
636+ const fwd = new Headers ( { "content-type" : "application/json" } ) ;
637+ ws . data . headers ?. forEach ( ( value , key ) => fwd . set ( key , value ) ) ;
638+ const req = new Request ( "http://localhost/v1/responses" , {
639+ method : "POST" ,
640+ headers : fwd ,
641+ body : JSON . stringify ( { ...payload , stream : true } ) ,
642+ } ) ;
643+ try {
644+ const response = await handleResponses ( req , config , logCtx , { forceEmptyResponseId : true } ) ;
645+ await sendResponseToWebSocket ( ws , response , isCurrent ) ;
646+ } catch ( err ) {
647+ if ( ! isCurrent ( ) ) return ;
648+ try {
649+ sendJsonFrame ( ws , buildWsErrorFrame ( 502 , {
650+ type : "proxy_error" ,
651+ message : err instanceof Error ? err . message : String ( err ) ,
652+ } ) ) ;
653+ } catch {
654+ /* socket already gone or send dropped */
655+ }
627656 }
628- const error = ( json . error as Record < string , unknown > ) ?? { message : text . slice ( 0 , 500 ) } ;
629- if ( ws . readyState === 1 ) ws . send ( JSON . stringify ( { type : "response.failed" , response : { status : "failed" , error } } ) ) ;
630- } catch {
631- if ( ws . readyState === 1 ) ws . send ( JSON . stringify ( { type : "response.failed" , response : { status : "failed" , error : { message : text . slice ( 0 , 500 ) } } } ) ) ;
632- }
657+ } ) ( ) ;
633658 } ,
634659 close ( ws : ServerWebSocket < WsData > ) {
635660 ws . data . cancel ?.( ) ; // RC2: abort the upstream when the client disconnects
0 commit comments