@@ -554,51 +554,55 @@ fn decode_anthropic_content_block(
554554 content: decode_tool_result_content( block. get( "content" ) . unwrap_or( & Value :: Null ) ) ,
555555 is_error: block. get( "is_error" ) . and_then( Value :: as_bool) ,
556556 } ) ] ,
557- Some ( "image" ) => {
558- let source = block
559- . get ( "source" )
560- . cloned ( )
561- . map ( ImageSource :: Raw )
562- . unwrap_or_else ( || ImageSource :: Raw ( Value :: Object ( block. clone ( ) ) ) ) ;
563- vec ! [ ContentBlock :: Image { source } ]
564- }
557+ Some ( "image" ) => vec ! [ ContentBlock :: Image {
558+ source: ImageSource :: Raw ( Value :: Object ( block. clone( ) ) ) ,
559+ } ] ,
565560 Some ( "input_image" ) | Some ( "image_url" ) => decode_image_source ( block)
566561 . map ( |source| vec ! [ ContentBlock :: Image { source } ] )
567562 . unwrap_or_default ( ) ,
568563 Some ( "input_file" ) | Some ( "file" ) => vec ! [ ContentBlock :: File {
569564 source: decode_file_source( block) ,
570565 } ] ,
566+ Some ( "document" ) => vec ! [ ContentBlock :: File {
567+ source: decode_anthropic_file_source( block) ,
568+ } ] ,
571569 _ => vec ! [ ContentBlock :: Unknown {
572570 provider: WireFormat :: AnthropicMessages . into( ) ,
573571 raw: Value :: Object ( block. clone( ) ) ,
574572 } ] ,
575573 } )
576574}
577575
578- // Converts Anthropic tool-result content into text-like IR blocks .
576+ // Preserves supported Anthropic tool-result blocks in the neutral IR .
579577fn decode_tool_result_content ( value : & Value ) -> Vec < ContentBlock > {
580578 match value {
581579 Value :: String ( text) => vec ! [ ContentBlock :: Text { text: text. clone( ) } ] ,
582580 Value :: Array ( blocks) => {
583- let mut text = Vec :: new ( ) ;
581+ let mut content = Vec :: new ( ) ;
584582 for block in blocks {
585583 if let Some ( block) = block. as_object ( ) {
586- if block. get ( "type" ) . and_then ( Value :: as_str) == Some ( "text" ) {
587- text. push (
588- block
584+ match block. get ( "type" ) . and_then ( Value :: as_str) {
585+ Some ( " text" ) => content . push ( ContentBlock :: Text {
586+ text : block
589587 . get ( "text" )
590588 . and_then ( Value :: as_str)
591589 . unwrap_or_default ( )
592590 . to_string ( ) ,
593- ) ;
594- } else {
595- text. push ( json_string ( & Value :: Object ( block. clone ( ) ) ) ) ;
591+ } ) ,
592+ Some ( "image" ) => content. push ( ContentBlock :: Image {
593+ source : ImageSource :: Raw ( Value :: Object ( block. clone ( ) ) ) ,
594+ } ) ,
595+ Some ( "document" ) => content. push ( ContentBlock :: File {
596+ source : decode_anthropic_file_source ( block) ,
597+ } ) ,
598+ _ => content. push ( ContentBlock :: Unknown {
599+ provider : WireFormat :: AnthropicMessages . into ( ) ,
600+ raw : Value :: Object ( block. clone ( ) ) ,
601+ } ) ,
596602 }
597603 }
598604 }
599- vec ! [ ContentBlock :: Text {
600- text: text. join( " " ) ,
601- } ]
605+ content
602606 }
603607 Value :: Null => vec ! [ ContentBlock :: Text {
604608 text: String :: new( ) ,
@@ -609,6 +613,11 @@ fn decode_tool_result_content(value: &Value) -> Vec<ContentBlock> {
609613 }
610614}
611615
616+ // Keeps Anthropic document fields together for same-format re-encoding.
617+ fn decode_anthropic_file_source ( block : & Map < String , Value > ) -> FileSource {
618+ FileSource :: Raw ( Value :: Object ( block. clone ( ) ) )
619+ }
620+
612621// Decodes Anthropic tool definitions into normalized tool definitions.
613622fn decode_anthropic_tools ( value : Option < & Value > ) -> Vec < ToolDefinition > {
614623 value
@@ -815,11 +824,29 @@ fn encode_one_anthropic_block(block: &ContentBlock) -> Vec<Value> {
815824 "name" : call. name,
816825 "input" : anthropic_tool_input( & call. arguments) ,
817826 } ) ] ,
818- ContentBlock :: ToolResult ( result) => vec ! [ json!( {
819- "type" : "tool_result" ,
820- "tool_use_id" : sanitize_anthropic_tool_use_id( & result. tool_call_id) ,
821- "content" : text_from_blocks( & result. content, " " ) ,
822- } ) ] ,
827+ ContentBlock :: ToolResult ( result) => {
828+ let content = if result. content . iter ( ) . all ( |block| {
829+ matches ! (
830+ block,
831+ ContentBlock :: Text { .. } | ContentBlock :: Refusal { .. }
832+ )
833+ } ) {
834+ Value :: String ( text_from_blocks ( & result. content , " " ) )
835+ } else {
836+ Value :: Array (
837+ result
838+ . content
839+ . iter ( )
840+ . flat_map ( encode_one_anthropic_tool_result_block)
841+ . collect ( ) ,
842+ )
843+ } ;
844+ vec ! [ json!( {
845+ "type" : "tool_result" ,
846+ "tool_use_id" : sanitize_anthropic_tool_use_id( & result. tool_call_id) ,
847+ "content" : content,
848+ } ) ]
849+ }
823850 ContentBlock :: Image { source } => vec ! [ match source {
824851 ImageSource :: Url { url, .. } => {
825852 json!( { "type" : "image" , "source" : { "type" : "url" , "url" : url} } )
@@ -880,6 +907,29 @@ fn encode_one_anthropic_block(block: &ContentBlock) -> Vec<Value> {
880907 }
881908}
882909
910+ // Encodes only provider-safe block shapes inside Anthropic tool results.
911+ fn encode_one_anthropic_tool_result_block ( block : & ContentBlock ) -> Vec < Value > {
912+ match block {
913+ ContentBlock :: Text { .. }
914+ | ContentBlock :: Refusal { .. }
915+ | ContentBlock :: Image { .. }
916+ | ContentBlock :: File { .. } => encode_one_anthropic_block ( block) ,
917+ ContentBlock :: Unknown { provider, raw }
918+ if provider. as_str ( ) == WireFormat :: AnthropicMessages . as_str ( ) =>
919+ {
920+ vec ! [ raw. clone( ) ]
921+ }
922+ ContentBlock :: Unknown { raw, .. } => {
923+ vec ! [ json!( { "type" : "text" , "text" : json_string( raw) } ) ]
924+ }
925+ ContentBlock :: Reasoning { .. }
926+ | ContentBlock :: Audio { .. }
927+ | ContentBlock :: Video { .. }
928+ | ContentBlock :: ToolCall ( _)
929+ | ContentBlock :: ToolResult ( _) => Vec :: new ( ) ,
930+ }
931+ }
932+
883933// Anthropic requires `tool_use.input` to be object-shaped, while OpenAI and
884934// Responses commonly carry function-call arguments as JSON strings.
885935fn anthropic_tool_input ( arguments : & Value ) -> Value {
0 commit comments