@@ -242,34 +242,40 @@ describe("buildGuardedModelFetch", () => {
242242 expect ( release ) . toHaveBeenCalled ( ) ;
243243 } ) ;
244244
245- it ( "allows missing content-type when streamed OpenAI-compatible responses contain SSE" , async ( ) => {
246- fetchWithSsrFGuardMock . mockResolvedValue ( {
247- response : new Response ( responseStreamText ( 'data: {"ok": true}\n\ndata: [DONE]\n\n' ) ) ,
248- finalUrl : "https://chatgpt.com/backend-api/codex/responses" ,
249- release : vi . fn ( async ( ) => undefined ) ,
250- } ) ;
251- const model = {
252- id : "gpt-5.5" ,
253- provider : "openai" ,
254- api : "openclaw-openai-responses-transport" ,
255- baseUrl : "https://chatgpt.com/backend-api/codex" ,
256- } as unknown as Model < "openai-responses" > ;
245+ it . each ( [
246+ [ "direct" , "openai-chatgpt-responses" ] ,
247+ [ "transport alias" , "openclaw-openai-responses-transport" ] ,
248+ ] as const ) (
249+ "allows missing content-type when native ChatGPT/Codex Responses streams contain SSE through %s API" ,
250+ async ( _label , api ) => {
251+ fetchWithSsrFGuardMock . mockResolvedValue ( {
252+ response : new Response ( responseStreamText ( 'data: {"ok": true}\n\ndata: [DONE]\n\n' ) ) ,
253+ finalUrl : "https://chatgpt.com/backend-api/codex/responses" ,
254+ release : vi . fn ( async ( ) => undefined ) ,
255+ } ) ;
256+ const model = {
257+ id : "gpt-5.5" ,
258+ provider : "openai" ,
259+ api,
260+ baseUrl : "https://chatgpt.com/backend-api/codex" ,
261+ } as unknown as Model < "openai-responses" > ;
257262
258- const response = await buildGuardedModelFetch ( model ) (
259- "https://chatgpt.com/backend-api/codex/responses" ,
260- {
261- method : "POST" ,
262- headers : { "content-type" : "application/json" } ,
263- body : JSON . stringify ( { model : "gpt-5.5" , stream : true } ) ,
264- } ,
265- ) ;
266- const items = [ ] ;
267- for await ( const item of Stream . fromSSEResponse ( response , new AbortController ( ) ) ) {
268- items . push ( item ) ;
269- }
263+ const response = await buildGuardedModelFetch ( model ) (
264+ "https://chatgpt.com/backend-api/codex/responses" ,
265+ {
266+ method : "POST" ,
267+ headers : { "content-type" : "application/json" } ,
268+ body : JSON . stringify ( { model : "gpt-5.5" , stream : true } ) ,
269+ } ,
270+ ) ;
271+ const items = [ ] ;
272+ for await ( const item of Stream . fromSSEResponse ( response , new AbortController ( ) ) ) {
273+ items . push ( item ) ;
274+ }
270275
271- expect ( items ) . toEqual ( [ { ok : true } ] ) ;
272- } ) ;
276+ expect ( items ) . toEqual ( [ { ok : true } ] ) ;
277+ } ,
278+ ) ;
273279
274280 it ( "returns promptly for missing content-type SSE streams that remain open" , async ( ) => {
275281 const source = openResponseStreamText ( 'data: {"ok": true}\n\n' ) ;
@@ -341,11 +347,12 @@ describe("buildGuardedModelFetch", () => {
341347 expect ( items ) . toEqual ( [ { ok : true } ] ) ;
342348 } ) ;
343349
344- it ( "synthesizes SSE for missing content-type JSON returned to streaming SDK requests" , async ( ) => {
350+ it ( "rejects missing content-type JSON-like bodies returned to native ChatGPT/Codex streaming SDK requests" , async ( ) => {
351+ const release = vi . fn ( async ( ) => undefined ) ;
345352 fetchWithSsrFGuardMock . mockResolvedValue ( {
346353 response : new Response ( responseStreamText ( '{"ok": true}' ) ) ,
347354 finalUrl : "https://chatgpt.com/backend-api/codex/responses" ,
348- release : vi . fn ( async ( ) => undefined ) ,
355+ release,
349356 } ) ;
350357 const model = {
351358 id : "gpt-5.5" ,
@@ -354,21 +361,94 @@ describe("buildGuardedModelFetch", () => {
354361 baseUrl : "https://chatgpt.com/backend-api/codex" ,
355362 } as unknown as Model < "openai-responses" > ;
356363
357- const response = await buildGuardedModelFetch ( model ) (
358- "https://chatgpt.com/backend-api/codex/responses" ,
359- {
364+ await expect (
365+ buildGuardedModelFetch ( model ) ( "https://chatgpt.com/backend-api/codex/responses" , {
360366 method : "POST" ,
361367 headers : { "content-type" : "application/json" } ,
362368 body : JSON . stringify ( { model : "gpt-5.5" , stream : true } ) ,
363- } ,
364- ) ;
365- const items = [ ] ;
366- for await ( const item of Stream . fromSSEResponse ( response , new AbortController ( ) ) ) {
367- items . push ( item ) ;
368- }
369+ } ) ,
370+ ) . rejects . toMatchObject ( {
371+ name : "ProviderHttpError" ,
372+ status : 200 ,
373+ code : "invalid_provider_content_type" ,
374+ errorType : "invalid_response" ,
375+ } ) ;
376+ expect ( release ) . toHaveBeenCalled ( ) ;
377+ } ) ;
369378
370- expect ( response . headers . get ( "content-type" ) ) . toContain ( "text/event-stream" ) ;
371- expect ( items ) . toEqual ( [ { ok : true } ] ) ;
379+ it . each ( [
380+ [ "non-native provider" , "openrouter" , "openai-responses" , "https://openrouter.ai/api/v1" ] ,
381+ [
382+ "wrong base URL" ,
383+ "openai" ,
384+ "openclaw-openai-responses-transport" ,
385+ "https://api.openai.com/v1" ,
386+ ] ,
387+ [
388+ "insecure native URL" ,
389+ "openai" ,
390+ "openclaw-openai-responses-transport" ,
391+ "http://chatgpt.com/backend-api/codex" ,
392+ ] ,
393+ ] as const ) (
394+ "rejects missing content-type SSE outside native ChatGPT/Codex Responses: %s" ,
395+ async ( _label , provider , api , baseUrl ) => {
396+ const release = vi . fn ( async ( ) => undefined ) ;
397+ const model = {
398+ id : "gpt-5.4" ,
399+ provider,
400+ api,
401+ baseUrl,
402+ } as unknown as Model < "openai-responses" > ;
403+ fetchWithSsrFGuardMock . mockResolvedValue ( {
404+ response : new Response ( responseStreamText ( 'data: {"ok": true}\n\ndata: [DONE]\n\n' ) ) ,
405+ finalUrl : `${ baseUrl . replace ( / \/ + $ / u, "" ) } /responses` ,
406+ release,
407+ } ) ;
408+
409+ await expect (
410+ buildGuardedModelFetch ( model ) ( `${ baseUrl . replace ( / \/ + $ / u, "" ) } /responses` , {
411+ method : "POST" ,
412+ headers : { "content-type" : "application/json" } ,
413+ body : JSON . stringify ( { model : "gpt-5.4" , stream : true } ) ,
414+ } ) ,
415+ ) . rejects . toMatchObject ( {
416+ name : "ProviderHttpError" ,
417+ status : 200 ,
418+ code : "invalid_provider_content_type" ,
419+ errorType : "invalid_response" ,
420+ } ) ;
421+ expect ( release ) . toHaveBeenCalled ( ) ;
422+ } ,
423+ ) ;
424+
425+ it ( "rejects missing content-type streamed OpenAI-compatible responses with unknown bodies" , async ( ) => {
426+ const release = vi . fn ( async ( ) => undefined ) ;
427+ const model = {
428+ id : "gpt-5.5" ,
429+ provider : "openai" ,
430+ api : "openclaw-openai-responses-transport" ,
431+ baseUrl : "https://chatgpt.com/backend-api/codex" ,
432+ } as unknown as Model < "openai-responses" > ;
433+ fetchWithSsrFGuardMock . mockResolvedValue ( {
434+ response : new Response ( responseStreamText ( "not-sse" ) ) ,
435+ finalUrl : "https://chatgpt.com/backend-api/codex/responses" ,
436+ release,
437+ } ) ;
438+
439+ await expect (
440+ buildGuardedModelFetch ( model ) ( "https://chatgpt.com/backend-api/codex/responses" , {
441+ method : "POST" ,
442+ headers : { "content-type" : "application/json" } ,
443+ body : JSON . stringify ( { model : "gpt-5.5" , stream : true } ) ,
444+ } ) ,
445+ ) . rejects . toMatchObject ( {
446+ name : "ProviderHttpError" ,
447+ status : 200 ,
448+ code : "invalid_provider_content_type" ,
449+ errorType : "invalid_response" ,
450+ } ) ;
451+ expect ( release ) . toHaveBeenCalled ( ) ;
372452 } ) ;
373453
374454 it ( "rejects missing content-type streamed OpenAI-compatible responses with HTML bodies" , async ( ) => {
0 commit comments