@@ -360,7 +360,7 @@ where
360360 let over_budget = reasoning_meter. record( & reasoning) ;
361361 match current_thinking_idx {
362362 Some ( idx) => {
363- if let Some ( ContentBlock :: Thinking { text } ) =
363+ if let Some ( ContentBlock :: Thinking { text, .. } ) =
364364 partial. content. get_mut( idx)
365365 {
366366 text. push_str( & reasoning) ;
@@ -372,7 +372,11 @@ where
372372 }
373373 None => {
374374 current_thinking_idx = Some ( partial. content. len( ) ) ;
375- partial. content. push( ContentBlock :: Thinking { text: reasoning } ) ;
375+ partial. content. push( ContentBlock :: Thinking {
376+ text: reasoning,
377+ signature: None ,
378+ signature_model: None ,
379+ } ) ;
376380 current_text_idx = None ;
377381 yield StreamEvent :: Delta {
378382 partial: partial. clone( ) ,
@@ -420,6 +424,19 @@ where
420424 } )
421425 . collect:: <Vec <_>>( )
422426 . join( "\n " ) ;
427+ // GH #821: capture the provider-issued signature the
428+ // complete block carries (Anthropic sends it via a
429+ // `signature_delta` that rig folds into the final
430+ // `ReasoningContent::Text`). It must be echoed back
431+ // verbatim when the block is replayed, so dropping it
432+ // here — the old `Text { text, .. }` did — 400s the
433+ // first tool-use continuation with reasoning on.
434+ let signature: Option <String > = r. content. iter( ) . find_map( |c| match c {
435+ rig:: completion:: message:: ReasoningContent :: Text {
436+ signature, ..
437+ } => signature. clone( ) ,
438+ _ => None ,
439+ } ) ;
423440 // dirge-zf35: mirror the H-7 ToolCall dedupe. Some
424441 // providers stream `ReasoningDelta`s and THEN send a
425442 // complete `Reasoning` for the same content. If a
@@ -444,17 +461,32 @@ where
444461 Some ( ContentBlock :: Thinking { .. } )
445462 ) =>
446463 {
447- if let Some ( ContentBlock :: Thinking { text: acc } ) =
448- partial. content. get_mut( idx)
464+ if let Some ( ContentBlock :: Thinking {
465+ text: acc,
466+ signature: acc_signature,
467+ ..
468+ } ) = partial. content. get_mut( idx)
449469 {
450470 if acc. is_empty( ) || text. starts_with( acc. as_str( ) ) {
451471 * acc = text;
452472 } else {
453473 acc. push_str( & text) ;
454474 }
475+ // GH #821: the fold must carry the signature
476+ // too — in the Anthropic flow the text arrives
477+ // as deltas and the signature ONLY on this
478+ // complete block. Never clobber a previously
479+ // captured signature with `None`.
480+ if signature. is_some( ) {
481+ * acc_signature = signature;
482+ }
455483 }
456484 }
457- _ => partial. content. push( ContentBlock :: Thinking { text } ) ,
485+ _ => partial. content. push( ContentBlock :: Thinking {
486+ text,
487+ signature,
488+ signature_model: None ,
489+ } ) ,
458490 }
459491 current_thinking_idx = None ;
460492 current_text_idx = None ;
@@ -1153,7 +1185,7 @@ mod tests {
11531185 . content
11541186 . iter ( )
11551187 . filter_map ( |b| match b {
1156- ContentBlock :: Thinking { text } => Some ( text. clone ( ) ) ,
1188+ ContentBlock :: Thinking { text, .. } => Some ( text. clone ( ) ) ,
11571189 _ => None ,
11581190 } )
11591191 . collect ( ) ;
@@ -1207,7 +1239,7 @@ mod tests {
12071239 ) ;
12081240 match events. last ( ) . unwrap ( ) {
12091241 StreamEvent :: Done { message, .. } => {
1210- if let ContentBlock :: Thinking { text } = & message. content [ 0 ] {
1242+ if let ContentBlock :: Thinking { text, .. } = & message. content [ 0 ] {
12111243 assert_eq ! ( text, "Let me think about this" ) ;
12121244 } else {
12131245 panic ! ( "expected thinking" ) ;
@@ -1263,7 +1295,7 @@ mod tests {
12631295 . content
12641296 . iter ( )
12651297 . filter_map ( |b| match b {
1266- ContentBlock :: Thinking { text } => Some ( text) ,
1298+ ContentBlock :: Thinking { text, .. } => Some ( text) ,
12671299 _ => None ,
12681300 } )
12691301 . collect ( ) ;
@@ -1278,6 +1310,103 @@ mod tests {
12781310 }
12791311 }
12801312
1313+ /// GH #821: a complete reasoning block's signature must be captured,
1314+ /// not discarded — Anthropic requires it echoed back verbatim when
1315+ /// the block is replayed (the tool-use continuation 400s without it).
1316+ #[ tokio:: test]
1317+ async fn complete_reasoning_signature_is_captured ( ) {
1318+ let raw = raw_stream ( vec ! [ Ok ( StreamedAssistantContent :: Reasoning (
1319+ Reasoning :: new_with_signature( "All thinking" , Some ( "sig-821" . to_string( ) ) ) ,
1320+ ) ) ] ) ;
1321+ let events = drain ( wrap_streamed_assistant ( raw, None , None ) ) . await ;
1322+ match events. last ( ) . unwrap ( ) {
1323+ StreamEvent :: Done { message, .. } => match & message. content [ 0 ] {
1324+ ContentBlock :: Thinking {
1325+ text, signature, ..
1326+ } => {
1327+ assert_eq ! ( text, "All thinking" ) ;
1328+ assert_eq ! ( signature. as_deref( ) , Some ( "sig-821" ) ) ;
1329+ }
1330+ other => panic ! ( "expected thinking, got {other:?}" ) ,
1331+ } ,
1332+ _ => panic ! ( "expected Done last" ) ,
1333+ }
1334+ }
1335+
1336+ /// GH #821 + dirge-zf35: in the Anthropic flow the text arrives as
1337+ /// `ReasoningDelta`s and the signature ONLY on the trailing complete
1338+ /// `Reasoning`. The fold that merges the complete block into the open
1339+ /// delta-built block must carry the signature onto that block.
1340+ #[ tokio:: test]
1341+ async fn signature_survives_the_delta_fold ( ) {
1342+ let raw = raw_stream ( vec ! [
1343+ Ok ( StreamedAssistantContent :: ReasoningDelta {
1344+ id: None ,
1345+ reasoning: "Let me think" . to_string( ) ,
1346+ } ) ,
1347+ Ok ( StreamedAssistantContent :: ReasoningDelta {
1348+ id: None ,
1349+ reasoning: " about this" . to_string( ) ,
1350+ } ) ,
1351+ Ok ( StreamedAssistantContent :: Reasoning (
1352+ Reasoning :: new_with_signature(
1353+ "Let me think about this" ,
1354+ Some ( "sig-821" . to_string( ) ) ,
1355+ ) ,
1356+ ) ) ,
1357+ ] ) ;
1358+ let events = drain ( wrap_streamed_assistant ( raw, None , None ) ) . await ;
1359+ match events. last ( ) . unwrap ( ) {
1360+ StreamEvent :: Done { message, .. } => {
1361+ let blocks: Vec < _ > = message
1362+ . content
1363+ . iter ( )
1364+ . filter_map ( |b| match b {
1365+ ContentBlock :: Thinking {
1366+ text, signature, ..
1367+ } => Some ( ( text. as_str ( ) , signature. as_deref ( ) ) ) ,
1368+ _ => None ,
1369+ } )
1370+ . collect ( ) ;
1371+ assert_eq ! (
1372+ blocks,
1373+ vec![ ( "Let me think about this" , Some ( "sig-821" ) ) ] ,
1374+ "fold must keep one block AND adopt the complete block's signature"
1375+ ) ;
1376+ }
1377+ _ => panic ! ( "expected Done last" ) ,
1378+ }
1379+ }
1380+
1381+ /// GH #821, Gemini shape: when the complete `Reasoning` is only the
1382+ /// trailing chunk (append path of the dirge-zf35 fold), its signature
1383+ /// must still land on the accumulated block.
1384+ #[ tokio:: test]
1385+ async fn signature_survives_the_append_fold ( ) {
1386+ let raw = raw_stream ( vec ! [
1387+ Ok ( StreamedAssistantContent :: ReasoningDelta {
1388+ id: None ,
1389+ reasoning: "chunk A" . to_string( ) ,
1390+ } ) ,
1391+ Ok ( StreamedAssistantContent :: Reasoning (
1392+ Reasoning :: new_with_signature( "chunk Z" , Some ( "sig-tail" . to_string( ) ) ) ,
1393+ ) ) ,
1394+ ] ) ;
1395+ let events = drain ( wrap_streamed_assistant ( raw, None , None ) ) . await ;
1396+ match events. last ( ) . unwrap ( ) {
1397+ StreamEvent :: Done { message, .. } => match & message. content [ 0 ] {
1398+ ContentBlock :: Thinking {
1399+ text, signature, ..
1400+ } => {
1401+ assert_eq ! ( text, "chunk Achunk Z" ) ;
1402+ assert_eq ! ( signature. as_deref( ) , Some ( "sig-tail" ) ) ;
1403+ }
1404+ other => panic ! ( "expected thinking, got {other:?}" ) ,
1405+ } ,
1406+ _ => panic ! ( "expected Done last" ) ,
1407+ }
1408+ }
1409+
12811410 /// Gemini shape: non-signature thought parts stream as
12821411 /// `ReasoningDelta`s, and the complete `Reasoning` event carries
12831412 /// only the FINAL chunk (the thought_signature-bearing part), not
@@ -1305,7 +1434,7 @@ mod tests {
13051434 . content
13061435 . iter ( )
13071436 . filter_map ( |b| match b {
1308- ContentBlock :: Thinking { text } => Some ( text) ,
1437+ ContentBlock :: Thinking { text, .. } => Some ( text) ,
13091438 _ => None ,
13101439 } )
13111440 . collect ( ) ;
@@ -1344,7 +1473,7 @@ mod tests {
13441473 . content
13451474 . iter ( )
13461475 . filter_map ( |b| match b {
1347- ContentBlock :: Thinking { text } => Some ( text) ,
1476+ ContentBlock :: Thinking { text, .. } => Some ( text) ,
13481477 _ => None ,
13491478 } )
13501479 . collect ( ) ;
@@ -1413,7 +1542,7 @@ mod tests {
14131542 . content
14141543 . iter ( )
14151544 . filter_map ( |b| match b {
1416- ContentBlock :: Thinking { text } => Some ( text. as_str ( ) ) ,
1545+ ContentBlock :: Thinking { text, .. } => Some ( text. as_str ( ) ) ,
14171546 _ => None ,
14181547 } )
14191548 . collect :: < Vec < _ > > ( )
@@ -1835,7 +1964,7 @@ mod tests {
18351964 ) ) ;
18361965 assert ! ( matches!(
18371966 & final_msg. content[ 1 ] ,
1838- ContentBlock :: Thinking { text } if text == "thinking"
1967+ ContentBlock :: Thinking { text, .. } if text == "thinking"
18391968 ) ) ;
18401969 assert ! ( matches!(
18411970 & final_msg. content[ 2 ] ,
0 commit comments