@@ -118,35 +118,64 @@ export async function* encodeVercelDataStream(
118118
119119 let textOpen = true ;
120120 let finishReason = 'stop' ;
121+ let errorMessage : string | undefined ;
122+
123+ try {
124+ for await ( const part of events ) {
125+ // Surface error parts emitted by the underlying provider stream.
126+ if ( ( part as { type : string } ) . type === 'error' ) {
127+ const errPart = part as unknown as { error ?: unknown } ;
128+ const raw = errPart . error ;
129+ errorMessage =
130+ ( raw && typeof raw === 'object' && 'message' in raw
131+ ? String ( ( raw as { message : unknown } ) . message )
132+ : typeof raw === 'string'
133+ ? raw
134+ : 'Unknown provider error' ) ;
135+ finishReason = 'error' ;
136+ break ;
137+ }
121138
122- for await ( const part of events ) {
123- // Capture finish reason
124- if ( part . type === 'finish' ) {
125- finishReason = part . finishReason ?? 'stop' ;
126- }
139+ // Capture finish reason
140+ if ( part . type === 'finish' ) {
141+ finishReason = part . finishReason ?? 'stop' ;
142+ }
127143
128- // Before finish-step/finish, close the text part first
129- if ( part . type === 'finish-step' || part . type === 'finish' ) {
130- if ( textOpen ) {
131- yield sse ( { type : 'text-end' , id : '0' } ) ;
132- textOpen = false ;
144+ // Before finish-step/finish, close the text part first
145+ if ( part . type === 'finish-step' || part . type === 'finish' ) {
146+ if ( textOpen ) {
147+ yield sse ( { type : 'text-end' , id : '0' } ) ;
148+ textOpen = false ;
149+ }
150+ // Don't emit these via encodeStreamPart — we handle them in postamble
151+ continue ;
133152 }
134- // Don't emit these via encodeStreamPart — we handle them in postamble
135- continue ;
136- }
137153
138- const frame = encodeStreamPart ( part ) ;
139- if ( frame ) {
140- yield frame ;
154+ const frame = encodeStreamPart ( part ) ;
155+ if ( frame ) {
156+ yield frame ;
157+ }
141158 }
159+ } catch ( err ) {
160+ // Upstream provider threw (auth failure, network error, etc.). Without
161+ // this catch the SSE response would hang half-open and the client would
162+ // never leave its "streaming" state.
163+ errorMessage = err instanceof Error ? err . message : String ( err ) ;
164+ finishReason = 'error' ;
142165 }
143166
144167 // Close text if still open (safety)
145168 if ( textOpen ) {
146169 yield sse ( { type : 'text-end' , id : '0' } ) ;
147170 }
148171
149- // Postamble
172+ // If we recorded an error, emit it as a UI Message Stream `error` part so
173+ // the client can display it instead of spinning forever.
174+ if ( errorMessage ) {
175+ yield sse ( { type : 'error' , errorText : errorMessage } ) ;
176+ }
177+
178+ // Postamble — always emit so the client transitions out of "streaming".
150179 yield sse ( { type : 'finish-step' } ) ;
151180 yield sse ( { type : 'finish' , finishReason } ) ;
152181 yield 'data: [DONE]\n\n' ;
0 commit comments