@@ -29,6 +29,7 @@ import {
2929 type FailoverConfig ,
3030} from "@/services/failover" ;
3131import {
32+ acceptsEventStream ,
3233 extractUpstreamHeaders ,
3334 filterCandidates ,
3435 extractContentText ,
@@ -569,6 +570,12 @@ export const completionsApi = new Elysia({
569570 // Extract extra headers for passthrough
570571 const extraHeaders = extractUpstreamHeaders ( reqHeaders ) ;
571572
573+ // Determine streaming mode. Body wins when explicit; otherwise honor
574+ // the client's Accept: text/event-stream negotiation hint.
575+ if ( body . stream === undefined && acceptsEventStream ( reqHeaders ) ) {
576+ body . stream = true ;
577+ }
578+
572579 // Check ReqId for deduplication (if provided)
573580 const isStream = body . stream === true ;
574581 const reqIdResult = await checkReqId ( reqId , {
@@ -718,31 +725,51 @@ export const completionsApi = new Elysia({
718725 extraHeaders ,
719726 ) ;
720727
721- // Return an async generator for streaming
728+ // Return a native Response with proper SSE headers. Wrapping the
729+ // pre-formatted SSE generator in a ReadableStream ensures Elysia
730+ // skips its auto SSE-wrapping (which would double-prefix `data:`)
731+ // and lets us set Content-Type: text/event-stream explicitly.
722732 const streamResponse = result . response ;
723733 const streamSignal = request . signal ;
724- return ( async function * ( ) {
725- try {
726- yield * processStreamingResponse (
727- streamResponse ,
728- completion ,
729- bearer ,
730- providerType ,
731- apiKeyRecord ?? null ,
732- begin ,
733- streamSignal ,
734- reqIdContext ?? undefined ,
735- ) ;
736- } catch ( error ) {
737- // Don't log error if it's due to client abort
738- if ( ! streamSignal . aborted ) {
739- logger . error ( "Stream processing error" , error ) ;
740- // Note: HTTP status cannot be changed after streaming has started
741- // Use SSE format for error: data: {...}\n\n
742- yield `data: ${ JSON . stringify ( { error : { message : "Stream processing error" , type : "server_error" , code : "stream_error" } } ) } \n\n` ;
734+ const sseStream = new ReadableStream < Uint8Array > ( {
735+ async start ( controller ) {
736+ const encoder = new TextEncoder ( ) ;
737+ try {
738+ for await ( const chunk of processStreamingResponse (
739+ streamResponse ,
740+ completion ,
741+ bearer ,
742+ providerType ,
743+ apiKeyRecord ?? null ,
744+ begin ,
745+ streamSignal ,
746+ reqIdContext ?? undefined ,
747+ ) ) {
748+ controller . enqueue ( encoder . encode ( chunk ) ) ;
749+ }
750+ } catch ( error ) {
751+ if ( ! streamSignal . aborted ) {
752+ logger . error ( "Stream processing error" , error ) ;
753+ controller . enqueue (
754+ encoder . encode (
755+ `data: ${ JSON . stringify ( { error : { message : "Stream processing error" , type : "server_error" , code : "stream_error" } } ) } \n\n` ,
756+ ) ,
757+ ) ;
758+ }
759+ } finally {
760+ controller . close ( ) ;
743761 }
744- }
745- } ) ( ) ;
762+ } ,
763+ } ) ;
764+
765+ return new Response ( sseStream , {
766+ status : 200 ,
767+ headers : {
768+ "Content-Type" : "text/event-stream; charset=utf-8" ,
769+ "Cache-Control" : "no-cache" ,
770+ Connection : "keep-alive" ,
771+ } ,
772+ } ) ;
746773 } else {
747774 // Non-streaming request - return JSON response directly
748775 const result = await executeWithFailover (
0 commit comments