@@ -110,7 +110,7 @@ function createTestController() {
110110async function startTestProxy ( upstreamPort , options = { } ) {
111111 const controller = createTestController ( ) ;
112112 const runtime = await controller . createBuiltinProxyServer (
113- { host : '127.0.0.1' , port : 0 , timeoutMs : 2000 } ,
113+ { host : '127.0.0.1' , port : 0 , timeoutMs : options . timeoutMs || 2000 } ,
114114 {
115115 providerName : options . providerName || 'test' ,
116116 baseUrl : options . baseUrl || `http://127.0.0.1:${ upstreamPort } /v1` ,
@@ -167,6 +167,112 @@ test('builtin-proxy /v1/responses falls back to chat-only upstream and returns R
167167 }
168168} ) ;
169169
170+ test ( 'builtin-proxy /v1/responses falls back to chat when upstream responses times out' , async ( ) => {
171+ const sockets = new Set ( ) ;
172+ let capturedChatRequest = null ;
173+ const upstream = http . createServer ( ( req , res ) => {
174+ if ( req . url === '/v1/responses' && req . method === 'POST' ) {
175+ // Simulate gateways that accept /responses but never complete the request.
176+ return ;
177+ }
178+ if ( req . url === '/v1/chat/completions' && req . method === 'POST' ) {
179+ const chunks = [ ] ;
180+ req . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
181+ req . on ( 'end' , ( ) => {
182+ capturedChatRequest = JSON . parse ( Buffer . concat ( chunks ) . toString ( 'utf-8' ) ) ;
183+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
184+ res . end ( JSON . stringify ( {
185+ id : 'chatcmpl_after_timeout' ,
186+ model : 'gpt-test' ,
187+ choices : [ { message : { role : 'assistant' , content : 'fallback-after-timeout' } } ]
188+ } ) ) ;
189+ } ) ;
190+ return ;
191+ }
192+ res . writeHead ( 404 , { 'Content-Type' : 'application/json' } ) ;
193+ res . end ( JSON . stringify ( { error : 'not found' } ) ) ;
194+ } ) ;
195+ upstream . on ( 'connection' , ( socket ) => {
196+ sockets . add ( socket ) ;
197+ socket . on ( 'close' , ( ) => sockets . delete ( socket ) ) ;
198+ } ) ;
199+ const { port : upstreamPort } = await listen ( upstream ) ;
200+ let proxyRuntime = null ;
201+
202+ try {
203+ proxyRuntime = await startTestProxy ( upstreamPort , { timeoutMs : 1000 } ) ;
204+ const proxyPort = proxyRuntime . server . address ( ) . port ;
205+ const resp = await requestText ( `http://127.0.0.1:${ proxyPort } /v1/responses` , {
206+ method : 'POST' ,
207+ headers : { 'Content-Type' : 'application/json' } ,
208+ body : { model : 'gpt-test' , input : 'ping' , stream : false }
209+ } ) ;
210+ assert . equal ( resp . status , 200 ) ;
211+ assert . ok ( capturedChatRequest , 'chat fallback should run after responses timeout' ) ;
212+ assert . equal ( capturedChatRequest . stream , false ) ;
213+ const parsed = JSON . parse ( resp . text ) ;
214+ assert . equal ( parsed . output [ 0 ] . content [ 0 ] . text , 'fallback-after-timeout' ) ;
215+ } finally {
216+ if ( proxyRuntime ) {
217+ await closeServer ( proxyRuntime . server , proxyRuntime . connections ) ;
218+ }
219+ await closeServer ( upstream , sockets ) ;
220+ }
221+ } ) ;
222+
223+ test ( 'builtin-proxy /v1/responses stream=true streams chat fallback as Responses SSE' , async ( ) => {
224+ let capturedChatRequest = null ;
225+ const upstream = http . createServer ( ( req , res ) => {
226+ if ( req . url === '/v1/responses' && req . method === 'POST' ) {
227+ res . writeHead ( 404 , { 'Content-Type' : 'application/json' } ) ;
228+ res . end ( JSON . stringify ( { error : 'responses endpoint unavailable' } ) ) ;
229+ return ;
230+ }
231+ if ( req . url === '/v1/chat/completions' && req . method === 'POST' ) {
232+ const chunks = [ ] ;
233+ req . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
234+ req . on ( 'end' , ( ) => {
235+ capturedChatRequest = JSON . parse ( Buffer . concat ( chunks ) . toString ( 'utf-8' ) ) ;
236+ res . writeHead ( 200 , { 'Content-Type' : 'text/event-stream; charset=utf-8' } ) ;
237+ res . write ( 'data: {"id":"chatcmpl_stream","model":"gpt-test","choices":[{"delta":{"role":"assistant"}}]}\n\n' ) ;
238+ res . write ( 'data: {"id":"chatcmpl_stream","model":"gpt-test","choices":[{"delta":{"content":"hello"}}]}\n\n' ) ;
239+ res . write ( 'data: {"id":"chatcmpl_stream","model":"gpt-test","choices":[{"delta":{"content":"-stream"}}]}\n\n' ) ;
240+ res . end ( 'data: [DONE]\n\n' ) ;
241+ } ) ;
242+ return ;
243+ }
244+ res . writeHead ( 404 , { 'Content-Type' : 'application/json' } ) ;
245+ res . end ( JSON . stringify ( { error : 'not found' } ) ) ;
246+ } ) ;
247+ const { port : upstreamPort } = await listen ( upstream ) ;
248+ let proxyRuntime = null ;
249+
250+ try {
251+ proxyRuntime = await startTestProxy ( upstreamPort ) ;
252+ const proxyPort = proxyRuntime . server . address ( ) . port ;
253+ const sse = await requestText ( `http://127.0.0.1:${ proxyPort } /v1/responses` , {
254+ method : 'POST' ,
255+ headers : { 'Content-Type' : 'application/json' } ,
256+ body : { model : 'gpt-test' , input : 'ping' , stream : true }
257+ } ) ;
258+ assert . equal ( sse . status , 200 ) ;
259+ assert . ok ( capturedChatRequest , 'streaming chat fallback should be called' ) ;
260+ assert . equal ( capturedChatRequest . stream , true ) ;
261+ assert . match ( sse . headers [ 'content-type' ] , / t e x t \/ e v e n t - s t r e a m / i) ;
262+ assert . match ( sse . text , / e v e n t : r e s p o n s e \. c r e a t e d / ) ;
263+ assert . match ( sse . text , / e v e n t : r e s p o n s e \. o u t p u t _ t e x t \. d e l t a / ) ;
264+ assert . match ( sse . text , / " d e l t a " : " h e l l o " / ) ;
265+ assert . match ( sse . text , / " d e l t a " : " - s t r e a m " / ) ;
266+ assert . match ( sse . text , / e v e n t : r e s p o n s e \. c o m p l e t e d / ) ;
267+ assert . match ( sse . text , / d a t a : \[ D O N E \] / ) ;
268+ } finally {
269+ if ( proxyRuntime ) {
270+ await closeServer ( proxyRuntime . server , proxyRuntime . connections ) ;
271+ }
272+ await closeServer ( upstream ) ;
273+ }
274+ } ) ;
275+
170276test ( 'builtin-proxy /v1/responses stream=true returns SSE wrapper with done sentinel' , async ( ) => {
171277 const upstream = http . createServer ( ( req , res ) => {
172278 if ( req . url === '/v1/responses' && req . method === 'POST' ) {
0 commit comments