@@ -303,18 +303,65 @@ pub async fn generate_streaming(
303303
304304 let cancel_token = CancellationToken :: new ( ) ;
305305 let mut rx = client
306- . complete_streaming_structured ( & messages, Some ( & system) , & tools, & directive, cancel_token)
306+ . complete_streaming_structured (
307+ & messages,
308+ Some ( & system) ,
309+ & tools,
310+ & directive,
311+ cancel_token. clone ( ) ,
312+ )
307313 . await
308314 . context ( "LLM streaming call failed during structured generation" ) ?;
309315
310316 let mut json_buffer = String :: new ( ) ;
311317 let mut last_valid_partial: Option < Value > = None ;
312318 let mut final_response: Option < super :: LlmResponse > = None ;
313319 let mut last_parse_len: usize = 0 ;
320+ let mut complete_candidate: Option < ( Value , String , tokio:: time:: Instant ) > = None ;
314321 // Minimum bytes of new data before attempting a partial parse (reduces CPU)
315322 const PARSE_THRESHOLD : usize = 8 ;
316-
317- while let Some ( event) = rx. recv ( ) . await {
323+ // Well-behaved providers send Done immediately after the complete object.
324+ // A short grace preserves their final usage metadata while preventing an
325+ // otherwise valid result from hanging on a compatible endpoint that never
326+ // terminates its stream.
327+ const DONE_GRACE : std:: time:: Duration = std:: time:: Duration :: from_millis ( 250 ) ;
328+ loop {
329+ let event = if let Some ( ( _, _, deadline) ) = complete_candidate. as_ref ( ) {
330+ tokio:: select! {
331+ event = rx. recv( ) => event,
332+ _ = tokio:: time:: sleep_until( * deadline) => {
333+ let candidate = complete_candidate
334+ . take( )
335+ . expect( "complete streamed candidate exists" ) ;
336+ let ( value, raw_text, _) = candidate;
337+ cancel_token. cancel( ) ;
338+ on_partial( & value) ;
339+ return Ok ( StructuredResult {
340+ object: value,
341+ raw_text: Some ( raw_text) ,
342+ usage: TokenUsage :: default ( ) ,
343+ repair_rounds: 0 ,
344+ mode_used: mode,
345+ } ) ;
346+ }
347+ }
348+ } else {
349+ rx. recv ( ) . await
350+ } ;
351+ let Some ( event) = event else {
352+ if let Some ( ( value, raw_text, _) ) = complete_candidate. take ( ) {
353+ cancel_token. cancel ( ) ;
354+ on_partial ( & value) ;
355+ return Ok ( StructuredResult {
356+ object : value,
357+ raw_text : Some ( raw_text) ,
358+ usage : TokenUsage :: default ( ) ,
359+ repair_rounds : 0 ,
360+ mode_used : mode,
361+ } ) ;
362+ }
363+ break ;
364+ } ;
318365 match event {
319366 StreamEvent :: ToolUseInputDelta { delta, .. } if mode == StructuredMode :: Tool => {
320367 if final_response. is_some ( ) {
@@ -334,6 +381,17 @@ pub async fn generate_streaming(
334381 }
335382 last_parse_len = json_buffer. len ( ) ;
336383 }
384+ if complete_candidate. is_none ( ) && ( delta. contains ( '}' ) || delta. contains ( ']' ) ) {
385+ complete_candidate = resolve_structured (
386+ std:: slice:: from_ref ( & json_buffer) ,
387+ & req. schema ,
388+ envelope,
389+ )
390+ . valid
391+ . map ( |( value, raw_text) | {
392+ ( value, raw_text, tokio:: time:: Instant :: now ( ) + DONE_GRACE )
393+ } ) ;
394+ }
337395 }
338396 StreamEvent :: TextDelta ( delta) if mode != StructuredMode :: Tool => {
339397 if final_response. is_some ( ) {
@@ -356,9 +414,21 @@ pub async fn generate_streaming(
356414 }
357415 last_parse_len = json_buffer. len ( ) ;
358416 }
417+ if complete_candidate. is_none ( ) && ( delta. contains ( '}' ) || delta. contains ( ']' ) ) {
418+ complete_candidate = resolve_structured (
419+ std:: slice:: from_ref ( & json_buffer) ,
420+ & req. schema ,
421+ envelope,
422+ )
423+ . valid
424+ . map ( |( value, raw_text) | {
425+ ( value, raw_text, tokio:: time:: Instant :: now ( ) + DONE_GRACE )
426+ } ) ;
427+ }
359428 }
360429 StreamEvent :: Done ( resp) => {
361430 final_response = Some ( resp) ;
431+ break ;
362432 }
363433 _ => { }
364434 }
0 commit comments